@imdeadpool/codex-account-switcher 0.1.11 → 0.1.15
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 +7 -1
- package/dist/commands/remove-login-hook.js +1 -1
- package/dist/commands/setup-login-hook.js +4 -1
- package/dist/hooks/init/update-notifier.js +26 -1
- package/dist/lib/accounts/account-service.d.ts +4 -0
- package/dist/lib/accounts/account-service.js +78 -3
- package/dist/lib/config/login-hook.d.ts +1 -1
- package/dist/lib/config/login-hook.js +26 -19
- package/dist/lib/update-check.d.ts +1 -0
- package/dist/lib/update-check.js +11 -0
- package/dist/tests/login-hook.test.js +20 -0
- package/dist/tests/save-account-safety.test.js +111 -3
- package/dist/tests/update-check.test.js +11 -0
- package/package.json +4 -4
- package/scripts/postinstall-login-hook.cjs +93 -32
package/README.md
CHANGED
|
@@ -25,6 +25,11 @@ During global install, the package asks for permission to add an optional shell
|
|
|
25
25
|
(`~/.bashrc` or `~/.zshrc`) that auto-runs a silent snapshot sync after successful
|
|
26
26
|
official `codex login`.
|
|
27
27
|
|
|
28
|
+
On later global updates, if the hook is already installed, `codex-auth` refreshes the
|
|
29
|
+
hook block automatically to the latest template (no manual remove/setup needed).
|
|
30
|
+
The refreshed hook always wraps `codex` in your shell so sync/restore still run even if
|
|
31
|
+
another shell config already defined `codex` as a function.
|
|
32
|
+
|
|
28
33
|
- Choose `y` to enable fully automatic login snapshot capture.
|
|
29
34
|
- Choose `n` (default) to skip.
|
|
30
35
|
- Set `CODEX_AUTH_SKIP_POSTINSTALL=1` to always suppress this prompt.
|
|
@@ -123,7 +128,7 @@ codex-auth remove-login-hook
|
|
|
123
128
|
- `codex-auth config auto ...` – Enables/disables managed auto-switch and updates threshold percentages.
|
|
124
129
|
- `codex-auth config api enable|disable` – Chooses usage source mode (`api` or `local`).
|
|
125
130
|
- `codex-auth daemon --once|--watch` – Runs the auto-switch loop once or continuously.
|
|
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.
|
|
131
|
+
- `codex-auth setup-login-hook [-f <path>]` – Installs or refreshes 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.
|
|
127
132
|
- `codex-auth hook-status [-f <path>]` – Shows whether the optional login auto-snapshot hook is installed for the selected rc file.
|
|
128
133
|
- `codex-auth remove-login-hook [-f <path>]` – Removes the optional shell hook.
|
|
129
134
|
|
|
@@ -154,3 +159,4 @@ Notes:
|
|
|
154
159
|
- Works on macOS/Linux/Windows (regular-file auth snapshot activation).
|
|
155
160
|
- Requires Node 18+.
|
|
156
161
|
- Running bare `codex-auth` shows the help screen and also displays an update notice when a newer npm release is available.
|
|
162
|
+
- Running bare `codex-auth` now prompts to install updates immediately when a newer npm release is available (`[Y/n]`, Enter defaults to yes).
|
|
@@ -24,7 +24,7 @@ class RemoveLoginHookCommand extends base_command_1.BaseCommand {
|
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
-
RemoveLoginHookCommand.description = "Remove the shell hook that
|
|
27
|
+
RemoveLoginHookCommand.description = "Remove the shell hook that keeps terminal snapshot memory in sync";
|
|
28
28
|
RemoveLoginHookCommand.flags = {
|
|
29
29
|
shellRc: core_1.Flags.string({
|
|
30
30
|
char: "f",
|
|
@@ -17,6 +17,9 @@ class SetupLoginHookCommand extends base_command_1.BaseCommand {
|
|
|
17
17
|
if (result === "already-installed") {
|
|
18
18
|
this.log(`Login auto-snapshot hook is already installed in ${rcPath}.`);
|
|
19
19
|
}
|
|
20
|
+
else if (result === "updated") {
|
|
21
|
+
this.log(`Updated login auto-snapshot hook in ${rcPath}.`);
|
|
22
|
+
}
|
|
20
23
|
else {
|
|
21
24
|
this.log(`Installed login auto-snapshot hook in ${rcPath}.`);
|
|
22
25
|
}
|
|
@@ -24,7 +27,7 @@ class SetupLoginHookCommand extends base_command_1.BaseCommand {
|
|
|
24
27
|
});
|
|
25
28
|
}
|
|
26
29
|
}
|
|
27
|
-
SetupLoginHookCommand.description = "Install a shell hook that
|
|
30
|
+
SetupLoginHookCommand.description = "Install or refresh a shell hook that keeps terminal snapshot memory in sync";
|
|
28
31
|
SetupLoginHookCommand.flags = {
|
|
29
32
|
shellRc: core_1.Flags.string({
|
|
30
33
|
char: "f",
|
|
@@ -1,5 +1,9 @@
|
|
|
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 });
|
|
6
|
+
const promises_1 = __importDefault(require("node:readline/promises"));
|
|
3
7
|
const update_check_1 = require("../../lib/update-check");
|
|
4
8
|
const hook = async function (options) {
|
|
5
9
|
if (options.id)
|
|
@@ -18,6 +22,27 @@ const hook = async function (options) {
|
|
|
18
22
|
if (summary.state !== "update-available")
|
|
19
23
|
return;
|
|
20
24
|
this.log((0, update_check_1.formatUpdateSummaryInline)(summary));
|
|
21
|
-
|
|
25
|
+
const rl = promises_1.default.createInterface({
|
|
26
|
+
input: process.stdin,
|
|
27
|
+
output: process.stdout,
|
|
28
|
+
});
|
|
29
|
+
let shouldUpdate = false;
|
|
30
|
+
try {
|
|
31
|
+
const answer = await rl.question("Install the update now? [Y/n] ");
|
|
32
|
+
shouldUpdate = (0, update_check_1.shouldProceedWithYesDefault)(answer);
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
rl.close();
|
|
36
|
+
}
|
|
37
|
+
if (!shouldUpdate) {
|
|
38
|
+
this.log("Skipped update. Run `codex-auth self-update` anytime.");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const exitCode = await (0, update_check_1.runGlobalNpmInstall)(update_check_1.PACKAGE_NAME);
|
|
42
|
+
if (exitCode === 0) {
|
|
43
|
+
this.log(`✓ codex-auth updated to ${latestVersion}.`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
this.log(`Update failed (exit code ${exitCode}). Run: npm i -g ${update_check_1.PACKAGE_NAME}@latest`);
|
|
22
47
|
};
|
|
23
48
|
exports.default = hook;
|
|
@@ -75,10 +75,14 @@ export declare class AccountService {
|
|
|
75
75
|
private isExternalSyncForced;
|
|
76
76
|
private resolveSessionScopeKey;
|
|
77
77
|
private getSessionAccountName;
|
|
78
|
+
private getActiveSessionAccountName;
|
|
78
79
|
private setSessionAccountName;
|
|
79
80
|
private clearSessionAccountName;
|
|
80
81
|
private readSessionMap;
|
|
81
82
|
private writeSessionMap;
|
|
83
|
+
private isSessionPinnedToActiveCodex;
|
|
84
|
+
private readChildPids;
|
|
85
|
+
private isCodexProcess;
|
|
82
86
|
private snapshotsShareIdentity;
|
|
83
87
|
private renderSnapshotIdentity;
|
|
84
88
|
}
|
|
@@ -16,6 +16,7 @@ const service_manager_1 = require("./service-manager");
|
|
|
16
16
|
const ACCOUNT_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._@+-]*$/;
|
|
17
17
|
const EXTERNAL_SYNC_FORCE_ENV = "CODEX_AUTH_FORCE_EXTERNAL_SYNC";
|
|
18
18
|
const SESSION_KEY_ENV = "CODEX_AUTH_SESSION_KEY";
|
|
19
|
+
const SESSION_ACTIVE_OVERRIDE_ENV = "CODEX_AUTH_SESSION_ACTIVE_OVERRIDE";
|
|
19
20
|
class AccountService {
|
|
20
21
|
async syncExternalAuthSnapshotIfNeeded() {
|
|
21
22
|
const authPath = (0, paths_1.resolveAuthPath)();
|
|
@@ -33,7 +34,7 @@ class AccountService {
|
|
|
33
34
|
autoSwitchDisabled: false,
|
|
34
35
|
};
|
|
35
36
|
}
|
|
36
|
-
const sessionAccountName = await this.
|
|
37
|
+
const sessionAccountName = await this.getActiveSessionAccountName();
|
|
37
38
|
if (sessionAccountName) {
|
|
38
39
|
const sessionSnapshotPath = this.accountFilePath(sessionAccountName);
|
|
39
40
|
if (await this.pathExists(sessionSnapshotPath)) {
|
|
@@ -84,7 +85,7 @@ class AccountService {
|
|
|
84
85
|
};
|
|
85
86
|
}
|
|
86
87
|
async restoreSessionSnapshotIfNeeded() {
|
|
87
|
-
const sessionAccountName = await this.
|
|
88
|
+
const sessionAccountName = await this.getActiveSessionAccountName();
|
|
88
89
|
if (!sessionAccountName) {
|
|
89
90
|
return { restored: false };
|
|
90
91
|
}
|
|
@@ -191,7 +192,7 @@ class AccountService {
|
|
|
191
192
|
});
|
|
192
193
|
}
|
|
193
194
|
async getCurrentAccountName() {
|
|
194
|
-
const sessionAccountName = await this.
|
|
195
|
+
const sessionAccountName = await this.getActiveSessionAccountName();
|
|
195
196
|
if (sessionAccountName) {
|
|
196
197
|
const sessionSnapshotPath = this.accountFilePath(sessionAccountName);
|
|
197
198
|
if (await this.pathExists(sessionSnapshotPath)) {
|
|
@@ -740,6 +741,16 @@ class AccountService {
|
|
|
740
741
|
return null;
|
|
741
742
|
}
|
|
742
743
|
}
|
|
744
|
+
async getActiveSessionAccountName() {
|
|
745
|
+
const sessionAccountName = await this.getSessionAccountName();
|
|
746
|
+
if (!sessionAccountName)
|
|
747
|
+
return null;
|
|
748
|
+
const sessionIsActive = await this.isSessionPinnedToActiveCodex();
|
|
749
|
+
if (sessionIsActive)
|
|
750
|
+
return sessionAccountName;
|
|
751
|
+
await this.clearSessionAccountName();
|
|
752
|
+
return null;
|
|
753
|
+
}
|
|
743
754
|
async setSessionAccountName(accountName) {
|
|
744
755
|
const sessionKey = this.resolveSessionScopeKey();
|
|
745
756
|
if (!sessionKey)
|
|
@@ -808,6 +819,70 @@ class AccountService {
|
|
|
808
819
|
await this.ensureDir(node_path_1.default.dirname(sessionMapPath));
|
|
809
820
|
await promises_1.default.writeFile(sessionMapPath, `${JSON.stringify(sessionMap, null, 2)}\n`, "utf8");
|
|
810
821
|
}
|
|
822
|
+
async isSessionPinnedToActiveCodex() {
|
|
823
|
+
var _a;
|
|
824
|
+
const override = (_a = process.env[SESSION_ACTIVE_OVERRIDE_ENV]) === null || _a === void 0 ? void 0 : _a.trim().toLowerCase();
|
|
825
|
+
if (override) {
|
|
826
|
+
if (["1", "true", "yes", "on"].includes(override))
|
|
827
|
+
return true;
|
|
828
|
+
if (["0", "false", "no", "off"].includes(override))
|
|
829
|
+
return false;
|
|
830
|
+
}
|
|
831
|
+
const sessionKey = this.resolveSessionScopeKey();
|
|
832
|
+
if (!sessionKey)
|
|
833
|
+
return false;
|
|
834
|
+
if (sessionKey.startsWith("session:")) {
|
|
835
|
+
return true;
|
|
836
|
+
}
|
|
837
|
+
if (process.platform !== "linux") {
|
|
838
|
+
return true;
|
|
839
|
+
}
|
|
840
|
+
const ppidMatch = sessionKey.match(/^ppid:(\d+)$/);
|
|
841
|
+
if (!ppidMatch)
|
|
842
|
+
return false;
|
|
843
|
+
const parentPid = Number(ppidMatch[1]);
|
|
844
|
+
if (!Number.isFinite(parentPid) || parentPid <= 1)
|
|
845
|
+
return false;
|
|
846
|
+
const childPids = await this.readChildPids(parentPid);
|
|
847
|
+
if (childPids.length === 0)
|
|
848
|
+
return false;
|
|
849
|
+
for (const childPid of childPids) {
|
|
850
|
+
if (await this.isCodexProcess(childPid)) {
|
|
851
|
+
return true;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
return false;
|
|
855
|
+
}
|
|
856
|
+
async readChildPids(parentPid) {
|
|
857
|
+
try {
|
|
858
|
+
const childrenRaw = await promises_1.default.readFile(`/proc/${parentPid}/task/${parentPid}/children`, "utf8");
|
|
859
|
+
return childrenRaw
|
|
860
|
+
.split(/\s+/)
|
|
861
|
+
.map((value) => Number(value))
|
|
862
|
+
.filter((value) => Number.isInteger(value) && value > 1);
|
|
863
|
+
}
|
|
864
|
+
catch {
|
|
865
|
+
return [];
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
async isCodexProcess(pid) {
|
|
869
|
+
try {
|
|
870
|
+
const cmdline = await promises_1.default.readFile(`/proc/${pid}/cmdline`, "utf8");
|
|
871
|
+
const normalized = cmdline.replace(/\0/g, " ").trim();
|
|
872
|
+
if (!normalized)
|
|
873
|
+
return false;
|
|
874
|
+
if (/\bcodex-auth\b/.test(normalized))
|
|
875
|
+
return false;
|
|
876
|
+
if (/(^|\s|\/)codex(\s|$)/.test(normalized))
|
|
877
|
+
return true;
|
|
878
|
+
if (/(^|\s|\/)codex-linux-[^\s]*($|\s)/.test(normalized))
|
|
879
|
+
return true;
|
|
880
|
+
return false;
|
|
881
|
+
}
|
|
882
|
+
catch {
|
|
883
|
+
return false;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
811
886
|
snapshotsShareIdentity(a, b) {
|
|
812
887
|
var _a, _b;
|
|
813
888
|
if (a.authMode !== "chatgpt" || b.authMode !== "chatgpt") {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export declare const LOGIN_HOOK_MARK_START = "# >>> codex-auth-login-auto-snapshot >>>";
|
|
2
2
|
export declare const LOGIN_HOOK_MARK_END = "# <<< codex-auth-login-auto-snapshot <<<";
|
|
3
|
-
export type HookInstallStatus = "installed" | "already-installed";
|
|
3
|
+
export type HookInstallStatus = "installed" | "updated" | "already-installed";
|
|
4
4
|
export type HookRemoveStatus = "removed" | "not-installed";
|
|
5
5
|
export interface LoginHookStatus {
|
|
6
6
|
installed: boolean;
|
|
@@ -22,6 +22,10 @@ function hookBlockRegex() {
|
|
|
22
22
|
const end = escapeRegex(exports.LOGIN_HOOK_MARK_END);
|
|
23
23
|
return new RegExp(`\\n?${start}[\\s\\S]*?${end}\\n?`, "g");
|
|
24
24
|
}
|
|
25
|
+
function normalizeRcContents(contents) {
|
|
26
|
+
const collapsed = contents.replace(/\n{3,}/g, "\n\n");
|
|
27
|
+
return `${collapsed.replace(/\s*$/, "")}\n`;
|
|
28
|
+
}
|
|
25
29
|
function resolveDefaultShellRcPath() {
|
|
26
30
|
var _a;
|
|
27
31
|
const shell = ((_a = process.env.SHELL) !== null && _a !== void 0 ? _a : "").toLowerCase();
|
|
@@ -41,22 +45,20 @@ function renderLoginHookBlock() {
|
|
|
41
45
|
" [[ -w \"$__tty_target\" ]] || __tty_target=/dev/stdout",
|
|
42
46
|
" 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
47
|
"}",
|
|
44
|
-
"
|
|
45
|
-
" codex
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
" }",
|
|
59
|
-
"fi",
|
|
48
|
+
"codex() {",
|
|
49
|
+
" if command -v codex-auth >/dev/null 2>&1; then",
|
|
50
|
+
" command codex-auth restore-session >/dev/null 2>&1 || true",
|
|
51
|
+
" fi",
|
|
52
|
+
" command codex \"$@\"",
|
|
53
|
+
" local __codex_exit=$?",
|
|
54
|
+
" if command -v codex-auth >/dev/null 2>&1; then",
|
|
55
|
+
" CODEX_AUTH_FORCE_EXTERNAL_SYNC=1 command codex-auth status >/dev/null 2>&1 || true",
|
|
56
|
+
" fi",
|
|
57
|
+
" if [[ -z \"${CODEX_AUTH_SKIP_TTY_RESTORE:-}\" ]]; then",
|
|
58
|
+
" __codex_auth_restore_tty",
|
|
59
|
+
" fi",
|
|
60
|
+
" return $__codex_exit",
|
|
61
|
+
"}",
|
|
60
62
|
exports.LOGIN_HOOK_MARK_END,
|
|
61
63
|
].join("\n");
|
|
62
64
|
}
|
|
@@ -72,9 +74,14 @@ async function installLoginHook(rcPath = resolveDefaultShellRcPath()) {
|
|
|
72
74
|
throw error;
|
|
73
75
|
}
|
|
74
76
|
if (existing.includes(exports.LOGIN_HOOK_MARK_START) && existing.includes(exports.LOGIN_HOOK_MARK_END)) {
|
|
75
|
-
|
|
77
|
+
const refreshed = normalizeRcContents(existing.replace(hookBlockRegex(), `\n${renderLoginHookBlock()}\n`));
|
|
78
|
+
if (refreshed === normalizeRcContents(existing)) {
|
|
79
|
+
return "already-installed";
|
|
80
|
+
}
|
|
81
|
+
await promises_1.default.writeFile(rcPath, refreshed, "utf8");
|
|
82
|
+
return "updated";
|
|
76
83
|
}
|
|
77
|
-
const next = `${existing
|
|
84
|
+
const next = normalizeRcContents(`${existing}\n\n${renderLoginHookBlock()}\n`);
|
|
78
85
|
await promises_1.default.writeFile(rcPath, next, "utf8");
|
|
79
86
|
return "installed";
|
|
80
87
|
}
|
|
@@ -95,7 +102,7 @@ async function removeLoginHook(rcPath = resolveDefaultShellRcPath()) {
|
|
|
95
102
|
if (stripped === existing) {
|
|
96
103
|
return "not-installed";
|
|
97
104
|
}
|
|
98
|
-
await promises_1.default.writeFile(rcPath, stripped
|
|
105
|
+
await promises_1.default.writeFile(rcPath, normalizeRcContents(stripped), "utf8");
|
|
99
106
|
return "removed";
|
|
100
107
|
}
|
|
101
108
|
async function getLoginHookStatus(rcPath = resolveDefaultShellRcPath()) {
|
|
@@ -10,5 +10,6 @@ export declare function isVersionNewer(currentVersion: string, latestVersion: st
|
|
|
10
10
|
export declare function getUpdateSummary(currentVersion: string, latestVersion: string): UpdateSummary;
|
|
11
11
|
export declare function formatUpdateSummaryCard(summary: UpdateSummary): string[];
|
|
12
12
|
export declare function formatUpdateSummaryInline(summary: UpdateSummary): string;
|
|
13
|
+
export declare function shouldProceedWithYesDefault(answer: string): boolean;
|
|
13
14
|
export declare function fetchLatestNpmVersion(packageName: string, timeoutMs?: number): Promise<string | null>;
|
|
14
15
|
export declare function runGlobalNpmInstall(packageName: string, version?: "latest" | string): Promise<number>;
|
package/dist/lib/update-check.js
CHANGED
|
@@ -6,6 +6,7 @@ exports.isVersionNewer = isVersionNewer;
|
|
|
6
6
|
exports.getUpdateSummary = getUpdateSummary;
|
|
7
7
|
exports.formatUpdateSummaryCard = formatUpdateSummaryCard;
|
|
8
8
|
exports.formatUpdateSummaryInline = formatUpdateSummaryInline;
|
|
9
|
+
exports.shouldProceedWithYesDefault = shouldProceedWithYesDefault;
|
|
9
10
|
exports.fetchLatestNpmVersion = fetchLatestNpmVersion;
|
|
10
11
|
exports.runGlobalNpmInstall = runGlobalNpmInstall;
|
|
11
12
|
const node_child_process_1 = require("node:child_process");
|
|
@@ -68,6 +69,16 @@ function formatUpdateSummaryInline(summary) {
|
|
|
68
69
|
}
|
|
69
70
|
return `ℹ Update status unknown (current: ${summary.currentVersion}, latest: ${summary.latestVersion})`;
|
|
70
71
|
}
|
|
72
|
+
function shouldProceedWithYesDefault(answer) {
|
|
73
|
+
const normalized = answer.trim().toLowerCase();
|
|
74
|
+
if (!normalized)
|
|
75
|
+
return true;
|
|
76
|
+
if (normalized === "y" || normalized === "yes")
|
|
77
|
+
return true;
|
|
78
|
+
if (normalized === "n" || normalized === "no")
|
|
79
|
+
return false;
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
71
82
|
async function fetchLatestNpmVersion(packageName, timeoutMs = 2500) {
|
|
72
83
|
return new Promise((resolve) => {
|
|
73
84
|
const child = (0, node_child_process_1.spawn)("npm", ["view", packageName, "version", "--json"], {
|
|
@@ -38,6 +38,24 @@ async function withTempRcFile(t, fn) {
|
|
|
38
38
|
strict_1.default.equal(startCount, 1);
|
|
39
39
|
});
|
|
40
40
|
});
|
|
41
|
+
(0, node_test_1.default)("installLoginHook refreshes an existing legacy hook block", async (t) => {
|
|
42
|
+
await withTempRcFile(t, async (rcPath) => {
|
|
43
|
+
const legacyBlock = [
|
|
44
|
+
login_hook_1.LOGIN_HOOK_MARK_START,
|
|
45
|
+
"# legacy",
|
|
46
|
+
login_hook_1.LOGIN_HOOK_MARK_END,
|
|
47
|
+
].join("\n");
|
|
48
|
+
await promises_1.default.writeFile(rcPath, `# test bashrc\n\n${legacyBlock}\n`, "utf8");
|
|
49
|
+
const result = await (0, login_hook_1.installLoginHook)(rcPath);
|
|
50
|
+
strict_1.default.equal(result, "updated");
|
|
51
|
+
const contents = await promises_1.default.readFile(rcPath, "utf8");
|
|
52
|
+
strict_1.default.ok(contents.includes("command codex-auth restore-session"));
|
|
53
|
+
strict_1.default.ok(contents.includes("CODEX_AUTH_FORCE_EXTERNAL_SYNC=1 command codex-auth status"));
|
|
54
|
+
strict_1.default.ok(!contents.includes("# legacy"));
|
|
55
|
+
const startCount = contents.split(login_hook_1.LOGIN_HOOK_MARK_START).length - 1;
|
|
56
|
+
strict_1.default.equal(startCount, 1);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
41
59
|
(0, node_test_1.default)("removeLoginHook removes installed marker block", async (t) => {
|
|
42
60
|
await withTempRcFile(t, async (rcPath) => {
|
|
43
61
|
await (0, login_hook_1.installLoginHook)(rcPath);
|
|
@@ -68,9 +86,11 @@ async function withTempRcFile(t, fn) {
|
|
|
68
86
|
(0, node_test_1.default)("renderLoginHookBlock includes terminal-mode restore guard", () => {
|
|
69
87
|
const hook = (0, login_hook_1.renderLoginHookBlock)();
|
|
70
88
|
strict_1.default.ok(hook.includes("__codex_auth_restore_tty"));
|
|
89
|
+
strict_1.default.ok(hook.includes("codex() {"));
|
|
71
90
|
strict_1.default.ok(hook.includes("command codex-auth restore-session"));
|
|
72
91
|
strict_1.default.ok(hook.includes("CODEX_AUTH_FORCE_EXTERNAL_SYNC=1 command codex-auth status"));
|
|
73
92
|
strict_1.default.ok(!hook.includes("__first_non_flag"));
|
|
93
|
+
strict_1.default.ok(!hook.includes("if ! typeset -f codex"));
|
|
74
94
|
strict_1.default.ok(hook.includes("\\033[>4m"));
|
|
75
95
|
strict_1.default.ok(hook.includes("\\033[<u"));
|
|
76
96
|
strict_1.default.ok(hook.includes("\\033[?2026l"));
|
|
@@ -51,6 +51,7 @@ async function withIsolatedCodexDir(t, fn) {
|
|
|
51
51
|
CODEX_AUTH_ACCOUNTS_DIR: process.env.CODEX_AUTH_ACCOUNTS_DIR,
|
|
52
52
|
CODEX_AUTH_JSON_PATH: process.env.CODEX_AUTH_JSON_PATH,
|
|
53
53
|
CODEX_AUTH_CURRENT_PATH: process.env.CODEX_AUTH_CURRENT_PATH,
|
|
54
|
+
CODEX_AUTH_SESSION_ACTIVE_OVERRIDE: process.env.CODEX_AUTH_SESSION_ACTIVE_OVERRIDE,
|
|
54
55
|
};
|
|
55
56
|
process.env.CODEX_AUTH_CODEX_DIR = codexDir;
|
|
56
57
|
delete process.env.CODEX_AUTH_ACCOUNTS_DIR;
|
|
@@ -61,6 +62,7 @@ async function withIsolatedCodexDir(t, fn) {
|
|
|
61
62
|
process.env.CODEX_AUTH_ACCOUNTS_DIR = previousEnv.CODEX_AUTH_ACCOUNTS_DIR;
|
|
62
63
|
process.env.CODEX_AUTH_JSON_PATH = previousEnv.CODEX_AUTH_JSON_PATH;
|
|
63
64
|
process.env.CODEX_AUTH_CURRENT_PATH = previousEnv.CODEX_AUTH_CURRENT_PATH;
|
|
65
|
+
process.env.CODEX_AUTH_SESSION_ACTIVE_OVERRIDE = previousEnv.CODEX_AUTH_SESSION_ACTIVE_OVERRIDE;
|
|
64
66
|
await promises_1.default.rm(codexDir, { recursive: true, force: true });
|
|
65
67
|
});
|
|
66
68
|
await fn({ codexDir, accountsDir, authPath });
|
|
@@ -426,12 +428,42 @@ async function withIsolatedCodexDir(t, fn) {
|
|
|
426
428
|
strict_1.default.equal(authStat.isSymbolicLink(), false);
|
|
427
429
|
});
|
|
428
430
|
});
|
|
429
|
-
(0, node_test_1.default)("getCurrentAccountName
|
|
431
|
+
(0, node_test_1.default)("getCurrentAccountName falls back to global current pointer when codex is not active in this terminal", async (t) => {
|
|
430
432
|
await withIsolatedCodexDir(t, async ({ codexDir, accountsDir }) => {
|
|
431
433
|
const service = new account_service_1.AccountService();
|
|
432
434
|
const currentPath = node_path_1.default.join(codexDir, "current");
|
|
433
435
|
const sessionMapPath = node_path_1.default.join(accountsDir, "sessions.json");
|
|
434
436
|
const sessionKey = `ppid:${process.ppid}`;
|
|
437
|
+
process.env.CODEX_AUTH_SESSION_ACTIVE_OVERRIDE = "0";
|
|
438
|
+
await promises_1.default.writeFile(node_path_1.default.join(accountsDir, "odin@megkapja.hu.json"), buildAuthPayload("odin@megkapja.hu", {
|
|
439
|
+
accountId: "acct-odin",
|
|
440
|
+
userId: "user-odin",
|
|
441
|
+
}));
|
|
442
|
+
await promises_1.default.writeFile(node_path_1.default.join(accountsDir, "lajos@edix.hu.json"), buildAuthPayload("lajos@edix.hu", {
|
|
443
|
+
accountId: "acct-lajos",
|
|
444
|
+
userId: "user-lajos",
|
|
445
|
+
}));
|
|
446
|
+
await promises_1.default.writeFile(currentPath, "lajos@edix.hu\n", "utf8");
|
|
447
|
+
await promises_1.default.writeFile(sessionMapPath, `${JSON.stringify({
|
|
448
|
+
version: 1,
|
|
449
|
+
sessions: {
|
|
450
|
+
[sessionKey]: {
|
|
451
|
+
accountName: "odin@megkapja.hu",
|
|
452
|
+
updatedAt: new Date().toISOString(),
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
}, null, 2)}\n`, "utf8");
|
|
456
|
+
const active = await service.getCurrentAccountName();
|
|
457
|
+
strict_1.default.equal(active, "lajos@edix.hu");
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
(0, node_test_1.default)("getCurrentAccountName prefers session-scoped snapshot when codex is active in this terminal", async (t) => {
|
|
461
|
+
await withIsolatedCodexDir(t, async ({ codexDir, accountsDir }) => {
|
|
462
|
+
const service = new account_service_1.AccountService();
|
|
463
|
+
const currentPath = node_path_1.default.join(codexDir, "current");
|
|
464
|
+
const sessionMapPath = node_path_1.default.join(accountsDir, "sessions.json");
|
|
465
|
+
const sessionKey = `ppid:${process.ppid}`;
|
|
466
|
+
process.env.CODEX_AUTH_SESSION_ACTIVE_OVERRIDE = "1";
|
|
435
467
|
await promises_1.default.writeFile(node_path_1.default.join(accountsDir, "odin@megkapja.hu.json"), buildAuthPayload("odin@megkapja.hu", {
|
|
436
468
|
accountId: "acct-odin",
|
|
437
469
|
userId: "user-odin",
|
|
@@ -454,12 +486,49 @@ async function withIsolatedCodexDir(t, fn) {
|
|
|
454
486
|
strict_1.default.equal(active, "odin@megkapja.hu");
|
|
455
487
|
});
|
|
456
488
|
});
|
|
457
|
-
(0, node_test_1.default)("syncExternalAuthSnapshotIfNeeded
|
|
489
|
+
(0, node_test_1.default)("syncExternalAuthSnapshotIfNeeded follows global sync when codex is not active in this terminal", async (t) => {
|
|
490
|
+
await withIsolatedCodexDir(t, async ({ codexDir, accountsDir, authPath }) => {
|
|
491
|
+
const service = new account_service_1.AccountService();
|
|
492
|
+
const currentPath = node_path_1.default.join(codexDir, "current");
|
|
493
|
+
const sessionMapPath = node_path_1.default.join(accountsDir, "sessions.json");
|
|
494
|
+
const sessionKey = `ppid:${process.ppid}`;
|
|
495
|
+
process.env.CODEX_AUTH_SESSION_ACTIVE_OVERRIDE = "0";
|
|
496
|
+
await promises_1.default.writeFile(node_path_1.default.join(accountsDir, "odin@megkapja.hu.json"), buildAuthPayload("odin@megkapja.hu", {
|
|
497
|
+
accountId: "acct-odin",
|
|
498
|
+
userId: "user-odin",
|
|
499
|
+
}));
|
|
500
|
+
await promises_1.default.writeFile(currentPath, "odin@megkapja.hu\n", "utf8");
|
|
501
|
+
await promises_1.default.writeFile(sessionMapPath, `${JSON.stringify({
|
|
502
|
+
version: 1,
|
|
503
|
+
sessions: {
|
|
504
|
+
[sessionKey]: {
|
|
505
|
+
accountName: "odin@megkapja.hu",
|
|
506
|
+
updatedAt: new Date().toISOString(),
|
|
507
|
+
},
|
|
508
|
+
},
|
|
509
|
+
}, null, 2)}\n`, "utf8");
|
|
510
|
+
await promises_1.default.writeFile(authPath, buildAuthPayload("lajos@edix.hu", {
|
|
511
|
+
accountId: "acct-lajos",
|
|
512
|
+
userId: "user-lajos",
|
|
513
|
+
}), "utf8");
|
|
514
|
+
const result = await service.syncExternalAuthSnapshotIfNeeded();
|
|
515
|
+
strict_1.default.deepEqual(result, {
|
|
516
|
+
synchronized: true,
|
|
517
|
+
savedName: "lajos@edix.hu",
|
|
518
|
+
autoSwitchDisabled: false,
|
|
519
|
+
});
|
|
520
|
+
const parsed = await (0, auth_parser_1.parseAuthSnapshotFile)(node_path_1.default.join(accountsDir, "lajos@edix.hu.json"));
|
|
521
|
+
strict_1.default.equal(parsed.email, "lajos@edix.hu");
|
|
522
|
+
strict_1.default.equal((await promises_1.default.readFile(currentPath, "utf8")).trim(), "lajos@edix.hu");
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
(0, node_test_1.default)("syncExternalAuthSnapshotIfNeeded ignores external login from another terminal when codex remains active in this terminal", async (t) => {
|
|
458
526
|
await withIsolatedCodexDir(t, async ({ codexDir, accountsDir, authPath }) => {
|
|
459
527
|
const service = new account_service_1.AccountService();
|
|
460
528
|
const currentPath = node_path_1.default.join(codexDir, "current");
|
|
461
529
|
const sessionMapPath = node_path_1.default.join(accountsDir, "sessions.json");
|
|
462
530
|
const sessionKey = `ppid:${process.ppid}`;
|
|
531
|
+
process.env.CODEX_AUTH_SESSION_ACTIVE_OVERRIDE = "1";
|
|
463
532
|
await promises_1.default.writeFile(node_path_1.default.join(accountsDir, "odin@megkapja.hu.json"), buildAuthPayload("odin@megkapja.hu", {
|
|
464
533
|
accountId: "acct-odin",
|
|
465
534
|
userId: "user-odin",
|
|
@@ -525,12 +594,51 @@ async function withIsolatedCodexDir(t, fn) {
|
|
|
525
594
|
strict_1.default.equal((await promises_1.default.readFile(currentPath, "utf8")).trim(), "lajos@edix.hu");
|
|
526
595
|
});
|
|
527
596
|
});
|
|
528
|
-
(0, node_test_1.default)("restoreSessionSnapshotIfNeeded
|
|
597
|
+
(0, node_test_1.default)("restoreSessionSnapshotIfNeeded skips restore when codex is not active in this terminal", async (t) => {
|
|
598
|
+
await withIsolatedCodexDir(t, async ({ codexDir, accountsDir, authPath }) => {
|
|
599
|
+
const service = new account_service_1.AccountService();
|
|
600
|
+
const currentPath = node_path_1.default.join(codexDir, "current");
|
|
601
|
+
const sessionMapPath = node_path_1.default.join(accountsDir, "sessions.json");
|
|
602
|
+
const sessionKey = `ppid:${process.ppid}`;
|
|
603
|
+
process.env.CODEX_AUTH_SESSION_ACTIVE_OVERRIDE = "0";
|
|
604
|
+
await promises_1.default.writeFile(node_path_1.default.join(accountsDir, "odin@megkapja.hu.json"), buildAuthPayload("odin@megkapja.hu", {
|
|
605
|
+
accountId: "acct-odin",
|
|
606
|
+
userId: "user-odin",
|
|
607
|
+
}));
|
|
608
|
+
await promises_1.default.writeFile(node_path_1.default.join(accountsDir, "lajos@edix.hu.json"), buildAuthPayload("lajos@edix.hu", {
|
|
609
|
+
accountId: "acct-lajos",
|
|
610
|
+
userId: "user-lajos",
|
|
611
|
+
}));
|
|
612
|
+
await promises_1.default.writeFile(currentPath, "lajos@edix.hu\n", "utf8");
|
|
613
|
+
await promises_1.default.writeFile(sessionMapPath, `${JSON.stringify({
|
|
614
|
+
version: 1,
|
|
615
|
+
sessions: {
|
|
616
|
+
[sessionKey]: {
|
|
617
|
+
accountName: "odin@megkapja.hu",
|
|
618
|
+
updatedAt: new Date().toISOString(),
|
|
619
|
+
},
|
|
620
|
+
},
|
|
621
|
+
}, null, 2)}\n`, "utf8");
|
|
622
|
+
await promises_1.default.writeFile(authPath, buildAuthPayload("lajos@edix.hu", {
|
|
623
|
+
accountId: "acct-lajos",
|
|
624
|
+
userId: "user-lajos",
|
|
625
|
+
}), "utf8");
|
|
626
|
+
const restored = await service.restoreSessionSnapshotIfNeeded();
|
|
627
|
+
strict_1.default.deepEqual(restored, {
|
|
628
|
+
restored: false,
|
|
629
|
+
});
|
|
630
|
+
strict_1.default.equal((await promises_1.default.readFile(currentPath, "utf8")).trim(), "lajos@edix.hu");
|
|
631
|
+
const parsed = await (0, auth_parser_1.parseAuthSnapshotFile)(authPath);
|
|
632
|
+
strict_1.default.equal(parsed.email, "lajos@edix.hu");
|
|
633
|
+
});
|
|
634
|
+
});
|
|
635
|
+
(0, node_test_1.default)("restoreSessionSnapshotIfNeeded re-activates the session-pinned snapshot while codex stays active in this terminal", async (t) => {
|
|
529
636
|
await withIsolatedCodexDir(t, async ({ codexDir, accountsDir, authPath }) => {
|
|
530
637
|
const service = new account_service_1.AccountService();
|
|
531
638
|
const currentPath = node_path_1.default.join(codexDir, "current");
|
|
532
639
|
const sessionMapPath = node_path_1.default.join(accountsDir, "sessions.json");
|
|
533
640
|
const sessionKey = `ppid:${process.ppid}`;
|
|
641
|
+
process.env.CODEX_AUTH_SESSION_ACTIVE_OVERRIDE = "1";
|
|
534
642
|
await promises_1.default.writeFile(node_path_1.default.join(accountsDir, "odin@megkapja.hu.json"), buildAuthPayload("odin@megkapja.hu", {
|
|
535
643
|
accountId: "acct-odin",
|
|
536
644
|
userId: "user-odin",
|
|
@@ -65,3 +65,14 @@ const update_check_1 = require("../lib/update-check");
|
|
|
65
65
|
strict_1.default.equal(lines[0], "┌─ codex-auth update");
|
|
66
66
|
strict_1.default.equal(lines[3], "└─ status : update available");
|
|
67
67
|
});
|
|
68
|
+
(0, node_test_1.default)("shouldProceedWithYesDefault accepts enter and yes responses", () => {
|
|
69
|
+
strict_1.default.equal((0, update_check_1.shouldProceedWithYesDefault)(""), true);
|
|
70
|
+
strict_1.default.equal((0, update_check_1.shouldProceedWithYesDefault)(" "), true);
|
|
71
|
+
strict_1.default.equal((0, update_check_1.shouldProceedWithYesDefault)("y"), true);
|
|
72
|
+
strict_1.default.equal((0, update_check_1.shouldProceedWithYesDefault)("Yes"), true);
|
|
73
|
+
});
|
|
74
|
+
(0, node_test_1.default)("shouldProceedWithYesDefault rejects no and unknown responses", () => {
|
|
75
|
+
strict_1.default.equal((0, update_check_1.shouldProceedWithYesDefault)("n"), false);
|
|
76
|
+
strict_1.default.equal((0, update_check_1.shouldProceedWithYesDefault)("No"), false);
|
|
77
|
+
strict_1.default.equal((0, update_check_1.shouldProceedWithYesDefault)("later"), false);
|
|
78
|
+
});
|
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.15",
|
|
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": {
|
|
@@ -44,11 +44,11 @@
|
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@oclif/core": "^3.0.0",
|
|
46
46
|
"prompts": "^2.4.2",
|
|
47
|
-
"tslib": "^2.8.1"
|
|
47
|
+
"tslib": "^2.8.1",
|
|
48
|
+
"typescript": "^5.6.3"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
50
|
-
"@types/prompts": "^2.4.9"
|
|
51
|
-
"typescript": "^5.6.3"
|
|
51
|
+
"@types/prompts": "^2.4.9"
|
|
52
52
|
},
|
|
53
53
|
"oclif": {
|
|
54
54
|
"bin": "codex-auth",
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const fs = require("node:fs/promises");
|
|
4
|
+
const fsSync = require("node:fs");
|
|
4
5
|
const os = require("node:os");
|
|
5
6
|
const path = require("node:path");
|
|
7
|
+
const { spawnSync } = require("node:child_process");
|
|
6
8
|
const readline = require("node:readline/promises");
|
|
7
9
|
|
|
8
10
|
const MARK_START = "# >>> codex-auth-login-auto-snapshot >>>";
|
|
@@ -21,7 +23,7 @@ function targetShellRc() {
|
|
|
21
23
|
function renderHookBlock() {
|
|
22
24
|
return [
|
|
23
25
|
MARK_START,
|
|
24
|
-
"#
|
|
26
|
+
"# Keep terminal-scoped snapshot memory in sync before/after each `codex` run.",
|
|
25
27
|
"# Also restore common terminal modes to avoid leaked escape sequences after codex exits.",
|
|
26
28
|
"__codex_auth_restore_tty() {",
|
|
27
29
|
" [[ -t 1 ]] || return 0",
|
|
@@ -29,39 +31,81 @@ function renderHookBlock() {
|
|
|
29
31
|
" [[ -w \"$__tty_target\" ]] || __tty_target=/dev/stdout",
|
|
30
32
|
" 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
33
|
"}",
|
|
32
|
-
"
|
|
33
|
-
" codex
|
|
34
|
-
" command codex
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
" if [[ \"$__first_non_flag\" == \"login\" ]] && command -v codex-auth >/dev/null 2>&1; then",
|
|
47
|
-
" command codex-auth status >/dev/null 2>&1 || true",
|
|
48
|
-
" fi",
|
|
49
|
-
" fi",
|
|
50
|
-
" if [[ -z \"${CODEX_AUTH_SKIP_TTY_RESTORE:-}\" ]]; then",
|
|
51
|
-
" __codex_auth_restore_tty",
|
|
52
|
-
" fi",
|
|
53
|
-
" return $__codex_exit",
|
|
54
|
-
" }",
|
|
55
|
-
"fi",
|
|
34
|
+
"codex() {",
|
|
35
|
+
" if command -v codex-auth >/dev/null 2>&1; then",
|
|
36
|
+
" command codex-auth restore-session >/dev/null 2>&1 || true",
|
|
37
|
+
" fi",
|
|
38
|
+
" command codex \"$@\"",
|
|
39
|
+
" local __codex_exit=$?",
|
|
40
|
+
" if command -v codex-auth >/dev/null 2>&1; then",
|
|
41
|
+
" CODEX_AUTH_FORCE_EXTERNAL_SYNC=1 command codex-auth status >/dev/null 2>&1 || true",
|
|
42
|
+
" fi",
|
|
43
|
+
" if [[ -z \"${CODEX_AUTH_SKIP_TTY_RESTORE:-}\" ]]; then",
|
|
44
|
+
" __codex_auth_restore_tty",
|
|
45
|
+
" fi",
|
|
46
|
+
" return $__codex_exit",
|
|
47
|
+
"}",
|
|
56
48
|
MARK_END,
|
|
57
49
|
].join("\n");
|
|
58
50
|
}
|
|
59
51
|
|
|
52
|
+
function escapeRegex(input) {
|
|
53
|
+
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function hookBlockRegex() {
|
|
57
|
+
const start = escapeRegex(MARK_START);
|
|
58
|
+
const end = escapeRegex(MARK_END);
|
|
59
|
+
return new RegExp(`\\n?${start}[\\s\\S]*?${end}\\n?`, "g");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeRcContents(contents) {
|
|
63
|
+
const collapsed = contents.replace(/\n{3,}/g, "\n\n");
|
|
64
|
+
return `${collapsed.replace(/\s*$/, "")}\n`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function ensureBuiltDist() {
|
|
68
|
+
const projectRoot = path.resolve(__dirname, "..");
|
|
69
|
+
const distEntry = path.join(projectRoot, "dist", "index.js");
|
|
70
|
+
if (fsSync.existsSync(distEntry)) return;
|
|
71
|
+
|
|
72
|
+
const tscPath = path.join(projectRoot, "node_modules", "typescript", "bin", "tsc");
|
|
73
|
+
if (fsSync.existsSync(tscPath)) {
|
|
74
|
+
const result = spawnSync(process.execPath, [tscPath, "-p", "tsconfig.json"], {
|
|
75
|
+
cwd: projectRoot,
|
|
76
|
+
stdio: "inherit",
|
|
77
|
+
});
|
|
78
|
+
if (result.status !== 0) {
|
|
79
|
+
throw new Error(`TypeScript build failed with exit code ${result.status ?? "unknown"}.`);
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const npmBinary = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
85
|
+
const fallback = spawnSync(
|
|
86
|
+
npmBinary,
|
|
87
|
+
["exec", "--yes", "--package", "typescript@5.6.3", "--", "tsc", "-p", "tsconfig.json"],
|
|
88
|
+
{
|
|
89
|
+
cwd: projectRoot,
|
|
90
|
+
stdio: "inherit",
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
if (fallback.status !== 0) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Missing TypeScript compiler for git install bootstrap (fallback exit ${fallback.status ?? "unknown"}).`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!fsSync.existsSync(distEntry)) {
|
|
100
|
+
throw new Error("TypeScript build completed but dist/index.js is still missing.");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
60
104
|
async function maybeInstallHook() {
|
|
61
105
|
if (process.env.npm_config_global !== "true") return;
|
|
62
106
|
if (isTruthy(process.env.CODEX_AUTH_SKIP_POSTINSTALL)) return;
|
|
63
107
|
if (isTruthy(process.env.CI)) return;
|
|
64
|
-
|
|
108
|
+
const canPrompt = process.stdin.isTTY && process.stdout.isTTY;
|
|
65
109
|
|
|
66
110
|
const rcPath = targetShellRc();
|
|
67
111
|
await fs.mkdir(path.dirname(rcPath), { recursive: true });
|
|
@@ -73,7 +117,16 @@ async function maybeInstallHook() {
|
|
|
73
117
|
if (error && error.code !== "ENOENT") throw error;
|
|
74
118
|
}
|
|
75
119
|
|
|
76
|
-
if (rc.includes(MARK_START) && rc.includes(MARK_END))
|
|
120
|
+
if (rc.includes(MARK_START) && rc.includes(MARK_END)) {
|
|
121
|
+
const refreshed = normalizeRcContents(rc.replace(hookBlockRegex(), `\n${renderHookBlock()}\n`));
|
|
122
|
+
if (refreshed !== normalizeRcContents(rc)) {
|
|
123
|
+
await fs.writeFile(rcPath, refreshed, "utf8");
|
|
124
|
+
process.stdout.write(`\nUpdated shell hook in ${rcPath}. Restart terminal or run: source ${rcPath}\n`);
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!canPrompt) return;
|
|
77
130
|
|
|
78
131
|
const rl = readline.createInterface({
|
|
79
132
|
input: process.stdin,
|
|
@@ -89,12 +142,20 @@ async function maybeInstallHook() {
|
|
|
89
142
|
rl.close();
|
|
90
143
|
}
|
|
91
144
|
|
|
92
|
-
const next = `${rc
|
|
145
|
+
const next = normalizeRcContents(`${rc}\n\n${renderHookBlock()}\n`);
|
|
93
146
|
await fs.writeFile(rcPath, next, "utf8");
|
|
94
147
|
process.stdout.write(`\nInstalled shell hook in ${rcPath}. Restart terminal or run: source ${rcPath}\n`);
|
|
95
148
|
}
|
|
96
149
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
150
|
+
function runPostinstall() {
|
|
151
|
+
ensureBuiltDist();
|
|
152
|
+
return maybeInstallHook();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
Promise.resolve()
|
|
156
|
+
.then(() => runPostinstall())
|
|
157
|
+
.catch((error) => {
|
|
158
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
159
|
+
process.stderr.write(`\n[codex-auth postinstall] Failed: ${message}\n`);
|
|
160
|
+
process.exitCode = 1;
|
|
161
|
+
});
|