@imdeadpool/codex-account-switcher 0.1.6 → 0.1.8

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 CHANGED
@@ -19,6 +19,14 @@ Codex stores your authentication session in a single `auth.json` file. This tool
19
19
  npm i -g @imdeadpool/codex-account-switcher
20
20
  ```
21
21
 
22
+ During global install, the package asks for permission to add an optional shell hook
23
+ (`~/.bashrc` or `~/.zshrc`) that auto-runs a silent snapshot sync after successful
24
+ official `codex login`.
25
+
26
+ - Choose `y` to enable fully automatic login snapshot capture.
27
+ - Choose `n` (default) to skip.
28
+ - Set `CODEX_AUTH_SKIP_POSTINSTALL=1` to always suppress this prompt.
29
+
22
30
  ## Usage
23
31
 
24
32
  ```sh
@@ -37,7 +45,7 @@ codex-auth save <name>
37
45
  # force overwrite a name even when it currently maps to a different email
38
46
  codex-auth save <name> --force
39
47
 
40
- # switch active account (symlinks on macOS/Linux; copies on Windows)
48
+ # switch active account
41
49
  codex-auth use <name>
42
50
 
43
51
  # or pick interactively
@@ -74,13 +82,18 @@ codex-auth config api disable
74
82
  # daemon runtime (internal/service use)
75
83
  codex-auth daemon --once
76
84
  codex-auth daemon --watch
85
+
86
+ # optional shell hook helpers
87
+ codex-auth setup-login-hook
88
+ codex-auth hook-status
89
+ codex-auth remove-login-hook
77
90
  ```
78
91
 
79
92
  ### Command reference
80
93
 
81
94
  - `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
95
  - `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.
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.
96
+ - `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.
84
97
  - `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.
85
98
  - `codex-auth current` – Prints the active account name, or a friendly message if none is active.
86
99
  - `codex-auth remove [query|--all]` – Removes snapshots interactively or by selector. If the active account is removed, the best remaining account is activated automatically.
@@ -88,6 +101,9 @@ codex-auth daemon --watch
88
101
  - `codex-auth config auto ...` – Enables/disables managed auto-switch and updates threshold percentages.
89
102
  - `codex-auth config api enable|disable` – Chooses usage source mode (`api` or `local`).
90
103
  - `codex-auth daemon --once|--watch` – Runs the auto-switch loop once or continuously.
104
+ - `codex-auth setup-login-hook [-f <path>]` – Installs an optional shell hook in your rc file to auto-sync snapshots after successful official `codex login`.
105
+ - `codex-auth hook-status [-f <path>]` – Shows whether the optional login auto-snapshot hook is installed for the selected rc file.
106
+ - `codex-auth remove-login-hook [-f <path>]` – Removes the optional shell hook.
91
107
 
92
108
  ### Auto-switch behavior
93
109
 
@@ -113,5 +129,5 @@ Usage refresh is hybrid:
113
129
 
114
130
  Notes:
115
131
 
116
- - Works on macOS/Linux (symlink) and Windows (copy).
132
+ - Works on macOS/Linux/Windows (regular-file auth snapshot activation).
117
133
  - Requires Node 18+.
@@ -0,0 +1,9 @@
1
+ import { BaseCommand } from "../lib/base-command";
2
+ export default class HookStatusCommand extends BaseCommand {
3
+ protected readonly syncExternalAuthBeforeRun = false;
4
+ static description: string;
5
+ static flags: {
6
+ readonly shellRc: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
7
+ };
8
+ run(): Promise<void>;
9
+ }
@@ -0,0 +1,30 @@
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
+ const login_hook_1 = require("../lib/config/login-hook");
6
+ class HookStatusCommand extends base_command_1.BaseCommand {
7
+ constructor() {
8
+ super(...arguments);
9
+ this.syncExternalAuthBeforeRun = false;
10
+ }
11
+ async run() {
12
+ await this.runSafe(async () => {
13
+ var _a;
14
+ const { flags } = await this.parse(HookStatusCommand);
15
+ const rcPath = (_a = flags.shellRc) !== null && _a !== void 0 ? _a : (0, login_hook_1.resolveDefaultShellRcPath)();
16
+ const status = await (0, login_hook_1.getLoginHookStatus)(rcPath);
17
+ this.log(`login-hook: ${status.installed ? "installed" : "not-installed"}`);
18
+ this.log(`rc-file: ${status.rcPath}`);
19
+ });
20
+ }
21
+ }
22
+ HookStatusCommand.description = "Show whether the login auto-snapshot shell hook is installed";
23
+ HookStatusCommand.flags = {
24
+ shellRc: core_1.Flags.string({
25
+ char: "f",
26
+ description: "Explicit shell rc file path (defaults to ~/.bashrc or ~/.zshrc)",
27
+ required: false,
28
+ }),
29
+ };
30
+ exports.default = HookStatusCommand;
@@ -0,0 +1,9 @@
1
+ import { BaseCommand } from "../lib/base-command";
2
+ export default class RemoveLoginHookCommand extends BaseCommand {
3
+ protected readonly syncExternalAuthBeforeRun = false;
4
+ static description: string;
5
+ static flags: {
6
+ readonly shellRc: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
7
+ };
8
+ run(): Promise<void>;
9
+ }
@@ -0,0 +1,35 @@
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
+ const login_hook_1 = require("../lib/config/login-hook");
6
+ class RemoveLoginHookCommand extends base_command_1.BaseCommand {
7
+ constructor() {
8
+ super(...arguments);
9
+ this.syncExternalAuthBeforeRun = false;
10
+ }
11
+ async run() {
12
+ await this.runSafe(async () => {
13
+ var _a;
14
+ const { flags } = await this.parse(RemoveLoginHookCommand);
15
+ const rcPath = (_a = flags.shellRc) !== null && _a !== void 0 ? _a : (0, login_hook_1.resolveDefaultShellRcPath)();
16
+ const result = await (0, login_hook_1.removeLoginHook)(rcPath);
17
+ if (result === "not-installed") {
18
+ this.log(`No login auto-snapshot hook found in ${rcPath}.`);
19
+ }
20
+ else {
21
+ this.log(`Removed login auto-snapshot hook from ${rcPath}.`);
22
+ }
23
+ this.log(`Reload your shell: source ${rcPath}`);
24
+ });
25
+ }
26
+ }
27
+ RemoveLoginHookCommand.description = "Remove the shell hook that auto-syncs snapshots after `codex login`";
28
+ RemoveLoginHookCommand.flags = {
29
+ shellRc: core_1.Flags.string({
30
+ char: "f",
31
+ description: "Explicit shell rc file path (defaults to ~/.bashrc or ~/.zshrc)",
32
+ required: false,
33
+ }),
34
+ };
35
+ exports.default = RemoveLoginHookCommand;
@@ -0,0 +1,9 @@
1
+ import { BaseCommand } from "../lib/base-command";
2
+ export default class SetupLoginHookCommand extends BaseCommand {
3
+ protected readonly syncExternalAuthBeforeRun = false;
4
+ static description: string;
5
+ static flags: {
6
+ readonly shellRc: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
7
+ };
8
+ run(): Promise<void>;
9
+ }
@@ -0,0 +1,35 @@
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
+ const login_hook_1 = require("../lib/config/login-hook");
6
+ class SetupLoginHookCommand extends base_command_1.BaseCommand {
7
+ constructor() {
8
+ super(...arguments);
9
+ this.syncExternalAuthBeforeRun = false;
10
+ }
11
+ async run() {
12
+ await this.runSafe(async () => {
13
+ var _a;
14
+ const { flags } = await this.parse(SetupLoginHookCommand);
15
+ const rcPath = (_a = flags.shellRc) !== null && _a !== void 0 ? _a : (0, login_hook_1.resolveDefaultShellRcPath)();
16
+ const result = await (0, login_hook_1.installLoginHook)(rcPath);
17
+ if (result === "already-installed") {
18
+ this.log(`Login auto-snapshot hook is already installed in ${rcPath}.`);
19
+ }
20
+ else {
21
+ this.log(`Installed login auto-snapshot hook in ${rcPath}.`);
22
+ }
23
+ this.log(`Reload your shell: source ${rcPath}`);
24
+ });
25
+ }
26
+ }
27
+ SetupLoginHookCommand.description = "Install a shell hook that auto-syncs snapshots after successful `codex login`";
28
+ SetupLoginHookCommand.flags = {
29
+ shellRc: core_1.Flags.string({
30
+ char: "f",
31
+ description: "Explicit shell rc file path (defaults to ~/.bashrc or ~/.zshrc)",
32
+ required: false,
33
+ }),
34
+ };
35
+ exports.default = SetupLoginHookCommand;
@@ -0,0 +1,13 @@
1
+ export declare const LOGIN_HOOK_MARK_START = "# >>> codex-auth-login-auto-snapshot >>>";
2
+ export declare const LOGIN_HOOK_MARK_END = "# <<< codex-auth-login-auto-snapshot <<<";
3
+ export type HookInstallStatus = "installed" | "already-installed";
4
+ export type HookRemoveStatus = "removed" | "not-installed";
5
+ export interface LoginHookStatus {
6
+ installed: boolean;
7
+ rcPath: string;
8
+ }
9
+ export declare function resolveDefaultShellRcPath(): string;
10
+ export declare function renderLoginHookBlock(): string;
11
+ export declare function installLoginHook(rcPath?: string): Promise<HookInstallStatus>;
12
+ export declare function removeLoginHook(rcPath?: string): Promise<HookRemoveStatus>;
13
+ export declare function getLoginHookStatus(rcPath?: string): Promise<LoginHookStatus>;
@@ -0,0 +1,119 @@
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
+ exports.LOGIN_HOOK_MARK_END = exports.LOGIN_HOOK_MARK_START = void 0;
7
+ exports.resolveDefaultShellRcPath = resolveDefaultShellRcPath;
8
+ exports.renderLoginHookBlock = renderLoginHookBlock;
9
+ exports.installLoginHook = installLoginHook;
10
+ exports.removeLoginHook = removeLoginHook;
11
+ exports.getLoginHookStatus = getLoginHookStatus;
12
+ const promises_1 = __importDefault(require("node:fs/promises"));
13
+ const node_os_1 = __importDefault(require("node:os"));
14
+ const node_path_1 = __importDefault(require("node:path"));
15
+ exports.LOGIN_HOOK_MARK_START = "# >>> codex-auth-login-auto-snapshot >>>";
16
+ exports.LOGIN_HOOK_MARK_END = "# <<< codex-auth-login-auto-snapshot <<<";
17
+ function escapeRegex(input) {
18
+ return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
19
+ }
20
+ function hookBlockRegex() {
21
+ const start = escapeRegex(exports.LOGIN_HOOK_MARK_START);
22
+ const end = escapeRegex(exports.LOGIN_HOOK_MARK_END);
23
+ return new RegExp(`\\n?${start}[\\s\\S]*?${end}\\n?`, "g");
24
+ }
25
+ function resolveDefaultShellRcPath() {
26
+ var _a;
27
+ const shell = ((_a = process.env.SHELL) !== null && _a !== void 0 ? _a : "").toLowerCase();
28
+ if (shell.includes("zsh")) {
29
+ return node_path_1.default.join(node_os_1.default.homedir(), ".zshrc");
30
+ }
31
+ return node_path_1.default.join(node_os_1.default.homedir(), ".bashrc");
32
+ }
33
+ function renderLoginHookBlock() {
34
+ return [
35
+ exports.LOGIN_HOOK_MARK_START,
36
+ "# Auto-sync codex-auth snapshots after successful official `codex login`.",
37
+ "if ! typeset -f codex >/dev/null 2>&1; then",
38
+ " codex() {",
39
+ " command codex \"$@\"",
40
+ " local __codex_exit=$?",
41
+ " if [[ $__codex_exit -eq 0 ]]; then",
42
+ " local __first_non_flag=\"\"",
43
+ " local __arg",
44
+ " for __arg in \"$@\"; do",
45
+ " case \"$__arg\" in",
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",
54
+ " fi",
55
+ " return $__codex_exit",
56
+ " }",
57
+ "fi",
58
+ exports.LOGIN_HOOK_MARK_END,
59
+ ].join("\n");
60
+ }
61
+ async function installLoginHook(rcPath = resolveDefaultShellRcPath()) {
62
+ await promises_1.default.mkdir(node_path_1.default.dirname(rcPath), { recursive: true });
63
+ let existing = "";
64
+ try {
65
+ existing = await promises_1.default.readFile(rcPath, "utf8");
66
+ }
67
+ catch (error) {
68
+ const err = error;
69
+ if (err.code !== "ENOENT")
70
+ throw error;
71
+ }
72
+ if (existing.includes(exports.LOGIN_HOOK_MARK_START) && existing.includes(exports.LOGIN_HOOK_MARK_END)) {
73
+ return "already-installed";
74
+ }
75
+ const next = `${existing.replace(/\s*$/, "")}\n\n${renderLoginHookBlock()}\n`;
76
+ await promises_1.default.writeFile(rcPath, next, "utf8");
77
+ return "installed";
78
+ }
79
+ async function removeLoginHook(rcPath = resolveDefaultShellRcPath()) {
80
+ let existing = "";
81
+ try {
82
+ existing = await promises_1.default.readFile(rcPath, "utf8");
83
+ }
84
+ catch (error) {
85
+ const err = error;
86
+ if (err.code === "ENOENT") {
87
+ return "not-installed";
88
+ }
89
+ throw error;
90
+ }
91
+ const regex = hookBlockRegex();
92
+ const stripped = existing.replace(regex, "\n");
93
+ if (stripped === existing) {
94
+ return "not-installed";
95
+ }
96
+ await promises_1.default.writeFile(rcPath, stripped.replace(/\n{3,}/g, "\n\n"), "utf8");
97
+ return "removed";
98
+ }
99
+ async function getLoginHookStatus(rcPath = resolveDefaultShellRcPath()) {
100
+ let existing = "";
101
+ try {
102
+ existing = await promises_1.default.readFile(rcPath, "utf8");
103
+ }
104
+ catch (error) {
105
+ const err = error;
106
+ if (err.code === "ENOENT") {
107
+ return {
108
+ installed: false,
109
+ rcPath,
110
+ };
111
+ }
112
+ throw error;
113
+ }
114
+ const installed = existing.includes(exports.LOGIN_HOOK_MARK_START) && existing.includes(exports.LOGIN_HOOK_MARK_END);
115
+ return {
116
+ installed,
117
+ rcPath,
118
+ };
119
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,67 @@
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 node_test_1 = __importDefault(require("node:test"));
7
+ const strict_1 = __importDefault(require("node:assert/strict"));
8
+ const node_os_1 = __importDefault(require("node:os"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const promises_1 = __importDefault(require("node:fs/promises"));
11
+ const login_hook_1 = require("../lib/config/login-hook");
12
+ async function withTempRcFile(t, fn) {
13
+ const tempDir = await promises_1.default.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), "codex-auth-hook-"));
14
+ const rcPath = node_path_1.default.join(tempDir, ".bashrc");
15
+ await promises_1.default.writeFile(rcPath, "# test bashrc\n", "utf8");
16
+ t.after(async () => {
17
+ await promises_1.default.rm(tempDir, { recursive: true, force: true });
18
+ });
19
+ await fn(rcPath);
20
+ }
21
+ (0, node_test_1.default)("installLoginHook writes marker block when missing", async (t) => {
22
+ await withTempRcFile(t, async (rcPath) => {
23
+ const result = await (0, login_hook_1.installLoginHook)(rcPath);
24
+ strict_1.default.equal(result, "installed");
25
+ const contents = await promises_1.default.readFile(rcPath, "utf8");
26
+ strict_1.default.ok(contents.includes(login_hook_1.LOGIN_HOOK_MARK_START));
27
+ strict_1.default.ok(contents.includes(login_hook_1.LOGIN_HOOK_MARK_END));
28
+ });
29
+ });
30
+ (0, node_test_1.default)("installLoginHook is idempotent", async (t) => {
31
+ await withTempRcFile(t, async (rcPath) => {
32
+ const first = await (0, login_hook_1.installLoginHook)(rcPath);
33
+ const second = await (0, login_hook_1.installLoginHook)(rcPath);
34
+ strict_1.default.equal(first, "installed");
35
+ strict_1.default.equal(second, "already-installed");
36
+ const contents = await promises_1.default.readFile(rcPath, "utf8");
37
+ const startCount = contents.split(login_hook_1.LOGIN_HOOK_MARK_START).length - 1;
38
+ strict_1.default.equal(startCount, 1);
39
+ });
40
+ });
41
+ (0, node_test_1.default)("removeLoginHook removes installed marker block", async (t) => {
42
+ await withTempRcFile(t, async (rcPath) => {
43
+ await (0, login_hook_1.installLoginHook)(rcPath);
44
+ const result = await (0, login_hook_1.removeLoginHook)(rcPath);
45
+ strict_1.default.equal(result, "removed");
46
+ const contents = await promises_1.default.readFile(rcPath, "utf8");
47
+ strict_1.default.ok(!contents.includes(login_hook_1.LOGIN_HOOK_MARK_START));
48
+ strict_1.default.ok(!contents.includes(login_hook_1.LOGIN_HOOK_MARK_END));
49
+ });
50
+ });
51
+ (0, node_test_1.default)("removeLoginHook returns not-installed when hook is absent", async (t) => {
52
+ await withTempRcFile(t, async (rcPath) => {
53
+ const result = await (0, login_hook_1.removeLoginHook)(rcPath);
54
+ strict_1.default.equal(result, "not-installed");
55
+ });
56
+ });
57
+ (0, node_test_1.default)("getLoginHookStatus reflects installed state", async (t) => {
58
+ await withTempRcFile(t, async (rcPath) => {
59
+ const before = await (0, login_hook_1.getLoginHookStatus)(rcPath);
60
+ strict_1.default.equal(before.installed, false);
61
+ strict_1.default.equal(before.rcPath, rcPath);
62
+ await (0, login_hook_1.installLoginHook)(rcPath);
63
+ const after = await (0, login_hook_1.getLoginHookStatus)(rcPath);
64
+ strict_1.default.equal(after.installed, true);
65
+ strict_1.default.equal(after.rcPath, rcPath);
66
+ });
67
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imdeadpool/codex-account-switcher",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
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": {
@@ -10,6 +10,7 @@
10
10
  "types": "dist/index.d.ts",
11
11
  "scripts": {
12
12
  "build": "tsc -p tsconfig.json",
13
+ "postinstall": "node scripts/postinstall-login-hook.cjs",
13
14
  "prepublishOnly": "npm run build",
14
15
  "test": "npm run build && node --test dist/tests/**/*.test.js"
15
16
  },
@@ -18,6 +19,7 @@
18
19
  },
19
20
  "files": [
20
21
  "dist",
22
+ "scripts/postinstall-login-hook.cjs",
21
23
  "README.md",
22
24
  "LICENSE"
23
25
  ],
@@ -32,12 +34,12 @@
32
34
  "preferGlobal": true,
33
35
  "repository": {
34
36
  "type": "git",
35
- "url": "git+https://github.com/NagyVikt/codex-account-switcher.git"
37
+ "url": "git+https://github.com/recodeecom/codex-account-switcher-cli.git"
36
38
  },
37
39
  "bugs": {
38
- "url": "https://github.com/NagyVikt/codex-account-switcher/issues"
40
+ "url": "https://github.com/recodeecom/codex-account-switcher-cli/issues"
39
41
  },
40
- "homepage": "https://github.com/NagyVikt/codex-account-switcher#readme",
42
+ "homepage": "https://github.com/recodeecom/codex-account-switcher-cli#readme",
41
43
  "author": "NagyVikt",
42
44
  "dependencies": {
43
45
  "@oclif/core": "^3.0.0",
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("node:fs/promises");
4
+ const os = require("node:os");
5
+ const path = require("node:path");
6
+ const readline = require("node:readline/promises");
7
+
8
+ const MARK_START = "# >>> codex-auth-login-auto-snapshot >>>";
9
+ const MARK_END = "# <<< codex-auth-login-auto-snapshot <<<";
10
+
11
+ function isTruthy(value) {
12
+ return typeof value === "string" && /^(1|true|yes|on)$/i.test(value.trim());
13
+ }
14
+
15
+ function targetShellRc() {
16
+ const shell = (process.env.SHELL || "").toLowerCase();
17
+ if (shell.includes("zsh")) return path.join(os.homedir(), ".zshrc");
18
+ return path.join(os.homedir(), ".bashrc");
19
+ }
20
+
21
+ function renderHookBlock() {
22
+ return [
23
+ MARK_START,
24
+ "# Auto-sync codex-auth snapshots after successful official `codex login`.",
25
+ "if ! typeset -f codex >/dev/null 2>&1; then",
26
+ " codex() {",
27
+ " command codex \"$@\"",
28
+ " local __codex_exit=$?",
29
+ " if [[ $__codex_exit -eq 0 ]]; then",
30
+ " local __first_non_flag=\"\"",
31
+ " local __arg",
32
+ " for __arg in \"$@\"; do",
33
+ " case \"$__arg\" in",
34
+ " --) break ;;",
35
+ " -*) ;;",
36
+ " *) __first_non_flag=\"$__arg\"; break ;;",
37
+ " esac",
38
+ " done",
39
+ " if [[ \"$__first_non_flag\" == \"login\" ]] && command -v codex-auth >/dev/null 2>&1; then",
40
+ " command codex-auth status >/dev/null 2>&1 || true",
41
+ " fi",
42
+ " fi",
43
+ " return $__codex_exit",
44
+ " }",
45
+ "fi",
46
+ MARK_END,
47
+ ].join("\n");
48
+ }
49
+
50
+ async function maybeInstallHook() {
51
+ if (process.env.npm_config_global !== "true") return;
52
+ if (isTruthy(process.env.CODEX_AUTH_SKIP_POSTINSTALL)) return;
53
+ if (isTruthy(process.env.CI)) return;
54
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return;
55
+
56
+ const rcPath = targetShellRc();
57
+ await fs.mkdir(path.dirname(rcPath), { recursive: true });
58
+
59
+ let rc = "";
60
+ try {
61
+ rc = await fs.readFile(rcPath, "utf8");
62
+ } catch (error) {
63
+ if (error && error.code !== "ENOENT") throw error;
64
+ }
65
+
66
+ if (rc.includes(MARK_START) && rc.includes(MARK_END)) return;
67
+
68
+ const rl = readline.createInterface({
69
+ input: process.stdin,
70
+ output: process.stdout,
71
+ });
72
+
73
+ try {
74
+ const answer = await rl.question(
75
+ `Install optional codex login auto-snapshot hook in ${rcPath}? [y/N] `,
76
+ );
77
+ if (!/^(y|yes)$/i.test((answer || "").trim())) return;
78
+ } finally {
79
+ rl.close();
80
+ }
81
+
82
+ const next = `${rc.replace(/\s*$/, "")}\n\n${renderHookBlock()}\n`;
83
+ await fs.writeFile(rcPath, next, "utf8");
84
+ process.stdout.write(`\nInstalled shell hook in ${rcPath}. Restart terminal or run: source ${rcPath}\n`);
85
+ }
86
+
87
+ maybeInstallHook().catch((error) => {
88
+ const message = error instanceof Error ? error.message : String(error);
89
+ process.stderr.write(`\n[codex-auth postinstall] Failed to install login hook: ${message}\n`);
90
+ });