@imdeadpool/codex-account-switcher 0.1.8 → 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
 
@@ -96,6 +102,7 @@ codex-auth remove-login-hook
96
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.
97
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.
98
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.
99
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.
100
107
  - `codex-auth status` – Prints auto-switch state, managed service status, active thresholds, and usage mode.
101
108
  - `codex-auth config auto ...` – Enables/disables managed auto-switch and updates threshold percentages.
@@ -131,3 +138,4 @@ Notes:
131
138
 
132
139
  - Works on macOS/Linux/Windows (regular-file auth snapshot activation).
133
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.
@@ -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,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,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,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
+ }
@@ -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.8",
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
  }