@imdeadpool/codex-account-switcher 0.1.9 → 0.1.11
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 +17 -2
- package/dist/commands/list.js +5 -2
- package/dist/commands/restore-session.d.ts +7 -0
- package/dist/commands/restore-session.js +17 -0
- package/dist/commands/self-update.d.ts +2 -0
- package/dist/commands/self-update.js +45 -5
- package/dist/hooks/init/update-notifier.js +5 -2
- package/dist/lib/accounts/account-service.d.ts +11 -0
- package/dist/lib/accounts/account-service.js +174 -3
- package/dist/lib/config/login-hook.js +16 -14
- package/dist/lib/config/paths.d.ts +2 -0
- package/dist/lib/config/paths.js +10 -1
- package/dist/lib/update-check.d.ts +9 -0
- package/dist/lib/update-check.js +41 -0
- package/dist/tests/login-hook.test.js +15 -0
- package/dist/tests/save-account-safety.test.js +137 -0
- package/dist/tests/update-check.test.js +38 -0
- package/package.json +1 -1
- package/scripts/postinstall-login-hook.cjs +10 -0
package/README.md
CHANGED
|
@@ -9,6 +9,8 @@ A command-line tool that lets you manage and switch between multiple Codex accou
|
|
|
9
9
|
|
|
10
10
|
Codex stores your authentication session in a single `auth.json` file. This tool works by creating named snapshots of that file for each of your accounts. When you want to switch, `codex-auth` swaps the active `~/.codex/auth.json` with the snapshot you select, instantly changing your logged-in account.
|
|
11
11
|
|
|
12
|
+
`codex-auth` also keeps a lightweight per-terminal session memory (scoped by shell parent PID by default), so older terminals keep using their original snapshot even if a different terminal logs into another account later.
|
|
13
|
+
|
|
12
14
|
## Requirements
|
|
13
15
|
|
|
14
16
|
- Node.js 18 or newer
|
|
@@ -26,6 +28,16 @@ official `codex login`.
|
|
|
26
28
|
- Choose `y` to enable fully automatic login snapshot capture.
|
|
27
29
|
- Choose `n` (default) to skip.
|
|
28
30
|
- Set `CODEX_AUTH_SKIP_POSTINSTALL=1` to always suppress this prompt.
|
|
31
|
+
- Set `CODEX_AUTH_SKIP_TTY_RESTORE=1` to keep the hook from restoring terminal modes after `codex` exits.
|
|
32
|
+
- Set `CODEX_AUTH_SESSION_KEY=<id>` to explicitly scope session-memory identity (optional; default uses shell PPID).
|
|
33
|
+
- For a calmer Codex footer, prefer a focused `[tui] status_line` such as:
|
|
34
|
+
|
|
35
|
+
```toml
|
|
36
|
+
[tui]
|
|
37
|
+
status_line = ["model-with-reasoning", "git-branch", "context-remaining"]
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This remains a manual Codex config choice; `codex-auth` does not rewrite `~/.codex/config.toml`.
|
|
29
41
|
|
|
30
42
|
## Usage
|
|
31
43
|
|
|
@@ -66,6 +78,9 @@ codex-auth self-update
|
|
|
66
78
|
# check only (no install)
|
|
67
79
|
codex-auth self-update --check
|
|
68
80
|
|
|
81
|
+
# reinstall latest even if already up to date
|
|
82
|
+
codex-auth self-update --reinstall
|
|
83
|
+
|
|
69
84
|
# remove accounts (interactive multi-select)
|
|
70
85
|
codex-auth remove
|
|
71
86
|
|
|
@@ -102,13 +117,13 @@ codex-auth remove-login-hook
|
|
|
102
117
|
- `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.
|
|
103
118
|
- `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.
|
|
104
119
|
- `codex-auth current` – Prints the active account name, or a friendly message if none is active.
|
|
105
|
-
- `codex-auth self-update [--check]` – Checks npm for
|
|
120
|
+
- `codex-auth self-update [--check] [--reinstall] [-y]` – Checks npm for newer release metadata. `--check` prints current/latest/status only. `--reinstall` forces reinstall even when already up to date. `-y` skips confirmation prompts.
|
|
106
121
|
- `codex-auth remove [query|--all]` – Removes snapshots interactively or by selector. If the active account is removed, the best remaining account is activated automatically.
|
|
107
122
|
- `codex-auth status` – Prints auto-switch state, managed service status, active thresholds, and usage mode.
|
|
108
123
|
- `codex-auth config auto ...` – Enables/disables managed auto-switch and updates threshold percentages.
|
|
109
124
|
- `codex-auth config api enable|disable` – Chooses usage source mode (`api` or `local`).
|
|
110
125
|
- `codex-auth daemon --once|--watch` – Runs the auto-switch loop once or continuously.
|
|
111
|
-
- `codex-auth setup-login-hook [-f <path>]` – Installs an optional shell hook in your rc file to
|
|
126
|
+
- `codex-auth setup-login-hook [-f <path>]` – Installs an optional shell hook in your rc file to restore session-pinned snapshot before each `codex` run, refresh codex-auth session memory after each `codex` exit, and restore common terminal modes before returning to your prompt.
|
|
112
127
|
- `codex-auth hook-status [-f <path>]` – Shows whether the optional login auto-snapshot hook is installed for the selected rc file.
|
|
113
128
|
- `codex-auth remove-login-hook [-f <path>]` – Removes the optional shell hook.
|
|
114
129
|
|
package/dist/commands/list.js
CHANGED
|
@@ -47,9 +47,12 @@ class ListCommand extends base_command_1.BaseCommand {
|
|
|
47
47
|
if (!currentVersion || typeof currentVersion !== "string")
|
|
48
48
|
return;
|
|
49
49
|
const latestVersion = await (0, update_check_1.fetchLatestNpmVersion)(update_check_1.PACKAGE_NAME);
|
|
50
|
-
if (!latestVersion
|
|
50
|
+
if (!latestVersion)
|
|
51
51
|
return;
|
|
52
|
-
|
|
52
|
+
const summary = (0, update_check_1.getUpdateSummary)(currentVersion, latestVersion);
|
|
53
|
+
if (summary.state !== "update-available")
|
|
54
|
+
return;
|
|
55
|
+
this.log((0, update_check_1.formatUpdateSummaryInline)(summary));
|
|
53
56
|
const prompt = await (0, prompts_1.default)({
|
|
54
57
|
type: "confirm",
|
|
55
58
|
name: "install",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const base_command_1 = require("../lib/base-command");
|
|
4
|
+
class RestoreSessionCommand extends base_command_1.BaseCommand {
|
|
5
|
+
constructor() {
|
|
6
|
+
super(...arguments);
|
|
7
|
+
this.syncExternalAuthBeforeRun = false;
|
|
8
|
+
}
|
|
9
|
+
async run() {
|
|
10
|
+
await this.runSafe(async () => {
|
|
11
|
+
await this.accounts.restoreSessionSnapshotIfNeeded();
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
RestoreSessionCommand.hidden = true;
|
|
16
|
+
RestoreSessionCommand.description = "Restore auth.json from the session-pinned snapshot (internal helper)";
|
|
17
|
+
exports.default = RestoreSessionCommand;
|
|
@@ -3,6 +3,8 @@ export default class SelfUpdateCommand extends BaseCommand {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static flags: {
|
|
5
5
|
readonly check: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
6
|
+
readonly reinstall: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
7
|
+
readonly yes: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
6
8
|
};
|
|
7
9
|
run(): Promise<void>;
|
|
8
10
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
const core_1 = require("@oclif/core");
|
|
7
|
+
const prompts_1 = __importDefault(require("prompts"));
|
|
4
8
|
const base_command_1 = require("../lib/base-command");
|
|
5
9
|
const update_check_1 = require("../lib/update-check");
|
|
6
10
|
class SelfUpdateCommand extends base_command_1.BaseCommand {
|
|
@@ -13,17 +17,43 @@ class SelfUpdateCommand extends base_command_1.BaseCommand {
|
|
|
13
17
|
this.warn("Could not check npm for the latest release right now.");
|
|
14
18
|
return;
|
|
15
19
|
}
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
const summary = (0, update_check_1.getUpdateSummary)(currentVersion, latestVersion);
|
|
21
|
+
const hasUpdate = summary.state === "update-available";
|
|
22
|
+
if (flags.check) {
|
|
23
|
+
for (const line of (0, update_check_1.formatUpdateSummaryCard)(summary)) {
|
|
24
|
+
this.log(line);
|
|
25
|
+
}
|
|
26
|
+
if (hasUpdate) {
|
|
27
|
+
this.log("Run `codex-auth self-update` to install the latest release.");
|
|
28
|
+
}
|
|
18
29
|
return;
|
|
19
30
|
}
|
|
20
|
-
|
|
21
|
-
|
|
31
|
+
if (!hasUpdate && !flags.reinstall) {
|
|
32
|
+
this.log((0, update_check_1.formatUpdateSummaryInline)(summary));
|
|
33
|
+
this.log("Use `codex-auth self-update --reinstall` if you want to reinstall anyway.");
|
|
22
34
|
return;
|
|
23
35
|
}
|
|
36
|
+
if (hasUpdate) {
|
|
37
|
+
this.log((0, update_check_1.formatUpdateSummaryInline)(summary));
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
this.log(`↺ Reinstall requested for latest version (${latestVersion}).`);
|
|
41
|
+
}
|
|
42
|
+
if (!flags.yes) {
|
|
43
|
+
const response = await (0, prompts_1.default)({
|
|
44
|
+
type: "confirm",
|
|
45
|
+
name: "proceed",
|
|
46
|
+
message: "Proceed with global npm update now?",
|
|
47
|
+
initial: true,
|
|
48
|
+
});
|
|
49
|
+
if (!response.proceed) {
|
|
50
|
+
this.log("Update cancelled.");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
24
54
|
const exitCode = await (0, update_check_1.runGlobalNpmInstall)(update_check_1.PACKAGE_NAME);
|
|
25
55
|
if (exitCode === 0) {
|
|
26
|
-
this.log(
|
|
56
|
+
this.log(`✓ Global update completed (installed ${latestVersion}).`);
|
|
27
57
|
return;
|
|
28
58
|
}
|
|
29
59
|
this.warn(`Global update failed (exit code ${exitCode}). Run: npm i -g ${update_check_1.PACKAGE_NAME}@latest`);
|
|
@@ -36,5 +66,15 @@ SelfUpdateCommand.flags = {
|
|
|
36
66
|
description: "Only check whether an update is available",
|
|
37
67
|
default: false,
|
|
38
68
|
}),
|
|
69
|
+
reinstall: core_1.Flags.boolean({
|
|
70
|
+
char: "r",
|
|
71
|
+
description: "Reinstall the latest version even when already up to date",
|
|
72
|
+
default: false,
|
|
73
|
+
}),
|
|
74
|
+
yes: core_1.Flags.boolean({
|
|
75
|
+
char: "y",
|
|
76
|
+
description: "Skip confirmation prompts",
|
|
77
|
+
default: false,
|
|
78
|
+
}),
|
|
39
79
|
};
|
|
40
80
|
exports.default = SelfUpdateCommand;
|
|
@@ -12,9 +12,12 @@ const hook = async function (options) {
|
|
|
12
12
|
if (!currentVersion)
|
|
13
13
|
return;
|
|
14
14
|
const latestVersion = await (0, update_check_1.fetchLatestNpmVersion)(update_check_1.PACKAGE_NAME);
|
|
15
|
-
if (!latestVersion
|
|
15
|
+
if (!latestVersion)
|
|
16
16
|
return;
|
|
17
|
-
|
|
17
|
+
const summary = (0, update_check_1.getUpdateSummary)(currentVersion, latestVersion);
|
|
18
|
+
if (summary.state !== "update-available")
|
|
19
|
+
return;
|
|
20
|
+
this.log((0, update_check_1.formatUpdateSummaryInline)(summary));
|
|
18
21
|
this.log("Run `codex-auth self-update` to install the latest version.");
|
|
19
22
|
};
|
|
20
23
|
exports.default = hook;
|
|
@@ -26,6 +26,10 @@ export interface ExternalAuthSyncResult {
|
|
|
26
26
|
}
|
|
27
27
|
export declare class AccountService {
|
|
28
28
|
syncExternalAuthSnapshotIfNeeded(): Promise<ExternalAuthSyncResult>;
|
|
29
|
+
restoreSessionSnapshotIfNeeded(): Promise<{
|
|
30
|
+
restored: boolean;
|
|
31
|
+
accountName?: string;
|
|
32
|
+
}>;
|
|
29
33
|
listAccountNames(): Promise<string[]>;
|
|
30
34
|
listAccountChoices(): Promise<AccountChoice[]>;
|
|
31
35
|
listAccountMappings(): Promise<AccountMapping[]>;
|
|
@@ -68,6 +72,13 @@ export declare class AccountService {
|
|
|
68
72
|
private persistRegistry;
|
|
69
73
|
private activateSnapshot;
|
|
70
74
|
private clearActivePointers;
|
|
75
|
+
private isExternalSyncForced;
|
|
76
|
+
private resolveSessionScopeKey;
|
|
77
|
+
private getSessionAccountName;
|
|
78
|
+
private setSessionAccountName;
|
|
79
|
+
private clearSessionAccountName;
|
|
80
|
+
private readSessionMap;
|
|
81
|
+
private writeSessionMap;
|
|
71
82
|
private snapshotsShareIdentity;
|
|
72
83
|
private renderSnapshotIdentity;
|
|
73
84
|
}
|
|
@@ -14,6 +14,8 @@ const registry_1 = require("./registry");
|
|
|
14
14
|
const usage_1 = require("./usage");
|
|
15
15
|
const service_manager_1 = require("./service-manager");
|
|
16
16
|
const ACCOUNT_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._@+-]*$/;
|
|
17
|
+
const EXTERNAL_SYNC_FORCE_ENV = "CODEX_AUTH_FORCE_EXTERNAL_SYNC";
|
|
18
|
+
const SESSION_KEY_ENV = "CODEX_AUTH_SESSION_KEY";
|
|
17
19
|
class AccountService {
|
|
18
20
|
async syncExternalAuthSnapshotIfNeeded() {
|
|
19
21
|
const authPath = (0, paths_1.resolveAuthPath)();
|
|
@@ -31,6 +33,21 @@ class AccountService {
|
|
|
31
33
|
autoSwitchDisabled: false,
|
|
32
34
|
};
|
|
33
35
|
}
|
|
36
|
+
const sessionAccountName = await this.getSessionAccountName();
|
|
37
|
+
if (sessionAccountName) {
|
|
38
|
+
const sessionSnapshotPath = this.accountFilePath(sessionAccountName);
|
|
39
|
+
if (await this.pathExists(sessionSnapshotPath)) {
|
|
40
|
+
const sessionSnapshot = await (0, auth_parser_1.parseAuthSnapshotFile)(sessionSnapshotPath);
|
|
41
|
+
if (sessionSnapshot.authMode === "chatgpt" &&
|
|
42
|
+
!this.snapshotsShareIdentity(sessionSnapshot, incomingSnapshot) &&
|
|
43
|
+
!this.isExternalSyncForced()) {
|
|
44
|
+
return {
|
|
45
|
+
synchronized: false,
|
|
46
|
+
autoSwitchDisabled: false,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
34
51
|
const resolvedName = await this.resolveLoginAccountNameFromCurrentAuth();
|
|
35
52
|
const activeName = await this.getCurrentAccountName();
|
|
36
53
|
if (activeName) {
|
|
@@ -66,14 +83,50 @@ class AccountService {
|
|
|
66
83
|
autoSwitchDisabled,
|
|
67
84
|
};
|
|
68
85
|
}
|
|
86
|
+
async restoreSessionSnapshotIfNeeded() {
|
|
87
|
+
const sessionAccountName = await this.getSessionAccountName();
|
|
88
|
+
if (!sessionAccountName) {
|
|
89
|
+
return { restored: false };
|
|
90
|
+
}
|
|
91
|
+
const snapshotPath = this.accountFilePath(sessionAccountName);
|
|
92
|
+
if (!(await this.pathExists(snapshotPath))) {
|
|
93
|
+
await this.clearSessionAccountName();
|
|
94
|
+
return { restored: false };
|
|
95
|
+
}
|
|
96
|
+
const authPath = (0, paths_1.resolveAuthPath)();
|
|
97
|
+
if (await this.pathExists(authPath)) {
|
|
98
|
+
const [sessionSnapshot, activeSnapshot] = await Promise.all([
|
|
99
|
+
(0, auth_parser_1.parseAuthSnapshotFile)(snapshotPath),
|
|
100
|
+
(0, auth_parser_1.parseAuthSnapshotFile)(authPath),
|
|
101
|
+
]);
|
|
102
|
+
if (this.snapshotsShareIdentity(sessionSnapshot, activeSnapshot)) {
|
|
103
|
+
return {
|
|
104
|
+
restored: false,
|
|
105
|
+
accountName: sessionAccountName,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
await this.activateSnapshot(sessionAccountName);
|
|
110
|
+
return {
|
|
111
|
+
restored: true,
|
|
112
|
+
accountName: sessionAccountName,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
69
115
|
async listAccountNames() {
|
|
70
116
|
const accountsDir = (0, paths_1.resolveAccountsDir)();
|
|
71
117
|
if (!(await this.pathExists(accountsDir))) {
|
|
72
118
|
return [];
|
|
73
119
|
}
|
|
120
|
+
const sessionMapPath = (0, paths_1.resolveSessionMapPath)();
|
|
121
|
+
const sessionMapBasename = node_path_1.default.dirname(sessionMapPath) === accountsDir
|
|
122
|
+
? node_path_1.default.basename(sessionMapPath)
|
|
123
|
+
: undefined;
|
|
74
124
|
const entries = await promises_1.default.readdir(accountsDir, { withFileTypes: true });
|
|
75
125
|
return entries
|
|
76
|
-
.filter((entry) => entry.isFile() &&
|
|
126
|
+
.filter((entry) => entry.isFile() &&
|
|
127
|
+
entry.name.endsWith(".json") &&
|
|
128
|
+
entry.name !== "registry.json" &&
|
|
129
|
+
entry.name !== sessionMapBasename)
|
|
77
130
|
.map((entry) => entry.name.replace(/\.json$/i, ""))
|
|
78
131
|
.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }));
|
|
79
132
|
}
|
|
@@ -138,10 +191,20 @@ class AccountService {
|
|
|
138
191
|
});
|
|
139
192
|
}
|
|
140
193
|
async getCurrentAccountName() {
|
|
194
|
+
const sessionAccountName = await this.getSessionAccountName();
|
|
195
|
+
if (sessionAccountName) {
|
|
196
|
+
const sessionSnapshotPath = this.accountFilePath(sessionAccountName);
|
|
197
|
+
if (await this.pathExists(sessionSnapshotPath)) {
|
|
198
|
+
return sessionAccountName;
|
|
199
|
+
}
|
|
200
|
+
await this.clearSessionAccountName();
|
|
201
|
+
}
|
|
141
202
|
const currentNamePath = (0, paths_1.resolveCurrentNamePath)();
|
|
142
203
|
const currentName = await this.readCurrentNameFile(currentNamePath);
|
|
143
|
-
if (currentName)
|
|
204
|
+
if (currentName) {
|
|
205
|
+
await this.setSessionAccountName(currentName);
|
|
144
206
|
return currentName;
|
|
207
|
+
}
|
|
145
208
|
const authPath = (0, paths_1.resolveAuthPath)();
|
|
146
209
|
if (!(await this.pathExists(authPath)))
|
|
147
210
|
return null;
|
|
@@ -157,7 +220,9 @@ class AccountService {
|
|
|
157
220
|
const base = node_path_1.default.basename(resolvedTarget);
|
|
158
221
|
if (!base.endsWith(".json") || base === "registry.json")
|
|
159
222
|
return null;
|
|
160
|
-
|
|
223
|
+
const resolvedName = base.replace(/\.json$/i, "");
|
|
224
|
+
await this.setSessionAccountName(resolvedName);
|
|
225
|
+
return resolvedName;
|
|
161
226
|
}
|
|
162
227
|
async saveAccount(rawName, options) {
|
|
163
228
|
const name = this.normalizeAccountName(rawName);
|
|
@@ -535,6 +600,7 @@ class AccountService {
|
|
|
535
600
|
const currentNamePath = (0, paths_1.resolveCurrentNamePath)();
|
|
536
601
|
await this.ensureDir(node_path_1.default.dirname(currentNamePath));
|
|
537
602
|
await promises_1.default.writeFile(currentNamePath, `${name}\n`, "utf8");
|
|
603
|
+
await this.setSessionAccountName(name);
|
|
538
604
|
}
|
|
539
605
|
async readCurrentNameFile(currentNamePath) {
|
|
540
606
|
try {
|
|
@@ -636,6 +702,111 @@ class AccountService {
|
|
|
636
702
|
const authPath = (0, paths_1.resolveAuthPath)();
|
|
637
703
|
await this.removeIfExists(currentPath);
|
|
638
704
|
await this.removeIfExists(authPath);
|
|
705
|
+
await this.clearSessionAccountName();
|
|
706
|
+
}
|
|
707
|
+
isExternalSyncForced() {
|
|
708
|
+
const raw = process.env[EXTERNAL_SYNC_FORCE_ENV];
|
|
709
|
+
if (!raw)
|
|
710
|
+
return false;
|
|
711
|
+
const normalized = raw.trim().toLowerCase();
|
|
712
|
+
if (!normalized)
|
|
713
|
+
return false;
|
|
714
|
+
return !["0", "false", "no", "off"].includes(normalized);
|
|
715
|
+
}
|
|
716
|
+
resolveSessionScopeKey() {
|
|
717
|
+
var _a;
|
|
718
|
+
const explicit = (_a = process.env[SESSION_KEY_ENV]) === null || _a === void 0 ? void 0 : _a.trim();
|
|
719
|
+
if (explicit) {
|
|
720
|
+
const sanitized = explicit.replace(/\s+/g, " ").slice(0, 160);
|
|
721
|
+
return `session:${sanitized}`;
|
|
722
|
+
}
|
|
723
|
+
if (typeof process.ppid === "number" && process.ppid > 1) {
|
|
724
|
+
return `ppid:${process.ppid}`;
|
|
725
|
+
}
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
async getSessionAccountName() {
|
|
729
|
+
const sessionKey = this.resolveSessionScopeKey();
|
|
730
|
+
if (!sessionKey)
|
|
731
|
+
return null;
|
|
732
|
+
const sessionMap = await this.readSessionMap();
|
|
733
|
+
const entry = sessionMap.sessions[sessionKey];
|
|
734
|
+
if (!(entry === null || entry === void 0 ? void 0 : entry.accountName))
|
|
735
|
+
return null;
|
|
736
|
+
try {
|
|
737
|
+
return this.normalizeAccountName(entry.accountName);
|
|
738
|
+
}
|
|
739
|
+
catch {
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
async setSessionAccountName(accountName) {
|
|
744
|
+
const sessionKey = this.resolveSessionScopeKey();
|
|
745
|
+
if (!sessionKey)
|
|
746
|
+
return;
|
|
747
|
+
const sessionMap = await this.readSessionMap();
|
|
748
|
+
sessionMap.sessions[sessionKey] = {
|
|
749
|
+
accountName,
|
|
750
|
+
updatedAt: new Date().toISOString(),
|
|
751
|
+
};
|
|
752
|
+
await this.writeSessionMap(sessionMap);
|
|
753
|
+
}
|
|
754
|
+
async clearSessionAccountName() {
|
|
755
|
+
const sessionKey = this.resolveSessionScopeKey();
|
|
756
|
+
if (!sessionKey)
|
|
757
|
+
return;
|
|
758
|
+
const sessionMap = await this.readSessionMap();
|
|
759
|
+
if (!sessionMap.sessions[sessionKey])
|
|
760
|
+
return;
|
|
761
|
+
delete sessionMap.sessions[sessionKey];
|
|
762
|
+
await this.writeSessionMap(sessionMap);
|
|
763
|
+
}
|
|
764
|
+
async readSessionMap() {
|
|
765
|
+
const sessionMapPath = (0, paths_1.resolveSessionMapPath)();
|
|
766
|
+
try {
|
|
767
|
+
const raw = await promises_1.default.readFile(sessionMapPath, "utf8");
|
|
768
|
+
const parsed = JSON.parse(raw);
|
|
769
|
+
if (!parsed || typeof parsed !== "object") {
|
|
770
|
+
return {
|
|
771
|
+
version: 1,
|
|
772
|
+
sessions: {},
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
const root = parsed;
|
|
776
|
+
const sessionsRaw = root.sessions && typeof root.sessions === "object"
|
|
777
|
+
? root.sessions
|
|
778
|
+
: {};
|
|
779
|
+
const sessions = {};
|
|
780
|
+
for (const [key, value] of Object.entries(sessionsRaw)) {
|
|
781
|
+
if (!value || typeof value !== "object")
|
|
782
|
+
continue;
|
|
783
|
+
const rawEntry = value;
|
|
784
|
+
const accountName = typeof rawEntry.accountName === "string" ? rawEntry.accountName.trim() : "";
|
|
785
|
+
if (!accountName)
|
|
786
|
+
continue;
|
|
787
|
+
sessions[key] = {
|
|
788
|
+
accountName,
|
|
789
|
+
updatedAt: typeof rawEntry.updatedAt === "string" && rawEntry.updatedAt.length > 0
|
|
790
|
+
? rawEntry.updatedAt
|
|
791
|
+
: new Date().toISOString(),
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
return {
|
|
795
|
+
version: 1,
|
|
796
|
+
sessions,
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
catch {
|
|
800
|
+
return {
|
|
801
|
+
version: 1,
|
|
802
|
+
sessions: {},
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
async writeSessionMap(sessionMap) {
|
|
807
|
+
const sessionMapPath = (0, paths_1.resolveSessionMapPath)();
|
|
808
|
+
await this.ensureDir(node_path_1.default.dirname(sessionMapPath));
|
|
809
|
+
await promises_1.default.writeFile(sessionMapPath, `${JSON.stringify(sessionMap, null, 2)}\n`, "utf8");
|
|
639
810
|
}
|
|
640
811
|
snapshotsShareIdentity(a, b) {
|
|
641
812
|
var _a, _b;
|
|
@@ -33,24 +33,26 @@ function resolveDefaultShellRcPath() {
|
|
|
33
33
|
function renderLoginHookBlock() {
|
|
34
34
|
return [
|
|
35
35
|
exports.LOGIN_HOOK_MARK_START,
|
|
36
|
-
"#
|
|
36
|
+
"# Keep terminal-scoped snapshot memory in sync before/after each `codex` run.",
|
|
37
|
+
"# Also restore common terminal modes to avoid leaked escape sequences after codex exits.",
|
|
38
|
+
"__codex_auth_restore_tty() {",
|
|
39
|
+
" [[ -t 1 ]] || return 0",
|
|
40
|
+
" local __tty_target=/dev/tty",
|
|
41
|
+
" [[ -w \"$__tty_target\" ]] || __tty_target=/dev/stdout",
|
|
42
|
+
" printf '\\033[>4m\\033[<u\\033[?2026l\\033[?1004l\\033[?1l\\033[?2004l\\033[?1000l\\033[?1002l\\033[?1003l\\033[?1006l\\033[?1015l\\033[?1049l\\033[0m\\033[?25h\\033[H\\033>' >\"$__tty_target\" 2>/dev/null || true",
|
|
43
|
+
"}",
|
|
37
44
|
"if ! typeset -f codex >/dev/null 2>&1; then",
|
|
38
45
|
" codex() {",
|
|
46
|
+
" if command -v codex-auth >/dev/null 2>&1; then",
|
|
47
|
+
" command codex-auth restore-session >/dev/null 2>&1 || true",
|
|
48
|
+
" fi",
|
|
39
49
|
" command codex \"$@\"",
|
|
40
50
|
" local __codex_exit=$?",
|
|
41
|
-
" if
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
" --) break ;;",
|
|
47
|
-
" -*) ;;",
|
|
48
|
-
" *) __first_non_flag=\"$__arg\"; break ;;",
|
|
49
|
-
" esac",
|
|
50
|
-
" done",
|
|
51
|
-
" if [[ \"$__first_non_flag\" == \"login\" ]] && command -v codex-auth >/dev/null 2>&1; then",
|
|
52
|
-
" command codex-auth status >/dev/null 2>&1 || true",
|
|
53
|
-
" fi",
|
|
51
|
+
" if command -v codex-auth >/dev/null 2>&1; then",
|
|
52
|
+
" CODEX_AUTH_FORCE_EXTERNAL_SYNC=1 command codex-auth status >/dev/null 2>&1 || true",
|
|
53
|
+
" fi",
|
|
54
|
+
" if [[ -z \"${CODEX_AUTH_SKIP_TTY_RESTORE:-}\" ]]; then",
|
|
55
|
+
" __codex_auth_restore_tty",
|
|
54
56
|
" fi",
|
|
55
57
|
" return $__codex_exit",
|
|
56
58
|
" }",
|
|
@@ -3,8 +3,10 @@ export declare function resolveAccountsDir(): string;
|
|
|
3
3
|
export declare function resolveAuthPath(): string;
|
|
4
4
|
export declare function resolveCurrentNamePath(): string;
|
|
5
5
|
export declare function resolveRegistryPath(): string;
|
|
6
|
+
export declare function resolveSessionMapPath(): string;
|
|
6
7
|
export declare const codexDir: string;
|
|
7
8
|
export declare const accountsDir: string;
|
|
8
9
|
export declare const authPath: string;
|
|
9
10
|
export declare const currentNamePath: string;
|
|
10
11
|
export declare const registryPath: string;
|
|
12
|
+
export declare const sessionMapPath: string;
|
package/dist/lib/config/paths.js
CHANGED
|
@@ -3,12 +3,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.registryPath = exports.currentNamePath = exports.authPath = exports.accountsDir = exports.codexDir = void 0;
|
|
6
|
+
exports.sessionMapPath = exports.registryPath = exports.currentNamePath = exports.authPath = exports.accountsDir = exports.codexDir = void 0;
|
|
7
7
|
exports.resolveCodexDir = resolveCodexDir;
|
|
8
8
|
exports.resolveAccountsDir = resolveAccountsDir;
|
|
9
9
|
exports.resolveAuthPath = resolveAuthPath;
|
|
10
10
|
exports.resolveCurrentNamePath = resolveCurrentNamePath;
|
|
11
11
|
exports.resolveRegistryPath = resolveRegistryPath;
|
|
12
|
+
exports.resolveSessionMapPath = resolveSessionMapPath;
|
|
12
13
|
const node_os_1 = __importDefault(require("node:os"));
|
|
13
14
|
const node_path_1 = __importDefault(require("node:path"));
|
|
14
15
|
function resolvePath(raw) {
|
|
@@ -46,8 +47,16 @@ function resolveCurrentNamePath() {
|
|
|
46
47
|
function resolveRegistryPath() {
|
|
47
48
|
return node_path_1.default.join(resolveAccountsDir(), "registry.json");
|
|
48
49
|
}
|
|
50
|
+
function resolveSessionMapPath() {
|
|
51
|
+
const envPath = process.env.CODEX_AUTH_SESSION_MAP_PATH;
|
|
52
|
+
if (envPath && envPath.trim().length > 0) {
|
|
53
|
+
return resolvePath(envPath.trim());
|
|
54
|
+
}
|
|
55
|
+
return node_path_1.default.join(resolveAccountsDir(), "sessions.json");
|
|
56
|
+
}
|
|
49
57
|
exports.codexDir = resolveCodexDir();
|
|
50
58
|
exports.accountsDir = resolveAccountsDir();
|
|
51
59
|
exports.authPath = resolveAuthPath();
|
|
52
60
|
exports.currentNamePath = resolveCurrentNamePath();
|
|
53
61
|
exports.registryPath = resolveRegistryPath();
|
|
62
|
+
exports.sessionMapPath = resolveSessionMapPath();
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
export declare const PACKAGE_NAME = "@imdeadpool/codex-account-switcher";
|
|
2
|
+
export type UpdateState = "update-available" | "up-to-date" | "unknown";
|
|
3
|
+
export interface UpdateSummary {
|
|
4
|
+
currentVersion: string;
|
|
5
|
+
latestVersion: string;
|
|
6
|
+
state: UpdateState;
|
|
7
|
+
}
|
|
2
8
|
export declare function parseVersionTriplet(version: string): [number, number, number] | null;
|
|
3
9
|
export declare function isVersionNewer(currentVersion: string, latestVersion: string): boolean;
|
|
10
|
+
export declare function getUpdateSummary(currentVersion: string, latestVersion: string): UpdateSummary;
|
|
11
|
+
export declare function formatUpdateSummaryCard(summary: UpdateSummary): string[];
|
|
12
|
+
export declare function formatUpdateSummaryInline(summary: UpdateSummary): string;
|
|
4
13
|
export declare function fetchLatestNpmVersion(packageName: string, timeoutMs?: number): Promise<string | null>;
|
|
5
14
|
export declare function runGlobalNpmInstall(packageName: string, version?: "latest" | string): Promise<number>;
|
package/dist/lib/update-check.js
CHANGED
|
@@ -3,6 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.PACKAGE_NAME = void 0;
|
|
4
4
|
exports.parseVersionTriplet = parseVersionTriplet;
|
|
5
5
|
exports.isVersionNewer = isVersionNewer;
|
|
6
|
+
exports.getUpdateSummary = getUpdateSummary;
|
|
7
|
+
exports.formatUpdateSummaryCard = formatUpdateSummaryCard;
|
|
8
|
+
exports.formatUpdateSummaryInline = formatUpdateSummaryInline;
|
|
6
9
|
exports.fetchLatestNpmVersion = fetchLatestNpmVersion;
|
|
7
10
|
exports.runGlobalNpmInstall = runGlobalNpmInstall;
|
|
8
11
|
const node_child_process_1 = require("node:child_process");
|
|
@@ -27,6 +30,44 @@ function isVersionNewer(currentVersion, latestVersion) {
|
|
|
27
30
|
}
|
|
28
31
|
return false;
|
|
29
32
|
}
|
|
33
|
+
function getUpdateSummary(currentVersion, latestVersion) {
|
|
34
|
+
const current = parseVersionTriplet(currentVersion);
|
|
35
|
+
const latest = parseVersionTriplet(latestVersion);
|
|
36
|
+
if (!current || !latest) {
|
|
37
|
+
return {
|
|
38
|
+
currentVersion,
|
|
39
|
+
latestVersion,
|
|
40
|
+
state: "unknown",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
currentVersion,
|
|
45
|
+
latestVersion,
|
|
46
|
+
state: isVersionNewer(currentVersion, latestVersion) ? "update-available" : "up-to-date",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function formatUpdateSummaryCard(summary) {
|
|
50
|
+
const statusLabel = summary.state === "update-available"
|
|
51
|
+
? "update available"
|
|
52
|
+
: summary.state === "up-to-date"
|
|
53
|
+
? "up to date"
|
|
54
|
+
: "unknown";
|
|
55
|
+
return [
|
|
56
|
+
"┌─ codex-auth update",
|
|
57
|
+
`│ current: ${summary.currentVersion}`,
|
|
58
|
+
`│ latest : ${summary.latestVersion}`,
|
|
59
|
+
`└─ status : ${statusLabel}`,
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
function formatUpdateSummaryInline(summary) {
|
|
63
|
+
if (summary.state === "update-available") {
|
|
64
|
+
return `⬆ Update available: ${summary.currentVersion} -> ${summary.latestVersion}`;
|
|
65
|
+
}
|
|
66
|
+
if (summary.state === "up-to-date") {
|
|
67
|
+
return `✓ Up to date: ${summary.currentVersion}`;
|
|
68
|
+
}
|
|
69
|
+
return `ℹ Update status unknown (current: ${summary.currentVersion}, latest: ${summary.latestVersion})`;
|
|
70
|
+
}
|
|
30
71
|
async function fetchLatestNpmVersion(packageName, timeoutMs = 2500) {
|
|
31
72
|
return new Promise((resolve) => {
|
|
32
73
|
const child = (0, node_child_process_1.spawn)("npm", ["view", packageName, "version", "--json"], {
|
|
@@ -65,3 +65,18 @@ async function withTempRcFile(t, fn) {
|
|
|
65
65
|
strict_1.default.equal(after.rcPath, rcPath);
|
|
66
66
|
});
|
|
67
67
|
});
|
|
68
|
+
(0, node_test_1.default)("renderLoginHookBlock includes terminal-mode restore guard", () => {
|
|
69
|
+
const hook = (0, login_hook_1.renderLoginHookBlock)();
|
|
70
|
+
strict_1.default.ok(hook.includes("__codex_auth_restore_tty"));
|
|
71
|
+
strict_1.default.ok(hook.includes("command codex-auth restore-session"));
|
|
72
|
+
strict_1.default.ok(hook.includes("CODEX_AUTH_FORCE_EXTERNAL_SYNC=1 command codex-auth status"));
|
|
73
|
+
strict_1.default.ok(!hook.includes("__first_non_flag"));
|
|
74
|
+
strict_1.default.ok(hook.includes("\\033[>4m"));
|
|
75
|
+
strict_1.default.ok(hook.includes("\\033[<u"));
|
|
76
|
+
strict_1.default.ok(hook.includes("\\033[?2026l"));
|
|
77
|
+
strict_1.default.ok(hook.includes("\\033[?1004l"));
|
|
78
|
+
strict_1.default.ok(hook.includes("\\033[?2004l"));
|
|
79
|
+
strict_1.default.ok(hook.includes("\\033[0m"));
|
|
80
|
+
strict_1.default.ok(hook.includes("\\033[?25h"));
|
|
81
|
+
strict_1.default.ok(hook.includes("CODEX_AUTH_SKIP_TTY_RESTORE"));
|
|
82
|
+
});
|
|
@@ -426,3 +426,140 @@ async function withIsolatedCodexDir(t, fn) {
|
|
|
426
426
|
strict_1.default.equal(authStat.isSymbolicLink(), false);
|
|
427
427
|
});
|
|
428
428
|
});
|
|
429
|
+
(0, node_test_1.default)("getCurrentAccountName prefers session-scoped snapshot over global current pointer", async (t) => {
|
|
430
|
+
await withIsolatedCodexDir(t, async ({ codexDir, accountsDir }) => {
|
|
431
|
+
const service = new account_service_1.AccountService();
|
|
432
|
+
const currentPath = node_path_1.default.join(codexDir, "current");
|
|
433
|
+
const sessionMapPath = node_path_1.default.join(accountsDir, "sessions.json");
|
|
434
|
+
const sessionKey = `ppid:${process.ppid}`;
|
|
435
|
+
await promises_1.default.writeFile(node_path_1.default.join(accountsDir, "odin@megkapja.hu.json"), buildAuthPayload("odin@megkapja.hu", {
|
|
436
|
+
accountId: "acct-odin",
|
|
437
|
+
userId: "user-odin",
|
|
438
|
+
}));
|
|
439
|
+
await promises_1.default.writeFile(node_path_1.default.join(accountsDir, "lajos@edix.hu.json"), buildAuthPayload("lajos@edix.hu", {
|
|
440
|
+
accountId: "acct-lajos",
|
|
441
|
+
userId: "user-lajos",
|
|
442
|
+
}));
|
|
443
|
+
await promises_1.default.writeFile(currentPath, "lajos@edix.hu\n", "utf8");
|
|
444
|
+
await promises_1.default.writeFile(sessionMapPath, `${JSON.stringify({
|
|
445
|
+
version: 1,
|
|
446
|
+
sessions: {
|
|
447
|
+
[sessionKey]: {
|
|
448
|
+
accountName: "odin@megkapja.hu",
|
|
449
|
+
updatedAt: new Date().toISOString(),
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
}, null, 2)}\n`, "utf8");
|
|
453
|
+
const active = await service.getCurrentAccountName();
|
|
454
|
+
strict_1.default.equal(active, "odin@megkapja.hu");
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
(0, node_test_1.default)("syncExternalAuthSnapshotIfNeeded ignores external login from another terminal when session snapshot differs", async (t) => {
|
|
458
|
+
await withIsolatedCodexDir(t, async ({ codexDir, accountsDir, authPath }) => {
|
|
459
|
+
const service = new account_service_1.AccountService();
|
|
460
|
+
const currentPath = node_path_1.default.join(codexDir, "current");
|
|
461
|
+
const sessionMapPath = node_path_1.default.join(accountsDir, "sessions.json");
|
|
462
|
+
const sessionKey = `ppid:${process.ppid}`;
|
|
463
|
+
await promises_1.default.writeFile(node_path_1.default.join(accountsDir, "odin@megkapja.hu.json"), buildAuthPayload("odin@megkapja.hu", {
|
|
464
|
+
accountId: "acct-odin",
|
|
465
|
+
userId: "user-odin",
|
|
466
|
+
}));
|
|
467
|
+
await promises_1.default.writeFile(currentPath, "odin@megkapja.hu\n", "utf8");
|
|
468
|
+
await promises_1.default.writeFile(sessionMapPath, `${JSON.stringify({
|
|
469
|
+
version: 1,
|
|
470
|
+
sessions: {
|
|
471
|
+
[sessionKey]: {
|
|
472
|
+
accountName: "odin@megkapja.hu",
|
|
473
|
+
updatedAt: new Date().toISOString(),
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
}, null, 2)}\n`, "utf8");
|
|
477
|
+
await promises_1.default.writeFile(authPath, buildAuthPayload("lajos@edix.hu", {
|
|
478
|
+
accountId: "acct-lajos",
|
|
479
|
+
userId: "user-lajos",
|
|
480
|
+
}), "utf8");
|
|
481
|
+
const result = await service.syncExternalAuthSnapshotIfNeeded();
|
|
482
|
+
strict_1.default.deepEqual(result, {
|
|
483
|
+
synchronized: false,
|
|
484
|
+
autoSwitchDisabled: false,
|
|
485
|
+
});
|
|
486
|
+
await strict_1.default.rejects(() => promises_1.default.access(node_path_1.default.join(accountsDir, "lajos@edix.hu.json")));
|
|
487
|
+
strict_1.default.equal((await promises_1.default.readFile(currentPath, "utf8")).trim(), "odin@megkapja.hu");
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
(0, node_test_1.default)("syncExternalAuthSnapshotIfNeeded can be forced for explicit in-terminal codex login sync", async (t) => {
|
|
491
|
+
await withIsolatedCodexDir(t, async ({ codexDir, accountsDir, authPath }) => {
|
|
492
|
+
const service = new account_service_1.AccountService();
|
|
493
|
+
const currentPath = node_path_1.default.join(codexDir, "current");
|
|
494
|
+
const sessionMapPath = node_path_1.default.join(accountsDir, "sessions.json");
|
|
495
|
+
const sessionKey = `ppid:${process.ppid}`;
|
|
496
|
+
const previousFlag = process.env.CODEX_AUTH_FORCE_EXTERNAL_SYNC;
|
|
497
|
+
await promises_1.default.writeFile(node_path_1.default.join(accountsDir, "odin@megkapja.hu.json"), buildAuthPayload("odin@megkapja.hu", {
|
|
498
|
+
accountId: "acct-odin",
|
|
499
|
+
userId: "user-odin",
|
|
500
|
+
}));
|
|
501
|
+
await promises_1.default.writeFile(currentPath, "odin@megkapja.hu\n", "utf8");
|
|
502
|
+
await promises_1.default.writeFile(sessionMapPath, `${JSON.stringify({
|
|
503
|
+
version: 1,
|
|
504
|
+
sessions: {
|
|
505
|
+
[sessionKey]: {
|
|
506
|
+
accountName: "odin@megkapja.hu",
|
|
507
|
+
updatedAt: new Date().toISOString(),
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
}, null, 2)}\n`, "utf8");
|
|
511
|
+
await promises_1.default.writeFile(authPath, buildAuthPayload("lajos@edix.hu", {
|
|
512
|
+
accountId: "acct-lajos",
|
|
513
|
+
userId: "user-lajos",
|
|
514
|
+
}), "utf8");
|
|
515
|
+
process.env.CODEX_AUTH_FORCE_EXTERNAL_SYNC = "1";
|
|
516
|
+
t.after(() => {
|
|
517
|
+
process.env.CODEX_AUTH_FORCE_EXTERNAL_SYNC = previousFlag;
|
|
518
|
+
});
|
|
519
|
+
const result = await service.syncExternalAuthSnapshotIfNeeded();
|
|
520
|
+
strict_1.default.deepEqual(result, {
|
|
521
|
+
synchronized: true,
|
|
522
|
+
savedName: "lajos@edix.hu",
|
|
523
|
+
autoSwitchDisabled: false,
|
|
524
|
+
});
|
|
525
|
+
strict_1.default.equal((await promises_1.default.readFile(currentPath, "utf8")).trim(), "lajos@edix.hu");
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
(0, node_test_1.default)("restoreSessionSnapshotIfNeeded re-activates the session-pinned snapshot when auth.json drifts", async (t) => {
|
|
529
|
+
await withIsolatedCodexDir(t, async ({ codexDir, accountsDir, authPath }) => {
|
|
530
|
+
const service = new account_service_1.AccountService();
|
|
531
|
+
const currentPath = node_path_1.default.join(codexDir, "current");
|
|
532
|
+
const sessionMapPath = node_path_1.default.join(accountsDir, "sessions.json");
|
|
533
|
+
const sessionKey = `ppid:${process.ppid}`;
|
|
534
|
+
await promises_1.default.writeFile(node_path_1.default.join(accountsDir, "odin@megkapja.hu.json"), buildAuthPayload("odin@megkapja.hu", {
|
|
535
|
+
accountId: "acct-odin",
|
|
536
|
+
userId: "user-odin",
|
|
537
|
+
}));
|
|
538
|
+
await promises_1.default.writeFile(node_path_1.default.join(accountsDir, "lajos@edix.hu.json"), buildAuthPayload("lajos@edix.hu", {
|
|
539
|
+
accountId: "acct-lajos",
|
|
540
|
+
userId: "user-lajos",
|
|
541
|
+
}));
|
|
542
|
+
await promises_1.default.writeFile(currentPath, "lajos@edix.hu\n", "utf8");
|
|
543
|
+
await promises_1.default.writeFile(sessionMapPath, `${JSON.stringify({
|
|
544
|
+
version: 1,
|
|
545
|
+
sessions: {
|
|
546
|
+
[sessionKey]: {
|
|
547
|
+
accountName: "odin@megkapja.hu",
|
|
548
|
+
updatedAt: new Date().toISOString(),
|
|
549
|
+
},
|
|
550
|
+
},
|
|
551
|
+
}, null, 2)}\n`, "utf8");
|
|
552
|
+
await promises_1.default.writeFile(authPath, buildAuthPayload("lajos@edix.hu", {
|
|
553
|
+
accountId: "acct-lajos",
|
|
554
|
+
userId: "user-lajos",
|
|
555
|
+
}), "utf8");
|
|
556
|
+
const restored = await service.restoreSessionSnapshotIfNeeded();
|
|
557
|
+
strict_1.default.deepEqual(restored, {
|
|
558
|
+
restored: true,
|
|
559
|
+
accountName: "odin@megkapja.hu",
|
|
560
|
+
});
|
|
561
|
+
strict_1.default.equal((await promises_1.default.readFile(currentPath, "utf8")).trim(), "odin@megkapja.hu");
|
|
562
|
+
const parsed = await (0, auth_parser_1.parseAuthSnapshotFile)(authPath);
|
|
563
|
+
strict_1.default.equal(parsed.email, "odin@megkapja.hu");
|
|
564
|
+
});
|
|
565
|
+
});
|
|
@@ -27,3 +27,41 @@ const update_check_1 = require("../lib/update-check");
|
|
|
27
27
|
strict_1.default.equal((0, update_check_1.isVersionNewer)("latest", "0.1.9"), false);
|
|
28
28
|
strict_1.default.equal((0, update_check_1.isVersionNewer)("0.1.8", "nightly"), false);
|
|
29
29
|
});
|
|
30
|
+
(0, node_test_1.default)("getUpdateSummary returns update-available state", () => {
|
|
31
|
+
const summary = (0, update_check_1.getUpdateSummary)("0.1.8", "0.1.9");
|
|
32
|
+
strict_1.default.deepEqual(summary, {
|
|
33
|
+
currentVersion: "0.1.8",
|
|
34
|
+
latestVersion: "0.1.9",
|
|
35
|
+
state: "update-available",
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
(0, node_test_1.default)("getUpdateSummary returns up-to-date state", () => {
|
|
39
|
+
const summary = (0, update_check_1.getUpdateSummary)("0.1.9", "0.1.9");
|
|
40
|
+
strict_1.default.deepEqual(summary, {
|
|
41
|
+
currentVersion: "0.1.9",
|
|
42
|
+
latestVersion: "0.1.9",
|
|
43
|
+
state: "up-to-date",
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
(0, node_test_1.default)("formatUpdateSummaryInline renders human-friendly states", () => {
|
|
47
|
+
strict_1.default.equal((0, update_check_1.formatUpdateSummaryInline)({
|
|
48
|
+
currentVersion: "0.1.8",
|
|
49
|
+
latestVersion: "0.1.9",
|
|
50
|
+
state: "update-available",
|
|
51
|
+
}), "⬆ Update available: 0.1.8 -> 0.1.9");
|
|
52
|
+
strict_1.default.equal((0, update_check_1.formatUpdateSummaryInline)({
|
|
53
|
+
currentVersion: "0.1.9",
|
|
54
|
+
latestVersion: "0.1.9",
|
|
55
|
+
state: "up-to-date",
|
|
56
|
+
}), "✓ Up to date: 0.1.9");
|
|
57
|
+
});
|
|
58
|
+
(0, node_test_1.default)("formatUpdateSummaryCard renders a stable 4-line card", () => {
|
|
59
|
+
const lines = (0, update_check_1.formatUpdateSummaryCard)({
|
|
60
|
+
currentVersion: "0.1.9",
|
|
61
|
+
latestVersion: "0.1.10",
|
|
62
|
+
state: "update-available",
|
|
63
|
+
});
|
|
64
|
+
strict_1.default.equal(lines.length, 4);
|
|
65
|
+
strict_1.default.equal(lines[0], "┌─ codex-auth update");
|
|
66
|
+
strict_1.default.equal(lines[3], "└─ status : update available");
|
|
67
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imdeadpool/codex-account-switcher",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "A command-line tool that lets you manage and switch between multiple Codex accounts instantly, no more constant logins and logouts.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -22,6 +22,13 @@ function renderHookBlock() {
|
|
|
22
22
|
return [
|
|
23
23
|
MARK_START,
|
|
24
24
|
"# Auto-sync codex-auth snapshots after successful official `codex login`.",
|
|
25
|
+
"# Also restore common terminal modes to avoid leaked escape sequences after codex exits.",
|
|
26
|
+
"__codex_auth_restore_tty() {",
|
|
27
|
+
" [[ -t 1 ]] || return 0",
|
|
28
|
+
" local __tty_target=/dev/tty",
|
|
29
|
+
" [[ -w \"$__tty_target\" ]] || __tty_target=/dev/stdout",
|
|
30
|
+
" printf '\\033[>4m\\033[<u\\033[?2026l\\033[?1004l\\033[?1l\\033[?2004l\\033[?1000l\\033[?1002l\\033[?1003l\\033[?1006l\\033[?1015l\\033[?1049l\\033[0m\\033[?25h\\033[H\\033>' >\"$__tty_target\" 2>/dev/null || true",
|
|
31
|
+
"}",
|
|
25
32
|
"if ! typeset -f codex >/dev/null 2>&1; then",
|
|
26
33
|
" codex() {",
|
|
27
34
|
" command codex \"$@\"",
|
|
@@ -40,6 +47,9 @@ function renderHookBlock() {
|
|
|
40
47
|
" command codex-auth status >/dev/null 2>&1 || true",
|
|
41
48
|
" fi",
|
|
42
49
|
" fi",
|
|
50
|
+
" if [[ -z \"${CODEX_AUTH_SKIP_TTY_RESTORE:-}\" ]]; then",
|
|
51
|
+
" __codex_auth_restore_tty",
|
|
52
|
+
" fi",
|
|
43
53
|
" return $__codex_exit",
|
|
44
54
|
" }",
|
|
45
55
|
"fi",
|