@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.
- package/README.md +68 -2
- package/dist/commands/config.d.ts +15 -0
- package/dist/commands/config.js +81 -0
- package/dist/commands/daemon.d.ts +9 -0
- package/dist/commands/daemon.js +39 -0
- package/dist/commands/list.d.ts +3 -0
- package/dist/commands/list.js +30 -5
- package/dist/commands/login.d.ts +15 -0
- package/dist/commands/login.js +97 -0
- package/dist/commands/remove.d.ts +14 -0
- package/dist/commands/remove.js +104 -0
- package/dist/commands/save.d.ts +4 -1
- package/dist/commands/save.js +24 -6
- package/dist/commands/status.d.ts +5 -0
- package/dist/commands/status.js +16 -0
- package/dist/lib/accounts/account-service.d.ts +59 -2
- package/dist/lib/accounts/account-service.js +551 -36
- package/dist/lib/accounts/auth-parser.d.ts +3 -0
- package/dist/lib/accounts/auth-parser.js +83 -0
- package/dist/lib/accounts/errors.d.ts +15 -0
- package/dist/lib/accounts/errors.js +34 -2
- package/dist/lib/accounts/index.d.ts +3 -1
- package/dist/lib/accounts/index.js +5 -1
- package/dist/lib/accounts/registry.d.ts +6 -0
- package/dist/lib/accounts/registry.js +166 -0
- package/dist/lib/accounts/service-manager.d.ts +4 -0
- package/dist/lib/accounts/service-manager.js +204 -0
- package/dist/lib/accounts/types.d.ts +71 -0
- package/dist/lib/accounts/types.js +5 -0
- package/dist/lib/accounts/usage.d.ts +10 -0
- package/dist/lib/accounts/usage.js +246 -0
- package/dist/lib/base-command.d.ts +1 -0
- package/dist/lib/base-command.js +4 -0
- package/dist/lib/config/paths.d.ts +6 -0
- package/dist/lib/config/paths.js +46 -5
- package/dist/tests/auth-parser.test.d.ts +1 -0
- package/dist/tests/auth-parser.test.js +65 -0
- package/dist/tests/registry.test.d.ts +1 -0
- package/dist/tests/registry.test.js +37 -0
- package/dist/tests/save-account-safety.test.d.ts +1 -0
- package/dist/tests/save-account-safety.test.js +399 -0
- package/dist/tests/usage.test.d.ts +1 -0
- package/dist/tests/usage.test.js +29 -0
- 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
|
|
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;
|
package/dist/commands/list.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/commands/list.js
CHANGED
|
@@ -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
|
-
|
|
8
|
-
const
|
|
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
|
|
14
|
-
const mark =
|
|
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;
|
package/dist/commands/save.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/commands/save.js
CHANGED
|
@@ -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
|
|
10
|
-
|
|
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:
|
|
19
|
-
description: "
|
|
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,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;
|