@imdeadpool/codex-account-switcher 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +68 -2
  2. package/dist/commands/config.d.ts +15 -0
  3. package/dist/commands/config.js +81 -0
  4. package/dist/commands/daemon.d.ts +9 -0
  5. package/dist/commands/daemon.js +39 -0
  6. package/dist/commands/list.d.ts +3 -0
  7. package/dist/commands/list.js +30 -5
  8. package/dist/commands/login.d.ts +15 -0
  9. package/dist/commands/login.js +97 -0
  10. package/dist/commands/remove.d.ts +14 -0
  11. package/dist/commands/remove.js +104 -0
  12. package/dist/commands/save.d.ts +4 -1
  13. package/dist/commands/save.js +24 -6
  14. package/dist/commands/status.d.ts +5 -0
  15. package/dist/commands/status.js +16 -0
  16. package/dist/lib/accounts/account-service.d.ts +59 -2
  17. package/dist/lib/accounts/account-service.js +551 -36
  18. package/dist/lib/accounts/auth-parser.d.ts +3 -0
  19. package/dist/lib/accounts/auth-parser.js +83 -0
  20. package/dist/lib/accounts/errors.d.ts +15 -0
  21. package/dist/lib/accounts/errors.js +34 -2
  22. package/dist/lib/accounts/index.d.ts +3 -1
  23. package/dist/lib/accounts/index.js +5 -1
  24. package/dist/lib/accounts/registry.d.ts +6 -0
  25. package/dist/lib/accounts/registry.js +166 -0
  26. package/dist/lib/accounts/service-manager.d.ts +4 -0
  27. package/dist/lib/accounts/service-manager.js +204 -0
  28. package/dist/lib/accounts/types.d.ts +71 -0
  29. package/dist/lib/accounts/types.js +5 -0
  30. package/dist/lib/accounts/usage.d.ts +10 -0
  31. package/dist/lib/accounts/usage.js +246 -0
  32. package/dist/lib/base-command.d.ts +1 -0
  33. package/dist/lib/base-command.js +4 -0
  34. package/dist/lib/config/paths.d.ts +6 -0
  35. package/dist/lib/config/paths.js +46 -5
  36. package/dist/tests/auth-parser.test.d.ts +1 -0
  37. package/dist/tests/auth-parser.test.js +65 -0
  38. package/dist/tests/registry.test.d.ts +1 -0
  39. package/dist/tests/registry.test.js +37 -0
  40. package/dist/tests/save-account-safety.test.d.ts +1 -0
  41. package/dist/tests/save-account-safety.test.js +399 -0
  42. package/dist/tests/usage.test.d.ts +1 -0
  43. package/dist/tests/usage.test.js +29 -0
  44. package/package.json +7 -6
package/README.md CHANGED
@@ -22,9 +22,21 @@ npm i -g @imdeadpool/codex-account-switcher
22
22
  ## Usage
23
23
 
24
24
  ```sh
25
+ # login to Codex and immediately snapshot the refreshed auth session
26
+ codex-auth login [name]
27
+
28
+ # headless/remote login flow + snapshot
29
+ codex-auth login [name] --device-auth
30
+
31
+ # force overwrite when reusing a name across different detected identities
32
+ codex-auth login [name] --force
33
+
25
34
  # save the current logged-in token as a named account
26
35
  codex-auth save <name>
27
36
 
37
+ # force overwrite a name even when it currently maps to a different email
38
+ codex-auth save <name> --force
39
+
28
40
  # switch active account (symlinks on macOS/Linux; copies on Windows)
29
41
  codex-auth use <name>
30
42
 
@@ -34,16 +46,70 @@ codex-auth use
34
46
  # list accounts
35
47
  codex-auth list
36
48
 
49
+ # list accounts with mapping metadata (email/account/user/usage)
50
+ codex-auth list --details
51
+
37
52
  # show current account name
38
53
  codex-auth current
54
+
55
+ # remove accounts (interactive multi-select)
56
+ codex-auth remove
57
+
58
+ # remove by selector or all
59
+ codex-auth remove <query>
60
+ codex-auth remove --all
61
+
62
+ # show auto-switch + service status
63
+ codex-auth status
64
+
65
+ # auto-switch configuration
66
+ codex-auth config auto enable
67
+ codex-auth config auto disable
68
+ codex-auth config auto --5h 12 --weekly 8
69
+
70
+ # usage source configuration
71
+ codex-auth config api enable
72
+ codex-auth config api disable
73
+
74
+ # daemon runtime (internal/service use)
75
+ codex-auth daemon --once
76
+ codex-auth daemon --watch
39
77
  ```
40
78
 
41
79
  ### Command reference
42
80
 
43
- - `codex-auth save <name>` – Validates `<name>`, ensures `auth.json` exists, then snapshots it to `~/.codex/accounts/<name>.json`.
81
+ - `codex-auth save <name> [--force]` – Validates `<name>`, ensures `auth.json` exists, then snapshots it to `~/.codex/accounts/<name>.json`. By default, it blocks overwriting a name when the existing snapshot email differs from current auth. If `name` is omitted, it first tries reusing the active snapshot name when identity matches; otherwise it infers one from auth email.
82
+ - `codex-auth login [name] [--device-auth] [--force]` – Runs `codex login` (optionally with device auth), waits for refreshed auth snapshot detection, then saves it. If `name` is omitted, it always infers one from auth email with unique-suffix handling for multi-workspace identities.
44
83
  - `codex-auth use [name]` – Accepts a name or launches an interactive selector with the current account pre-selected. Copies on Windows, creates a symlink elsewhere, and records the active name.
45
- - `codex-auth list` – Lists all saved snapshots alphabetically and marks the active one with `*`.
84
+ - `codex-auth list [--details]` – Lists all saved snapshots alphabetically and marks the active one with `*`. `--details` adds per-snapshot mapping metadata (email, account id, user id, and usage metadata) for easier session/account troubleshooting.
46
85
  - `codex-auth current` – Prints the active account name, or a friendly message if none is active.
86
+ - `codex-auth remove [query|--all]` – Removes snapshots interactively or by selector. If the active account is removed, the best remaining account is activated automatically.
87
+ - `codex-auth status` – Prints auto-switch state, managed service status, active thresholds, and usage mode.
88
+ - `codex-auth config auto ...` – Enables/disables managed auto-switch and updates threshold percentages.
89
+ - `codex-auth config api enable|disable` – Chooses usage source mode (`api` or `local`).
90
+ - `codex-auth daemon --once|--watch` – Runs the auto-switch loop once or continuously.
91
+
92
+ ### Auto-switch behavior
93
+
94
+ When auto-switch is enabled, the daemon evaluates the active account and switches when either threshold is crossed:
95
+
96
+ - `5h` remaining `< threshold5h` (default `10%`)
97
+ - `weekly` remaining `< thresholdWeekly` (default `5%`)
98
+
99
+ Usage refresh is hybrid:
100
+
101
+ 1. API mode (`config api enable`): query ChatGPT usage endpoint for each account.
102
+ 2. Local fallback: active account usage can fall back to local session rollout logs when API data is unavailable.
103
+
104
+ ### Managed background service
105
+
106
+ `codex-auth config auto enable` installs a managed watcher per OS:
107
+
108
+ - Linux: user `systemd` service
109
+ - macOS: LaunchAgent
110
+ - Windows: Scheduled Task
111
+
112
+ `codex-auth status` reports whether the managed watcher is active.
47
113
 
48
114
  Notes:
49
115
 
@@ -0,0 +1,15 @@
1
+ import { BaseCommand } from "../lib/base-command";
2
+ export default class ConfigCommand extends BaseCommand {
3
+ static description: string;
4
+ static args: {
5
+ readonly section: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
6
+ readonly action: import("@oclif/core/lib/interfaces").Arg<string | undefined, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ readonly "5h": import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
10
+ readonly weekly: import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
11
+ };
12
+ run(): Promise<void>;
13
+ private handleAutoConfig;
14
+ private handleApiConfig;
15
+ }
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const base_command_1 = require("../lib/base-command");
5
+ class ConfigCommand extends base_command_1.BaseCommand {
6
+ async run() {
7
+ await this.runSafe(async () => {
8
+ var _a;
9
+ const { args, flags } = await this.parse(ConfigCommand);
10
+ const section = args.section;
11
+ const action = (_a = args.action) === null || _a === void 0 ? void 0 : _a.toLowerCase();
12
+ if (section === "auto") {
13
+ await this.handleAutoConfig(action, flags["5h"], flags.weekly);
14
+ return;
15
+ }
16
+ await this.handleApiConfig(action);
17
+ });
18
+ }
19
+ async handleAutoConfig(action, threshold5h, thresholdWeekly) {
20
+ const hasThresholds = typeof threshold5h === "number" || typeof thresholdWeekly === "number";
21
+ if (action === "enable") {
22
+ if (hasThresholds) {
23
+ this.error("`config auto` cannot mix enable/disable with threshold flags.");
24
+ }
25
+ const status = await this.accounts.setAutoSwitchEnabled(true);
26
+ this.log(`auto-switch enabled; usage mode: ${status.usageMode === "api" ? "api" : "local-only"}`);
27
+ return;
28
+ }
29
+ if (action === "disable") {
30
+ if (hasThresholds) {
31
+ this.error("`config auto` cannot mix enable/disable with threshold flags.");
32
+ }
33
+ await this.accounts.setAutoSwitchEnabled(false);
34
+ this.log("auto-switch disabled");
35
+ return;
36
+ }
37
+ if (action) {
38
+ this.error(`Unknown action \"${action}\" for \`config auto\`.`);
39
+ }
40
+ if (!hasThresholds) {
41
+ this.error("`config auto` requires `enable`, `disable`, or threshold flags.");
42
+ }
43
+ const status = await this.accounts.configureAutoSwitchThresholds({
44
+ threshold5hPercent: threshold5h,
45
+ thresholdWeeklyPercent: thresholdWeekly,
46
+ });
47
+ this.log(`auto-switch thresholds updated: 5h<${status.threshold5hPercent}%, weekly<${status.thresholdWeeklyPercent}%`);
48
+ }
49
+ async handleApiConfig(action) {
50
+ if (action !== "enable" && action !== "disable") {
51
+ this.error("`config api` requires `enable` or `disable`.");
52
+ }
53
+ const status = await this.accounts.setApiUsageEnabled(action === "enable");
54
+ this.log(`usage mode: ${status.usageMode}`);
55
+ }
56
+ }
57
+ ConfigCommand.description = "Manage auto-switch and usage API configuration";
58
+ ConfigCommand.args = {
59
+ section: core_1.Args.string({
60
+ name: "section",
61
+ required: true,
62
+ options: ["auto", "api"],
63
+ description: "Config section",
64
+ }),
65
+ action: core_1.Args.string({
66
+ name: "action",
67
+ required: false,
68
+ description: "Action for the section",
69
+ }),
70
+ };
71
+ ConfigCommand.flags = {
72
+ "5h": core_1.Flags.integer({
73
+ description: "Set 5h threshold percent (1-100)",
74
+ required: false,
75
+ }),
76
+ weekly: core_1.Flags.integer({
77
+ description: "Set weekly threshold percent (1-100)",
78
+ required: false,
79
+ }),
80
+ };
81
+ exports.default = ConfigCommand;
@@ -0,0 +1,9 @@
1
+ import { BaseCommand } from "../lib/base-command";
2
+ export default class DaemonCommand extends BaseCommand {
3
+ static description: string;
4
+ static flags: {
5
+ readonly watch: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
6
+ readonly once: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
7
+ };
8
+ run(): Promise<void>;
9
+ }
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const base_command_1 = require("../lib/base-command");
5
+ class DaemonCommand extends base_command_1.BaseCommand {
6
+ async run() {
7
+ await this.runSafe(async () => {
8
+ const { flags } = await this.parse(DaemonCommand);
9
+ const watch = Boolean(flags.watch);
10
+ const once = Boolean(flags.once);
11
+ if (watch === once) {
12
+ this.error("`daemon` requires exactly one of `--watch` or `--once`.");
13
+ }
14
+ if (once) {
15
+ const result = await this.accounts.runAutoSwitchOnce();
16
+ if (result.switched) {
17
+ this.log(`switched: ${result.fromAccount} -> ${result.toAccount}`);
18
+ }
19
+ else {
20
+ this.log(`no switch: ${result.reason}`);
21
+ }
22
+ return;
23
+ }
24
+ await this.accounts.runDaemon("watch");
25
+ });
26
+ }
27
+ }
28
+ DaemonCommand.description = "Run the background auto-switch daemon";
29
+ DaemonCommand.flags = {
30
+ watch: core_1.Flags.boolean({
31
+ description: "Run continuously and evaluate switching every 30s",
32
+ default: false,
33
+ }),
34
+ once: core_1.Flags.boolean({
35
+ description: "Run one evaluation pass and exit",
36
+ default: false,
37
+ }),
38
+ };
39
+ exports.default = DaemonCommand;
@@ -1,5 +1,8 @@
1
1
  import { BaseCommand } from "../lib/base-command";
2
2
  export default class ListCommand extends BaseCommand {
3
3
  static description: string;
4
+ static flags: {
5
+ readonly details: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
6
+ };
4
7
  run(): Promise<void>;
5
8
  }
@@ -1,21 +1,46 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
3
4
  const base_command_1 = require("../lib/base-command");
4
5
  class ListCommand extends base_command_1.BaseCommand {
5
6
  async run() {
6
7
  await this.runSafe(async () => {
7
- const accounts = await this.accounts.listAccountNames();
8
- const current = await this.accounts.getCurrentAccountName();
8
+ var _a, _b, _c, _d, _e, _f;
9
+ const { flags } = await this.parse(ListCommand);
10
+ const detailed = Boolean(flags.details);
11
+ if (!detailed) {
12
+ const accounts = await this.accounts.listAccountNames();
13
+ const current = await this.accounts.getCurrentAccountName();
14
+ if (!accounts.length) {
15
+ this.log("No saved Codex accounts yet. Run `codex-auth save <name>`.");
16
+ return;
17
+ }
18
+ for (const name of accounts) {
19
+ const mark = current === name ? "*" : " ";
20
+ this.log(`${mark} ${name}`);
21
+ }
22
+ return;
23
+ }
24
+ const accounts = await this.accounts.listAccountMappings();
9
25
  if (!accounts.length) {
10
26
  this.log("No saved Codex accounts yet. Run `codex-auth save <name>`.");
11
27
  return;
12
28
  }
13
- for (const name of accounts) {
14
- const mark = current === name ? "*" : " ";
15
- this.log(`${mark} ${name}`);
29
+ for (const account of accounts) {
30
+ const mark = account.active ? "*" : " ";
31
+ this.log(`${mark} ${account.name}`);
32
+ this.log(` email=${(_a = account.email) !== null && _a !== void 0 ? _a : "-"} account=${(_b = account.accountId) !== null && _b !== void 0 ? _b : "-"} user=${(_c = account.userId) !== null && _c !== void 0 ? _c : "-"}`);
33
+ this.log(` plan=${(_d = account.planType) !== null && _d !== void 0 ? _d : "-"} usage=${(_e = account.usageSource) !== null && _e !== void 0 ? _e : "-"} lastUsageAt=${(_f = account.lastUsageAt) !== null && _f !== void 0 ? _f : "-"}`);
16
34
  }
17
35
  });
18
36
  }
19
37
  }
20
38
  ListCommand.description = "List accounts managed under ~/.codex";
39
+ ListCommand.flags = {
40
+ details: core_1.Flags.boolean({
41
+ char: "d",
42
+ description: "Show per-account mapping metadata (email/account/user/usage)",
43
+ default: false,
44
+ }),
45
+ };
21
46
  exports.default = ListCommand;
@@ -0,0 +1,15 @@
1
+ import { BaseCommand } from "../lib/base-command";
2
+ export default class LoginCommand extends BaseCommand {
3
+ protected readonly syncExternalAuthBeforeRun = false;
4
+ static description: string;
5
+ static args: {
6
+ readonly name: import("@oclif/core/lib/interfaces").Arg<string | undefined, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ readonly "device-auth": import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
10
+ readonly force: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
11
+ };
12
+ run(): Promise<void>;
13
+ private runCodexLogin;
14
+ private waitForCodexAuthSnapshot;
15
+ }
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const node_child_process_1 = require("node:child_process");
5
+ const base_command_1 = require("../lib/base-command");
6
+ const accounts_1 = require("../lib/accounts");
7
+ const auth_parser_1 = require("../lib/accounts/auth-parser");
8
+ const paths_1 = require("../lib/config/paths");
9
+ class LoginCommand extends base_command_1.BaseCommand {
10
+ constructor() {
11
+ super(...arguments);
12
+ this.syncExternalAuthBeforeRun = false;
13
+ }
14
+ async run() {
15
+ await this.runSafe(async () => {
16
+ const { args, flags } = await this.parse(LoginCommand);
17
+ const providedName = args.name;
18
+ const status = await this.accounts.getStatus();
19
+ if (status.autoSwitchEnabled) {
20
+ await this.accounts.setAutoSwitchEnabled(false);
21
+ this.log("Auto-switch disabled before login.");
22
+ }
23
+ await this.runCodexLogin(Boolean(flags["device-auth"]));
24
+ await this.waitForCodexAuthSnapshot();
25
+ const resolvedName = providedName
26
+ ? { name: providedName, source: "explicit" }
27
+ : await this.accounts.resolveLoginAccountNameFromCurrentAuth();
28
+ const forceOverwrite = Boolean(flags.force);
29
+ const savedName = await this.accounts.saveAccount(resolvedName.name, {
30
+ force: forceOverwrite,
31
+ });
32
+ const suffix = resolvedName.source === "explicit" ? "" : " (inferred from auth email)";
33
+ this.log(`Saved current Codex auth tokens as "${savedName}"${suffix}.`);
34
+ });
35
+ }
36
+ async runCodexLogin(deviceAuth) {
37
+ const loginArgs = deviceAuth ? ["login", "--device-auth"] : ["login"];
38
+ await new Promise((resolve, reject) => {
39
+ const child = (0, node_child_process_1.spawn)("codex", loginArgs, {
40
+ stdio: "inherit",
41
+ });
42
+ child.on("error", (error) => {
43
+ const err = error;
44
+ if (err.code === "ENOENT") {
45
+ reject(new accounts_1.CodexAuthError("`codex` CLI was not found in PATH. Install Codex CLI first, then retry."));
46
+ return;
47
+ }
48
+ reject(error);
49
+ });
50
+ child.on("exit", (code, signal) => {
51
+ if (code === 0) {
52
+ resolve();
53
+ return;
54
+ }
55
+ if (typeof code === "number") {
56
+ reject(new accounts_1.CodexAuthError(`\`codex ${loginArgs.join(" ")}\` failed with exit code ${code}.`));
57
+ return;
58
+ }
59
+ reject(new accounts_1.CodexAuthError(`\`codex ${loginArgs.join(" ")}\` was terminated by signal ${signal !== null && signal !== void 0 ? signal : "unknown"}.`));
60
+ });
61
+ });
62
+ }
63
+ async waitForCodexAuthSnapshot() {
64
+ const authPath = (0, paths_1.resolveAuthPath)();
65
+ const timeoutMs = 5000;
66
+ const pollMs = 200;
67
+ const deadline = Date.now() + timeoutMs;
68
+ while (Date.now() <= deadline) {
69
+ const parsed = await (0, auth_parser_1.parseAuthSnapshotFile)(authPath);
70
+ if (parsed.authMode !== "unknown") {
71
+ return;
72
+ }
73
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
74
+ }
75
+ throw new accounts_1.CodexAuthError("Timed out waiting for refreshed Codex auth snapshot after login. Retry `codex-auth login`.");
76
+ }
77
+ }
78
+ LoginCommand.description = "Run `codex login` and save the resulting ~/.codex/auth.json as a named account (or infer one from auth email)";
79
+ LoginCommand.args = {
80
+ name: core_1.Args.string({
81
+ name: "name",
82
+ required: false,
83
+ description: "Optional account snapshot name. If omitted, inferred from auth email",
84
+ }),
85
+ };
86
+ LoginCommand.flags = {
87
+ "device-auth": core_1.Flags.boolean({
88
+ description: "Pass through to `codex login --device-auth`",
89
+ default: false,
90
+ }),
91
+ force: core_1.Flags.boolean({
92
+ char: "f",
93
+ description: "Force overwrite when the existing snapshot name belongs to a different detected account identity",
94
+ default: false,
95
+ }),
96
+ };
97
+ exports.default = LoginCommand;
@@ -0,0 +1,14 @@
1
+ import { BaseCommand } from "../lib/base-command";
2
+ export default class RemoveCommand extends BaseCommand {
3
+ static description: string;
4
+ static args: {
5
+ readonly query: import("@oclif/core/lib/interfaces").Arg<string | undefined, Record<string, unknown>>;
6
+ };
7
+ static flags: {
8
+ readonly all: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
9
+ };
10
+ run(): Promise<void>;
11
+ private selectQueryMatches;
12
+ private promptForAccounts;
13
+ private buildChoiceLabel;
14
+ }
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const core_1 = require("@oclif/core");
7
+ const prompts_1 = __importDefault(require("prompts"));
8
+ const base_command_1 = require("../lib/base-command");
9
+ const accounts_1 = require("../lib/accounts");
10
+ class RemoveCommand extends base_command_1.BaseCommand {
11
+ async run() {
12
+ await this.runSafe(async () => {
13
+ const { args, flags } = await this.parse(RemoveCommand);
14
+ const query = args.query;
15
+ const removeAll = Boolean(flags.all);
16
+ if (query && removeAll) {
17
+ this.error("`remove` cannot combine a query with `--all`.");
18
+ }
19
+ let selectedNames;
20
+ if (removeAll) {
21
+ selectedNames = (await this.accounts.listAccountNames()).slice();
22
+ }
23
+ else if (query) {
24
+ selectedNames = await this.selectQueryMatches(query);
25
+ }
26
+ else {
27
+ selectedNames = await this.promptForAccounts(await this.accounts.listAccountChoices());
28
+ }
29
+ if (selectedNames.length === 0) {
30
+ throw new accounts_1.InvalidRemoveSelectionError();
31
+ }
32
+ const result = await this.accounts.removeAccounts(selectedNames);
33
+ this.log(`Removed ${result.removed.length} account(s): ${result.removed.join(", ")}`);
34
+ if (result.activated) {
35
+ this.log(`Activated fallback account: ${result.activated}`);
36
+ }
37
+ });
38
+ }
39
+ async selectQueryMatches(query) {
40
+ const matches = await this.accounts.findMatchingAccounts(query);
41
+ if (matches.length === 0) {
42
+ throw new accounts_1.AccountNotFoundError(query);
43
+ }
44
+ if (matches.length === 1) {
45
+ return [matches[0].name];
46
+ }
47
+ if (!process.stdin.isTTY) {
48
+ this.error(`Query "${query}" matched multiple accounts in non-interactive mode. Refine the query or run with a TTY.`);
49
+ }
50
+ return this.promptForAccounts(matches);
51
+ }
52
+ async promptForAccounts(choices) {
53
+ if (choices.length === 0) {
54
+ throw new accounts_1.AccountNotFoundError("*");
55
+ }
56
+ const response = await (0, prompts_1.default)({
57
+ type: "multiselect",
58
+ name: "accounts",
59
+ message: "Select accounts to remove",
60
+ choices: choices.map((choice) => ({
61
+ title: this.buildChoiceLabel(choice),
62
+ value: choice.name,
63
+ selected: false,
64
+ })),
65
+ instructions: false,
66
+ hint: "Space to toggle, Enter to confirm",
67
+ }, {
68
+ onCancel: () => {
69
+ throw new accounts_1.PromptCancelledError();
70
+ },
71
+ });
72
+ const accounts = response.accounts;
73
+ if (!accounts) {
74
+ throw new accounts_1.PromptCancelledError();
75
+ }
76
+ return accounts;
77
+ }
78
+ buildChoiceLabel(choice) {
79
+ const parts = [choice.name];
80
+ if (choice.email) {
81
+ parts.push(`<${choice.email}>`);
82
+ }
83
+ if (choice.active) {
84
+ parts.push("(active)");
85
+ }
86
+ return parts.join(" ");
87
+ }
88
+ }
89
+ RemoveCommand.description = "Remove accounts with interactive multi-select";
90
+ RemoveCommand.args = {
91
+ query: core_1.Args.string({
92
+ name: "query",
93
+ required: false,
94
+ description: "Account selector by name or email fragment",
95
+ }),
96
+ };
97
+ RemoveCommand.flags = {
98
+ all: core_1.Flags.boolean({
99
+ char: "a",
100
+ description: "Remove all saved accounts",
101
+ default: false,
102
+ }),
103
+ };
104
+ exports.default = RemoveCommand;
@@ -2,7 +2,10 @@ import { BaseCommand } from "../lib/base-command";
2
2
  export default class SaveCommand extends BaseCommand {
3
3
  static description: string;
4
4
  static args: {
5
- readonly name: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
5
+ readonly name: import("@oclif/core/lib/interfaces").Arg<string | undefined, Record<string, unknown>>;
6
+ };
7
+ static flags: {
8
+ readonly force: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
6
9
  };
7
10
  run(): Promise<void>;
8
11
  }
@@ -5,18 +5,36 @@ const base_command_1 = require("../lib/base-command");
5
5
  class SaveCommand extends base_command_1.BaseCommand {
6
6
  async run() {
7
7
  await this.runSafe(async () => {
8
- const { args } = await this.parse(SaveCommand);
9
- const savedName = await this.accounts.saveAccount(args.name);
10
- this.log(`Saved current Codex auth tokens as "${savedName}".`);
8
+ const { args, flags } = await this.parse(SaveCommand);
9
+ const providedName = args.name;
10
+ const resolvedName = providedName
11
+ ? { name: providedName, source: "explicit" }
12
+ : await this.accounts.resolveDefaultAccountNameFromCurrentAuth();
13
+ const savedName = await this.accounts.saveAccount(resolvedName.name, {
14
+ force: Boolean(flags.force),
15
+ });
16
+ const suffix = resolvedName.source === "explicit"
17
+ ? ""
18
+ : resolvedName.source === "active"
19
+ ? " (reused active account name)"
20
+ : " (inferred from auth email)";
21
+ this.log(`Saved current Codex auth tokens as "${savedName}"${suffix}.`);
11
22
  });
12
23
  }
13
24
  }
14
- SaveCommand.description = "Save the current ~/.codex/auth.json as a named account";
25
+ SaveCommand.description = "Save the current ~/.codex/auth.json as a named account (or infer one from auth email)";
15
26
  SaveCommand.args = {
16
27
  name: core_1.Args.string({
17
28
  name: "name",
18
- required: true,
19
- description: "Name for the account snapshot",
29
+ required: false,
30
+ description: "Optional account snapshot name. If omitted, inferred from auth email",
31
+ }),
32
+ };
33
+ SaveCommand.flags = {
34
+ force: core_1.Flags.boolean({
35
+ char: "f",
36
+ description: "Force overwrite when the existing snapshot name belongs to a different email account",
37
+ default: false,
20
38
  }),
21
39
  };
22
40
  exports.default = SaveCommand;
@@ -0,0 +1,5 @@
1
+ import { BaseCommand } from "../lib/base-command";
2
+ export default class StatusCommand extends BaseCommand {
3
+ static description: string;
4
+ run(): Promise<void>;
5
+ }
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const base_command_1 = require("../lib/base-command");
4
+ class StatusCommand extends base_command_1.BaseCommand {
5
+ async run() {
6
+ await this.runSafe(async () => {
7
+ const status = await this.accounts.getStatus();
8
+ this.log(`auto-switch: ${status.autoSwitchEnabled ? "ON" : "OFF"}`);
9
+ this.log(`service: ${status.serviceState}`);
10
+ this.log(`thresholds: 5h<${status.threshold5hPercent}%, weekly<${status.thresholdWeeklyPercent}%`);
11
+ this.log(`usage: ${status.usageMode}`);
12
+ });
13
+ }
14
+ }
15
+ StatusCommand.description = "Show auto-switch, service, and usage status";
16
+ exports.default = StatusCommand;