@imdeadpool/codex-account-switcher 0.1.5 → 0.1.7

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 (45) hide show
  1. package/README.md +79 -5
  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 +9 -6
  45. package/scripts/postinstall-login-hook.cjs +90 -0
package/README.md CHANGED
@@ -19,13 +19,33 @@ Codex stores your authentication session in a single `auth.json` file. This tool
19
19
  npm i -g @imdeadpool/codex-account-switcher
20
20
  ```
21
21
 
22
+ During global install, the package asks for permission to add an optional shell hook
23
+ (`~/.bashrc` or `~/.zshrc`) that auto-runs a silent snapshot sync after successful
24
+ official `codex login`.
25
+
26
+ - Choose `y` to enable fully automatic login snapshot capture.
27
+ - Choose `n` (default) to skip.
28
+ - Set `CODEX_AUTH_SKIP_POSTINSTALL=1` to always suppress this prompt.
29
+
22
30
  ## Usage
23
31
 
24
32
  ```sh
33
+ # login to Codex and immediately snapshot the refreshed auth session
34
+ codex-auth login [name]
35
+
36
+ # headless/remote login flow + snapshot
37
+ codex-auth login [name] --device-auth
38
+
39
+ # force overwrite when reusing a name across different detected identities
40
+ codex-auth login [name] --force
41
+
25
42
  # save the current logged-in token as a named account
26
43
  codex-auth save <name>
27
44
 
28
- # switch active account (symlinks on macOS/Linux; copies on Windows)
45
+ # force overwrite a name even when it currently maps to a different email
46
+ codex-auth save <name> --force
47
+
48
+ # switch active account
29
49
  codex-auth use <name>
30
50
 
31
51
  # or pick interactively
@@ -34,18 +54,72 @@ codex-auth use
34
54
  # list accounts
35
55
  codex-auth list
36
56
 
57
+ # list accounts with mapping metadata (email/account/user/usage)
58
+ codex-auth list --details
59
+
37
60
  # show current account name
38
61
  codex-auth current
62
+
63
+ # remove accounts (interactive multi-select)
64
+ codex-auth remove
65
+
66
+ # remove by selector or all
67
+ codex-auth remove <query>
68
+ codex-auth remove --all
69
+
70
+ # show auto-switch + service status
71
+ codex-auth status
72
+
73
+ # auto-switch configuration
74
+ codex-auth config auto enable
75
+ codex-auth config auto disable
76
+ codex-auth config auto --5h 12 --weekly 8
77
+
78
+ # usage source configuration
79
+ codex-auth config api enable
80
+ codex-auth config api disable
81
+
82
+ # daemon runtime (internal/service use)
83
+ codex-auth daemon --once
84
+ codex-auth daemon --watch
39
85
  ```
40
86
 
41
87
  ### Command reference
42
88
 
43
- - `codex-auth save <name>` – Validates `<name>`, ensures `auth.json` exists, then snapshots it to `~/.codex/accounts/<name>.json`.
44
- - `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 `*`.
89
+ - `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.
90
+ - `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.
91
+ - `codex-auth use [name]` – Accepts a name or launches an interactive selector with the current account pre-selected, writes `~/.codex/auth.json` as a regular file from the chosen snapshot, and records the active name.
92
+ - `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
93
  - `codex-auth current` – Prints the active account name, or a friendly message if none is active.
94
+ - `codex-auth remove [query|--all]` – Removes snapshots interactively or by selector. If the active account is removed, the best remaining account is activated automatically.
95
+ - `codex-auth status` – Prints auto-switch state, managed service status, active thresholds, and usage mode.
96
+ - `codex-auth config auto ...` – Enables/disables managed auto-switch and updates threshold percentages.
97
+ - `codex-auth config api enable|disable` – Chooses usage source mode (`api` or `local`).
98
+ - `codex-auth daemon --once|--watch` – Runs the auto-switch loop once or continuously.
99
+
100
+ ### Auto-switch behavior
101
+
102
+ When auto-switch is enabled, the daemon evaluates the active account and switches when either threshold is crossed:
103
+
104
+ - `5h` remaining `< threshold5h` (default `10%`)
105
+ - `weekly` remaining `< thresholdWeekly` (default `5%`)
106
+
107
+ Usage refresh is hybrid:
108
+
109
+ 1. API mode (`config api enable`): query ChatGPT usage endpoint for each account.
110
+ 2. Local fallback: active account usage can fall back to local session rollout logs when API data is unavailable.
111
+
112
+ ### Managed background service
113
+
114
+ `codex-auth config auto enable` installs a managed watcher per OS:
115
+
116
+ - Linux: user `systemd` service
117
+ - macOS: LaunchAgent
118
+ - Windows: Scheduled Task
119
+
120
+ `codex-auth status` reports whether the managed watcher is active.
47
121
 
48
122
  Notes:
49
123
 
50
- - Works on macOS/Linux (symlink) and Windows (copy).
124
+ - Works on macOS/Linux/Windows (regular-file auth snapshot activation).
51
125
  - Requires Node 18+.
@@ -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;