@imdeadpool/codex-account-switcher 0.1.7 → 0.1.9

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
@@ -60,6 +60,12 @@ codex-auth list --details
60
60
  # show current account name
61
61
  codex-auth current
62
62
 
63
+ # check for a newer release and update globally
64
+ codex-auth self-update
65
+
66
+ # check only (no install)
67
+ codex-auth self-update --check
68
+
63
69
  # remove accounts (interactive multi-select)
64
70
  codex-auth remove
65
71
 
@@ -82,6 +88,11 @@ codex-auth config api disable
82
88
  # daemon runtime (internal/service use)
83
89
  codex-auth daemon --once
84
90
  codex-auth daemon --watch
91
+
92
+ # optional shell hook helpers
93
+ codex-auth setup-login-hook
94
+ codex-auth hook-status
95
+ codex-auth remove-login-hook
85
96
  ```
86
97
 
87
98
  ### Command reference
@@ -91,11 +102,15 @@ codex-auth daemon --watch
91
102
  - `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.
92
103
  - `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.
93
104
  - `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 a newer release. Without flags, it installs the latest version globally when one is available.
94
106
  - `codex-auth remove [query|--all]` – Removes snapshots interactively or by selector. If the active account is removed, the best remaining account is activated automatically.
95
107
  - `codex-auth status` – Prints auto-switch state, managed service status, active thresholds, and usage mode.
96
108
  - `codex-auth config auto ...` – Enables/disables managed auto-switch and updates threshold percentages.
97
109
  - `codex-auth config api enable|disable` – Chooses usage source mode (`api` or `local`).
98
110
  - `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 auto-sync snapshots after successful official `codex login`.
112
+ - `codex-auth hook-status [-f <path>]` – Shows whether the optional login auto-snapshot hook is installed for the selected rc file.
113
+ - `codex-auth remove-login-hook [-f <path>]` – Removes the optional shell hook.
99
114
 
100
115
  ### Auto-switch behavior
101
116
 
@@ -123,3 +138,4 @@ Notes:
123
138
 
124
139
  - Works on macOS/Linux/Windows (regular-file auth snapshot activation).
125
140
  - Requires Node 18+.
141
+ - Running bare `codex-auth` shows the help screen and also displays an update notice when a newer npm release is available.
@@ -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;
@@ -5,4 +5,5 @@ export default class ListCommand extends BaseCommand {
5
5
  readonly details: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
6
6
  };
7
7
  run(): Promise<void>;
8
+ private maybeOfferGlobalUpdate;
8
9
  }
@@ -1,13 +1,19 @@
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");
9
+ const update_check_1 = require("../lib/update-check");
5
10
  class ListCommand extends base_command_1.BaseCommand {
6
11
  async run() {
7
12
  await this.runSafe(async () => {
8
13
  var _a, _b, _c, _d, _e, _f;
9
14
  const { flags } = await this.parse(ListCommand);
10
15
  const detailed = Boolean(flags.details);
16
+ await this.maybeOfferGlobalUpdate();
11
17
  if (!detailed) {
12
18
  const accounts = await this.accounts.listAccountNames();
13
19
  const current = await this.accounts.getCurrentAccountName();
@@ -34,6 +40,33 @@ class ListCommand extends base_command_1.BaseCommand {
34
40
  }
35
41
  });
36
42
  }
43
+ async maybeOfferGlobalUpdate() {
44
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
45
+ return;
46
+ const currentVersion = this.config.version;
47
+ if (!currentVersion || typeof currentVersion !== "string")
48
+ return;
49
+ const latestVersion = await (0, update_check_1.fetchLatestNpmVersion)(update_check_1.PACKAGE_NAME);
50
+ if (!latestVersion || !(0, update_check_1.isVersionNewer)(currentVersion, latestVersion))
51
+ return;
52
+ this.log(`Update available for codex-auth: ${currentVersion} -> ${latestVersion}`);
53
+ const prompt = await (0, prompts_1.default)({
54
+ type: "confirm",
55
+ name: "install",
56
+ message: "Press Enter to update globally now",
57
+ initial: true,
58
+ });
59
+ if (!prompt.install) {
60
+ this.log(`Skipped update. Run manually: npm i -g ${update_check_1.PACKAGE_NAME}@latest`);
61
+ return;
62
+ }
63
+ const installExitCode = await (0, update_check_1.runGlobalNpmInstall)(update_check_1.PACKAGE_NAME);
64
+ if (installExitCode === 0) {
65
+ this.log("Global update completed.");
66
+ return;
67
+ }
68
+ this.warn(`Global update failed (exit code ${installExitCode}). Try: npm i -g ${update_check_1.PACKAGE_NAME}@latest`);
69
+ }
37
70
  }
38
71
  ListCommand.description = "List accounts managed under ~/.codex";
39
72
  ListCommand.flags = {
@@ -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,8 @@
1
+ import { BaseCommand } from "../lib/base-command";
2
+ export default class SelfUpdateCommand extends BaseCommand {
3
+ static description: string;
4
+ static flags: {
5
+ readonly check: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
6
+ };
7
+ run(): Promise<void>;
8
+ }
@@ -0,0 +1,40 @@
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 update_check_1 = require("../lib/update-check");
6
+ class SelfUpdateCommand extends base_command_1.BaseCommand {
7
+ async run() {
8
+ await this.runSafe(async () => {
9
+ const { flags } = await this.parse(SelfUpdateCommand);
10
+ const currentVersion = this.config.version;
11
+ const latestVersion = await (0, update_check_1.fetchLatestNpmVersion)(update_check_1.PACKAGE_NAME);
12
+ if (!latestVersion) {
13
+ this.warn("Could not check npm for the latest release right now.");
14
+ return;
15
+ }
16
+ if (!(0, update_check_1.isVersionNewer)(currentVersion, latestVersion)) {
17
+ this.log(`codex-auth is up to date (${currentVersion}).`);
18
+ return;
19
+ }
20
+ this.log(`Update available: ${currentVersion} -> ${latestVersion}`);
21
+ if (flags.check) {
22
+ return;
23
+ }
24
+ const exitCode = await (0, update_check_1.runGlobalNpmInstall)(update_check_1.PACKAGE_NAME);
25
+ if (exitCode === 0) {
26
+ this.log("Global update completed.");
27
+ return;
28
+ }
29
+ this.warn(`Global update failed (exit code ${exitCode}). Run: npm i -g ${update_check_1.PACKAGE_NAME}@latest`);
30
+ });
31
+ }
32
+ }
33
+ SelfUpdateCommand.description = "Check for updates and upgrade codex-auth globally";
34
+ SelfUpdateCommand.flags = {
35
+ check: core_1.Flags.boolean({
36
+ description: "Only check whether an update is available",
37
+ default: false,
38
+ }),
39
+ };
40
+ exports.default = SelfUpdateCommand;
@@ -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,3 @@
1
+ import type { Hook } from "@oclif/core";
2
+ declare const hook: Hook.Init;
3
+ export default hook;
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const update_check_1 = require("../../lib/update-check");
4
+ const hook = async function (options) {
5
+ if (options.id)
6
+ return;
7
+ if (options.argv.length > 0)
8
+ return;
9
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
10
+ return;
11
+ const currentVersion = options.config.version;
12
+ if (!currentVersion)
13
+ return;
14
+ const latestVersion = await (0, update_check_1.fetchLatestNpmVersion)(update_check_1.PACKAGE_NAME);
15
+ if (!latestVersion || !(0, update_check_1.isVersionNewer)(currentVersion, latestVersion))
16
+ return;
17
+ this.log(`Update available for codex-auth: ${currentVersion} -> ${latestVersion}`);
18
+ this.log("Run `codex-auth self-update` to install the latest version.");
19
+ };
20
+ exports.default = hook;
@@ -61,6 +61,7 @@ export declare class AccountService {
61
61
  private writeCurrentName;
62
62
  private readCurrentNameFile;
63
63
  private pathExists;
64
+ private filesMatch;
64
65
  private hydrateSnapshotMetadata;
65
66
  private resolveUniqueInferredName;
66
67
  private loadReconciledRegistry;
@@ -31,16 +31,26 @@ class AccountService {
31
31
  autoSwitchDisabled: false,
32
32
  };
33
33
  }
34
+ const resolvedName = await this.resolveLoginAccountNameFromCurrentAuth();
34
35
  const activeName = await this.getCurrentAccountName();
35
36
  if (activeName) {
36
37
  const activeSnapshotPath = this.accountFilePath(activeName);
37
38
  if (await this.pathExists(activeSnapshotPath)) {
38
39
  const activeSnapshot = await (0, auth_parser_1.parseAuthSnapshotFile)(activeSnapshotPath);
39
40
  if (this.snapshotsShareIdentity(activeSnapshot, incomingSnapshot)) {
40
- return {
41
- synchronized: false,
42
- autoSwitchDisabled: false,
43
- };
41
+ if (activeName === resolvedName.name) {
42
+ return {
43
+ synchronized: false,
44
+ autoSwitchDisabled: false,
45
+ };
46
+ }
47
+ const authMatchesActiveSnapshot = await this.filesMatch(authPath, activeSnapshotPath);
48
+ if (authMatchesActiveSnapshot) {
49
+ return {
50
+ synchronized: false,
51
+ autoSwitchDisabled: false,
52
+ };
53
+ }
44
54
  }
45
55
  }
46
56
  }
@@ -49,7 +59,6 @@ class AccountService {
49
59
  if (autoSwitchDisabled) {
50
60
  await this.setAutoSwitchEnabled(false);
51
61
  }
52
- const resolvedName = await this.resolveLoginAccountNameFromCurrentAuth();
53
62
  const savedName = await this.saveAccount(resolvedName.name);
54
63
  return {
55
64
  synchronized: true,
@@ -550,6 +559,15 @@ class AccountService {
550
559
  return false;
551
560
  }
552
561
  }
562
+ async filesMatch(firstPath, secondPath) {
563
+ try {
564
+ const [first, second] = await Promise.all([promises_1.default.readFile(firstPath), promises_1.default.readFile(secondPath)]);
565
+ return first.equals(second);
566
+ }
567
+ catch {
568
+ return false;
569
+ }
570
+ }
553
571
  async hydrateSnapshotMetadata(registry, accountName) {
554
572
  var _a;
555
573
  const parsed = await (0, auth_parser_1.parseAuthSnapshotFile)(this.accountFilePath(accountName));
@@ -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,5 @@
1
+ export declare const PACKAGE_NAME = "@imdeadpool/codex-account-switcher";
2
+ export declare function parseVersionTriplet(version: string): [number, number, number] | null;
3
+ export declare function isVersionNewer(currentVersion: string, latestVersion: string): boolean;
4
+ export declare function fetchLatestNpmVersion(packageName: string, timeoutMs?: number): Promise<string | null>;
5
+ export declare function runGlobalNpmInstall(packageName: string, version?: "latest" | string): Promise<number>;
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PACKAGE_NAME = void 0;
4
+ exports.parseVersionTriplet = parseVersionTriplet;
5
+ exports.isVersionNewer = isVersionNewer;
6
+ exports.fetchLatestNpmVersion = fetchLatestNpmVersion;
7
+ exports.runGlobalNpmInstall = runGlobalNpmInstall;
8
+ const node_child_process_1 = require("node:child_process");
9
+ const SEMVER_TRIPLET = /^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/;
10
+ exports.PACKAGE_NAME = "@imdeadpool/codex-account-switcher";
11
+ function parseVersionTriplet(version) {
12
+ const match = version.trim().match(SEMVER_TRIPLET);
13
+ if (!match)
14
+ return null;
15
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
16
+ }
17
+ function isVersionNewer(currentVersion, latestVersion) {
18
+ const current = parseVersionTriplet(currentVersion);
19
+ const latest = parseVersionTriplet(latestVersion);
20
+ if (!current || !latest)
21
+ return false;
22
+ for (let i = 0; i < 3; i += 1) {
23
+ if (latest[i] > current[i])
24
+ return true;
25
+ if (latest[i] < current[i])
26
+ return false;
27
+ }
28
+ return false;
29
+ }
30
+ async function fetchLatestNpmVersion(packageName, timeoutMs = 2500) {
31
+ return new Promise((resolve) => {
32
+ const child = (0, node_child_process_1.spawn)("npm", ["view", packageName, "version", "--json"], {
33
+ stdio: ["ignore", "pipe", "ignore"],
34
+ });
35
+ let output = "";
36
+ const timeout = setTimeout(() => {
37
+ child.kill("SIGTERM");
38
+ resolve(null);
39
+ }, timeoutMs);
40
+ child.stdout.on("data", (chunk) => {
41
+ output += chunk.toString();
42
+ });
43
+ child.on("error", () => {
44
+ clearTimeout(timeout);
45
+ resolve(null);
46
+ });
47
+ child.on("exit", (code) => {
48
+ clearTimeout(timeout);
49
+ if (code !== 0) {
50
+ resolve(null);
51
+ return;
52
+ }
53
+ const trimmed = output.trim();
54
+ if (!trimmed) {
55
+ resolve(null);
56
+ return;
57
+ }
58
+ try {
59
+ const parsed = JSON.parse(trimmed);
60
+ if (typeof parsed === "string" && parsed.trim().length > 0) {
61
+ resolve(parsed.trim());
62
+ return;
63
+ }
64
+ }
65
+ catch {
66
+ // fall through
67
+ }
68
+ resolve(trimmed.replace(/^"+|"+$/g, ""));
69
+ });
70
+ });
71
+ }
72
+ async function runGlobalNpmInstall(packageName, version = "latest") {
73
+ return new Promise((resolve) => {
74
+ const child = (0, node_child_process_1.spawn)("npm", ["i", "-g", `${packageName}@${version}`], {
75
+ stdio: "inherit",
76
+ });
77
+ child.on("error", () => {
78
+ resolve(1);
79
+ });
80
+ child.on("exit", (code) => {
81
+ resolve(typeof code === "number" ? code : 1);
82
+ });
83
+ });
84
+ }
@@ -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
+ });
@@ -19,9 +19,10 @@ function encodeBase64Url(input) {
19
19
  .replace(/=+$/g, "");
20
20
  }
21
21
  function buildAuthPayload(email, options) {
22
- var _a, _b;
22
+ var _a, _b, _c;
23
23
  const accountId = (_a = options === null || options === void 0 ? void 0 : options.accountId) !== null && _a !== void 0 ? _a : "acct-1";
24
24
  const userId = (_b = options === null || options === void 0 ? void 0 : options.userId) !== null && _b !== void 0 ? _b : "user-1";
25
+ const tokenSeed = (_c = options === null || options === void 0 ? void 0 : options.tokenSeed) !== null && _c !== void 0 ? _c : email;
25
26
  const idTokenPayload = {
26
27
  email,
27
28
  "https://api.openai.com/auth": {
@@ -33,8 +34,8 @@ function buildAuthPayload(email, options) {
33
34
  const idToken = `${encodeBase64Url(JSON.stringify({ alg: "none" }))}.${encodeBase64Url(JSON.stringify(idTokenPayload))}.sig`;
34
35
  return JSON.stringify({
35
36
  tokens: {
36
- access_token: `token-${email}`,
37
- refresh_token: `refresh-${email}`,
37
+ access_token: `token-${tokenSeed}`,
38
+ refresh_token: `refresh-${tokenSeed}`,
38
39
  id_token: idToken,
39
40
  account_id: accountId,
40
41
  },
@@ -362,6 +363,34 @@ async function withIsolatedCodexDir(t, fn) {
362
363
  strict_1.default.equal(registry.autoSwitch.enabled, false);
363
364
  });
364
365
  });
366
+ (0, node_test_1.default)("syncExternalAuthSnapshotIfNeeded re-keys active alias to inferred email name when external login identity matches", async (t) => {
367
+ await withIsolatedCodexDir(t, async ({ codexDir, accountsDir, authPath }) => {
368
+ const service = new account_service_1.AccountService();
369
+ const activeAlias = "team-primary";
370
+ const incomingEmail = "admin@kozpontihusbolt.hu";
371
+ const currentPath = node_path_1.default.join(codexDir, "current");
372
+ await promises_1.default.writeFile(node_path_1.default.join(accountsDir, `${activeAlias}.json`), buildAuthPayload(incomingEmail, {
373
+ accountId: "acct-team",
374
+ userId: "user-team",
375
+ tokenSeed: "pre-login",
376
+ }), "utf8");
377
+ await promises_1.default.writeFile(currentPath, `${activeAlias}\n`, "utf8");
378
+ await promises_1.default.writeFile(authPath, buildAuthPayload(incomingEmail, {
379
+ accountId: "acct-team",
380
+ userId: "user-team",
381
+ tokenSeed: "post-login",
382
+ }), "utf8");
383
+ const result = await service.syncExternalAuthSnapshotIfNeeded();
384
+ strict_1.default.deepEqual(result, {
385
+ synchronized: true,
386
+ savedName: incomingEmail,
387
+ autoSwitchDisabled: false,
388
+ });
389
+ strict_1.default.equal((await promises_1.default.readFile(currentPath, "utf8")).trim(), incomingEmail);
390
+ const inferredSnapshot = await (0, auth_parser_1.parseAuthSnapshotFile)(node_path_1.default.join(accountsDir, `${incomingEmail}.json`));
391
+ strict_1.default.equal(inferredSnapshot.email, incomingEmail);
392
+ });
393
+ });
365
394
  (0, node_test_1.default)("syncExternalAuthSnapshotIfNeeded materializes auth symlink so external codex login can no longer overwrite snapshot files", async (t) => {
366
395
  if (process.platform === "win32") {
367
396
  t.skip("symlink conversion behavior is Unix-specific in this test");
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,29 @@
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 update_check_1 = require("../lib/update-check");
9
+ (0, node_test_1.default)("parseVersionTriplet parses standard semver triplets", () => {
10
+ strict_1.default.deepEqual((0, update_check_1.parseVersionTriplet)("0.1.9"), [0, 1, 9]);
11
+ strict_1.default.deepEqual((0, update_check_1.parseVersionTriplet)("v1.20.3"), [1, 20, 3]);
12
+ });
13
+ (0, node_test_1.default)("parseVersionTriplet supports pre-release/build suffixes", () => {
14
+ strict_1.default.deepEqual((0, update_check_1.parseVersionTriplet)("1.2.3-beta.1"), [1, 2, 3]);
15
+ strict_1.default.deepEqual((0, update_check_1.parseVersionTriplet)("1.2.3+build"), [1, 2, 3]);
16
+ });
17
+ (0, node_test_1.default)("parseVersionTriplet rejects non-triplet versions", () => {
18
+ strict_1.default.equal((0, update_check_1.parseVersionTriplet)("1.2"), null);
19
+ strict_1.default.equal((0, update_check_1.parseVersionTriplet)("latest"), null);
20
+ });
21
+ (0, node_test_1.default)("isVersionNewer compares semver triplets correctly", () => {
22
+ strict_1.default.equal((0, update_check_1.isVersionNewer)("0.1.8", "0.1.9"), true);
23
+ strict_1.default.equal((0, update_check_1.isVersionNewer)("0.1.9", "0.1.9"), false);
24
+ strict_1.default.equal((0, update_check_1.isVersionNewer)("0.2.0", "0.1.9"), false);
25
+ });
26
+ (0, node_test_1.default)("isVersionNewer returns false when either version is invalid", () => {
27
+ strict_1.default.equal((0, update_check_1.isVersionNewer)("latest", "0.1.9"), false);
28
+ strict_1.default.equal((0, update_check_1.isVersionNewer)("0.1.8", "nightly"), false);
29
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imdeadpool/codex-account-switcher",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
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": {
@@ -52,6 +52,9 @@
52
52
  },
53
53
  "oclif": {
54
54
  "bin": "codex-auth",
55
- "commands": "./dist/commands"
55
+ "commands": "./dist/commands",
56
+ "hooks": {
57
+ "init": "./dist/hooks/init/update-notifier"
58
+ }
56
59
  }
57
60
  }