@imdeadpool/codex-account-switcher 0.1.10 → 0.1.13

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
@@ -25,6 +25,9 @@ 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
+
28
31
  - Choose `y` to enable fully automatic login snapshot capture.
29
32
  - Choose `n` (default) to skip.
30
33
  - Set `CODEX_AUTH_SKIP_POSTINSTALL=1` to always suppress this prompt.
@@ -123,7 +126,7 @@ codex-auth remove-login-hook
123
126
  - `codex-auth config auto ...` – Enables/disables managed auto-switch and updates threshold percentages.
124
127
  - `codex-auth config api enable|disable` – Chooses usage source mode (`api` or `local`).
125
128
  - `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, auto-sync snapshots after successful official `codex login`, and restore common terminal modes before returning to your prompt.
129
+ - `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
130
  - `codex-auth hook-status [-f <path>]` – Shows whether the optional login auto-snapshot hook is installed for the selected rc file.
128
131
  - `codex-auth remove-login-hook [-f <path>]` – Removes the optional shell hook.
129
132
 
@@ -154,3 +157,4 @@ Notes:
154
157
  - Works on macOS/Linux/Windows (regular-file auth snapshot activation).
155
158
  - Requires Node 18+.
156
159
  - Running bare `codex-auth` shows the help screen and also displays an update notice when a newer npm release is available.
160
+ - 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 auto-syncs snapshots after `codex login`";
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 auto-syncs snapshots after successful `codex login`";
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
- this.log("Run `codex-auth self-update` to install the latest version.");
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;
@@ -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();
@@ -33,7 +37,7 @@ function resolveDefaultShellRcPath() {
33
37
  function renderLoginHookBlock() {
34
38
  return [
35
39
  exports.LOGIN_HOOK_MARK_START,
36
- "# Auto-sync codex-auth snapshots after successful official `codex login`.",
40
+ "# Keep terminal-scoped snapshot memory in sync before/after each `codex` run.",
37
41
  "# Also restore common terminal modes to avoid leaked escape sequences after codex exits.",
38
42
  "__codex_auth_restore_tty() {",
39
43
  " [[ -t 1 ]] || return 0",
@@ -48,19 +52,8 @@ function renderLoginHookBlock() {
48
52
  " fi",
49
53
  " command codex \"$@\"",
50
54
  " local __codex_exit=$?",
51
- " if [[ $__codex_exit -eq 0 ]]; then",
52
- " local __first_non_flag=\"\"",
53
- " local __arg",
54
- " for __arg in \"$@\"; do",
55
- " case \"$__arg\" in",
56
- " --) break ;;",
57
- " -*) ;;",
58
- " *) __first_non_flag=\"$__arg\"; break ;;",
59
- " esac",
60
- " done",
61
- " if [[ \"$__first_non_flag\" == \"login\" ]] && command -v codex-auth >/dev/null 2>&1; then",
62
- " CODEX_AUTH_FORCE_EXTERNAL_SYNC=1 command codex-auth status >/dev/null 2>&1 || true",
63
- " fi",
55
+ " if command -v codex-auth >/dev/null 2>&1; then",
56
+ " CODEX_AUTH_FORCE_EXTERNAL_SYNC=1 command codex-auth status >/dev/null 2>&1 || true",
64
57
  " fi",
65
58
  " if [[ -z \"${CODEX_AUTH_SKIP_TTY_RESTORE:-}\" ]]; then",
66
59
  " __codex_auth_restore_tty",
@@ -83,9 +76,14 @@ async function installLoginHook(rcPath = resolveDefaultShellRcPath()) {
83
76
  throw error;
84
77
  }
85
78
  if (existing.includes(exports.LOGIN_HOOK_MARK_START) && existing.includes(exports.LOGIN_HOOK_MARK_END)) {
86
- return "already-installed";
79
+ const refreshed = normalizeRcContents(existing.replace(hookBlockRegex(), `\n${renderLoginHookBlock()}\n`));
80
+ if (refreshed === normalizeRcContents(existing)) {
81
+ return "already-installed";
82
+ }
83
+ await promises_1.default.writeFile(rcPath, refreshed, "utf8");
84
+ return "updated";
87
85
  }
88
- const next = `${existing.replace(/\s*$/, "")}\n\n${renderLoginHookBlock()}\n`;
86
+ const next = normalizeRcContents(`${existing}\n\n${renderLoginHookBlock()}\n`);
89
87
  await promises_1.default.writeFile(rcPath, next, "utf8");
90
88
  return "installed";
91
89
  }
@@ -106,7 +104,7 @@ async function removeLoginHook(rcPath = resolveDefaultShellRcPath()) {
106
104
  if (stripped === existing) {
107
105
  return "not-installed";
108
106
  }
109
- await promises_1.default.writeFile(rcPath, stripped.replace(/\n{3,}/g, "\n\n"), "utf8");
107
+ await promises_1.default.writeFile(rcPath, normalizeRcContents(stripped), "utf8");
110
108
  return "removed";
111
109
  }
112
110
  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>;
@@ -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);
@@ -70,6 +88,7 @@ async function withTempRcFile(t, fn) {
70
88
  strict_1.default.ok(hook.includes("__codex_auth_restore_tty"));
71
89
  strict_1.default.ok(hook.includes("command codex-auth restore-session"));
72
90
  strict_1.default.ok(hook.includes("CODEX_AUTH_FORCE_EXTERNAL_SYNC=1 command codex-auth status"));
91
+ strict_1.default.ok(!hook.includes("__first_non_flag"));
73
92
  strict_1.default.ok(hook.includes("\\033[>4m"));
74
93
  strict_1.default.ok(hook.includes("\\033[<u"));
75
94
  strict_1.default.ok(hook.includes("\\033[?2026l"));
@@ -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.10",
3
+ "version": "0.1.13",
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": {
@@ -21,7 +21,7 @@ function targetShellRc() {
21
21
  function renderHookBlock() {
22
22
  return [
23
23
  MARK_START,
24
- "# Auto-sync codex-auth snapshots after successful official `codex login`.",
24
+ "# Keep terminal-scoped snapshot memory in sync before/after each `codex` run.",
25
25
  "# Also restore common terminal modes to avoid leaked escape sequences after codex exits.",
26
26
  "__codex_auth_restore_tty() {",
27
27
  " [[ -t 1 ]] || return 0",
@@ -31,21 +31,13 @@ function renderHookBlock() {
31
31
  "}",
32
32
  "if ! typeset -f codex >/dev/null 2>&1; then",
33
33
  " codex() {",
34
+ " if command -v codex-auth >/dev/null 2>&1; then",
35
+ " command codex-auth restore-session >/dev/null 2>&1 || true",
36
+ " fi",
34
37
  " command codex \"$@\"",
35
38
  " local __codex_exit=$?",
36
- " if [[ $__codex_exit -eq 0 ]]; then",
37
- " local __first_non_flag=\"\"",
38
- " local __arg",
39
- " for __arg in \"$@\"; do",
40
- " case \"$__arg\" in",
41
- " --) break ;;",
42
- " -*) ;;",
43
- " *) __first_non_flag=\"$__arg\"; break ;;",
44
- " esac",
45
- " done",
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",
39
+ " if command -v codex-auth >/dev/null 2>&1; then",
40
+ " CODEX_AUTH_FORCE_EXTERNAL_SYNC=1 command codex-auth status >/dev/null 2>&1 || true",
49
41
  " fi",
50
42
  " if [[ -z \"${CODEX_AUTH_SKIP_TTY_RESTORE:-}\" ]]; then",
51
43
  " __codex_auth_restore_tty",
@@ -57,11 +49,26 @@ function renderHookBlock() {
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
+
60
67
  async function maybeInstallHook() {
61
68
  if (process.env.npm_config_global !== "true") return;
62
69
  if (isTruthy(process.env.CODEX_AUTH_SKIP_POSTINSTALL)) return;
63
70
  if (isTruthy(process.env.CI)) return;
64
- if (!process.stdin.isTTY || !process.stdout.isTTY) return;
71
+ const canPrompt = process.stdin.isTTY && process.stdout.isTTY;
65
72
 
66
73
  const rcPath = targetShellRc();
67
74
  await fs.mkdir(path.dirname(rcPath), { recursive: true });
@@ -73,7 +80,16 @@ async function maybeInstallHook() {
73
80
  if (error && error.code !== "ENOENT") throw error;
74
81
  }
75
82
 
76
- if (rc.includes(MARK_START) && rc.includes(MARK_END)) return;
83
+ if (rc.includes(MARK_START) && rc.includes(MARK_END)) {
84
+ const refreshed = normalizeRcContents(rc.replace(hookBlockRegex(), `\n${renderHookBlock()}\n`));
85
+ if (refreshed !== normalizeRcContents(rc)) {
86
+ await fs.writeFile(rcPath, refreshed, "utf8");
87
+ process.stdout.write(`\nUpdated shell hook in ${rcPath}. Restart terminal or run: source ${rcPath}\n`);
88
+ }
89
+ return;
90
+ }
91
+
92
+ if (!canPrompt) return;
77
93
 
78
94
  const rl = readline.createInterface({
79
95
  input: process.stdin,
@@ -89,7 +105,7 @@ async function maybeInstallHook() {
89
105
  rl.close();
90
106
  }
91
107
 
92
- const next = `${rc.replace(/\s*$/, "")}\n\n${renderHookBlock()}\n`;
108
+ const next = normalizeRcContents(`${rc}\n\n${renderHookBlock()}\n`);
93
109
  await fs.writeFile(rcPath, next, "utf8");
94
110
  process.stdout.write(`\nInstalled shell hook in ${rcPath}. Restart terminal or run: source ${rcPath}\n`);
95
111
  }