Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 12 additions & 39 deletions lib/codex-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
extractAccountEmail,
extractAccountId,
formatAccountLabel,
formatCooldown,
formatWaitTime,
getAccountIdCandidates,
resolveRequestAccountId,
Expand Down Expand Up @@ -37,6 +36,10 @@ import {
loadCodexCliState,
} from "./codex-cli/state.js";
import { setCodexCliActiveSelection } from "./codex-cli/writer.js";
import {
runFeaturesCommand,
runStatusCommand,
} from "./codex-manager/commands/status.js";
import { runSwitchCommand } from "./codex-manager/commands/switch.js";
import {
applyUiThemeFromDashboardSettings,
Expand Down Expand Up @@ -431,12 +434,7 @@ const IMPLEMENTED_FEATURES: ImplementedFeature[] = [
];

function runFeaturesReport(): number {
console.log(`Implemented features (${IMPLEMENTED_FEATURES.length})`);
console.log("");
for (const feature of IMPLEMENTED_FEATURES) {
console.log(`${feature.id}. ${feature.name}`);
}
return 0;
return runFeaturesCommand({ implementedFeatures: IMPLEMENTED_FEATURES });
}

function resolveActiveIndex(
Expand Down Expand Up @@ -1985,38 +1983,13 @@ async function syncSelectionToCodex(
}

async function showAccountStatus(): Promise<void> {
setStoragePath(null);
const storage = await loadAccounts();
const path = getStoragePath();
if (!storage || storage.accounts.length === 0) {
console.log("No accounts configured.");
console.log(`Storage: ${path}`);
return;
}

const now = Date.now();
const activeIndex = resolveActiveIndex(storage, "codex");
console.log(`Accounts (${storage.accounts.length})`);
console.log(`Storage: ${path}`);
console.log("");
for (let i = 0; i < storage.accounts.length; i += 1) {
const account = storage.accounts[i];
if (!account) continue;
const label = formatAccountLabel(account, i);
const markers: string[] = [];
if (i === activeIndex) markers.push("current");
if (account.enabled === false) markers.push("disabled");
const rateLimit = formatRateLimitEntry(account, now, "codex");
if (rateLimit) markers.push("rate-limited");
const cooldown = formatCooldown(account, now);
if (cooldown) markers.push(`cooldown:${cooldown}`);
const markerLabel = markers.length > 0 ? ` [${markers.join(", ")}]` : "";
const lastUsed =
typeof account.lastUsed === "number" && account.lastUsed > 0
? `used ${formatWaitTime(now - account.lastUsed)} ago`
: "never used";
console.log(`${i + 1}. ${label}${markerLabel} ${lastUsed}`);
}
await runStatusCommand({
setStoragePath,
getStoragePath,
loadAccounts,
resolveActiveIndex,
formatRateLimitEntry,
});
}

interface HealthCheckOptions {
Expand Down
82 changes: 82 additions & 0 deletions lib/codex-manager/commands/status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {
formatAccountLabel,
formatCooldown,
formatWaitTime,
} from "../../accounts.js";
import type { ModelFamily } from "../../prompts/codex.js";
import type { AccountStorageV3 } from "../../storage.js";

type LoadedStorage = AccountStorageV3 | null;

export interface StatusCommandDeps {
setStoragePath: (path: string | null) => void;
getStoragePath: () => string | null;
loadAccounts: () => Promise<LoadedStorage>;
resolveActiveIndex: (
storage: AccountStorageV3,
family?: ModelFamily,
) => number;
formatRateLimitEntry: (
account: AccountStorageV3["accounts"][number],
now: number,
family: ModelFamily,
) => string | null;
getNow?: () => number;
logInfo?: (message: string) => void;
}

export async function runStatusCommand(
deps: StatusCommandDeps,
): Promise<number> {
deps.setStoragePath(null);
const storage = await deps.loadAccounts();
const path = deps.getStoragePath();
const logInfo = deps.logInfo ?? console.log;
if (!storage || storage.accounts.length === 0) {
logInfo("No accounts configured.");
logInfo(`Storage: ${path}`);
return 0;
}

const now = deps.getNow?.() ?? Date.now();
const activeIndex = deps.resolveActiveIndex(storage, "codex");
logInfo(`Accounts (${storage.accounts.length})`);
logInfo(`Storage: ${path}`);
logInfo("");

for (let i = 0; i < storage.accounts.length; i += 1) {
const account = storage.accounts[i];
if (!account) continue;
const label = formatAccountLabel(account, i);
const markers: string[] = [];
if (i === activeIndex) markers.push("current");
if (account.enabled === false) markers.push("disabled");
const rateLimit = deps.formatRateLimitEntry(account, now, "codex");
if (rateLimit) markers.push("rate-limited");
const cooldown = formatCooldown(account, now);
if (cooldown) markers.push(`cooldown:${cooldown}`);
const markerLabel = markers.length > 0 ? ` [${markers.join(", ")}]` : "";
const lastUsed =
typeof account.lastUsed === "number" && account.lastUsed > 0
? `used ${formatWaitTime(now - account.lastUsed)} ago`
: "never used";
logInfo(`${i + 1}. ${label}${markerLabel} ${lastUsed}`);
}

return 0;
}

export interface FeaturesCommandDeps {
implementedFeatures: ReadonlyArray<{ id: number; name: string }>;
logInfo?: (message: string) => void;
}

export function runFeaturesCommand(deps: FeaturesCommandDeps): number {
const logInfo = deps.logInfo ?? console.log;
logInfo(`Implemented features (${deps.implementedFeatures.length})`);
logInfo("");
for (const feature of deps.implementedFeatures) {
logInfo(`${feature.id}. ${feature.name}`);
}
return 0;
}
101 changes: 101 additions & 0 deletions test/codex-manager-status-command.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { describe, expect, it, vi } from "vitest";
import {
type FeaturesCommandDeps,
runFeaturesCommand,
runStatusCommand,
type StatusCommandDeps,
} from "../lib/codex-manager/commands/status.js";
import type { AccountStorageV3 } from "../lib/storage.js";

function createStorage(): AccountStorageV3 {
return {
version: 3,
activeIndex: 0,
activeIndexByFamily: { codex: 0 },
accounts: [
{
email: "one@example.com",
refreshToken: "refresh-token-1",
addedAt: 1,
lastUsed: 1,
},
{
email: "two@example.com",
refreshToken: "refresh-token-2",
addedAt: 2,
lastUsed: 2,
enabled: false,
},
],
};
}

function createStatusDeps(
overrides: Partial<StatusCommandDeps> = {},
): StatusCommandDeps {
return {
setStoragePath: vi.fn(),
getStoragePath: vi.fn(() => "/tmp/codex.json"),
loadAccounts: vi.fn(async () => createStorage()),
resolveActiveIndex: vi.fn(() => 0),
formatRateLimitEntry: vi.fn(() => null),
getNow: vi.fn(() => 2_000),
logInfo: vi.fn(),
...overrides,
};
}

describe("runStatusCommand", () => {
it("prints empty storage state", async () => {
const deps = createStatusDeps({ loadAccounts: vi.fn(async () => null) });

const result = await runStatusCommand(deps);

expect(result).toBe(0);
expect(deps.getStoragePath).toHaveBeenCalledTimes(1);
expect(deps.logInfo).toHaveBeenCalledWith("No accounts configured.");
expect(deps.logInfo).toHaveBeenCalledWith("Storage: /tmp/codex.json");
});

it("prints account rows with current and disabled markers", async () => {
const deps = createStatusDeps({
formatRateLimitEntry: vi.fn((_account, _now, _family) => "limited"),
});

const result = await runStatusCommand(deps);

expect(result).toBe(0);
expect(deps.getStoragePath).toHaveBeenCalledTimes(1);
expect(deps.logInfo).toHaveBeenCalledWith("Accounts (2)");
expect(deps.logInfo).toHaveBeenCalledWith("Storage: /tmp/codex.json");
expect(deps.logInfo).toHaveBeenCalledWith(
expect.stringContaining(
"1. Account 1 (one@example.com) [current, rate-limited]",
),
);
expect(deps.logInfo).toHaveBeenCalledWith(
expect.stringContaining(
"2. Account 2 (two@example.com) [disabled, rate-limited]",
),
);
});
});

describe("runFeaturesCommand", () => {
it("prints the implemented feature list", () => {
const deps: FeaturesCommandDeps = {
implementedFeatures: [
{ id: 1, name: "Alpha" },
{ id: 2, name: "Beta" },
],
logInfo: vi.fn(),
};

const result = runFeaturesCommand(deps);

expect(result).toBe(0);
expect(deps.logInfo).toHaveBeenCalledWith("Implemented features (2)");
expect(deps.logInfo).toHaveBeenCalledWith("1. Alpha");
expect(deps.logInfo).toHaveBeenCalledWith("2. Beta");
});
});