@imdeadpool/codex-account-switcher 0.1.7 → 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
@@ -82,6 +82,11 @@ codex-auth config api disable
82
82
  # daemon runtime (internal/service use)
83
83
  codex-auth daemon --once
84
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
85
90
  ```
86
91
 
87
92
  ### Command reference
@@ -96,6 +101,9 @@ codex-auth daemon --watch
96
101
  - `codex-auth config auto ...` – Enables/disables managed auto-switch and updates threshold percentages.
97
102
  - `codex-auth config api enable|disable` – Chooses usage source mode (`api` or `local`).
98
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.
99
107
 
100
108
  ### Auto-switch behavior
101
109
 
@@ -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.7",
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": {