@minniexcode/codex-switch 0.0.10 → 0.0.12

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.
Files changed (51) hide show
  1. package/README.AI.md +68 -73
  2. package/README.CN.md +108 -111
  3. package/README.md +87 -80
  4. package/dist/app/add-provider.js +29 -15
  5. package/dist/app/bridge.js +15 -14
  6. package/dist/app/edit-provider.js +1 -1
  7. package/dist/app/get-status.js +21 -9
  8. package/dist/app/import-providers.js +1 -1
  9. package/dist/app/init-codex.js +13 -14
  10. package/dist/app/list-providers.js +48 -1
  11. package/dist/app/remove-provider.js +1 -1
  12. package/dist/app/run-doctor.js +12 -5
  13. package/dist/app/run-mutation.js +3 -2
  14. package/dist/app/setup-codex.js +3 -1
  15. package/dist/app/switch-provider.js +11 -13
  16. package/dist/cli/output.js +145 -18
  17. package/dist/cli.js +34 -2
  18. package/dist/commands/args.js +2 -2
  19. package/dist/commands/dispatch.js +40 -0
  20. package/dist/commands/handlers.js +130 -161
  21. package/dist/commands/help.js +11 -5
  22. package/dist/commands/registry.js +42 -20
  23. package/dist/domain/backups.js +4 -4
  24. package/dist/domain/config.js +110 -5
  25. package/dist/domain/providers.js +12 -0
  26. package/dist/domain/runtime-state.js +111 -13
  27. package/dist/infra/config-repo.js +16 -206
  28. package/dist/interaction/interactive.js +16 -6
  29. package/dist/runtime/copilot-adapter.js +12 -12
  30. package/dist/runtime/copilot-bridge.js +394 -45
  31. package/dist/runtime/copilot-cli.js +84 -12
  32. package/dist/runtime/copilot-installer.js +10 -9
  33. package/dist/runtime/copilot-sdk-loader.js +5 -5
  34. package/dist/storage/backup-repo.js +4 -4
  35. package/dist/storage/codex-paths.js +34 -8
  36. package/dist/storage/config-repo.js +0 -23
  37. package/dist/storage/lock-repo.js +2 -4
  38. package/dist/storage/runtime-state-repo.js +14 -13
  39. package/dist/storage/tool-config-repo.js +111 -0
  40. package/docs/Design/codex-switch-v0.0.11-design.md +824 -0
  41. package/docs/Design/codex-switch-v0.0.12-design.md +343 -0
  42. package/docs/PRD/codex-switch-prd-v0.0.11.md +577 -0
  43. package/docs/PRD/codex-switch-prd-v0.0.12.md +279 -0
  44. package/docs/PRD/codex-switch-prd-v0.1.0.md +125 -237
  45. package/docs/Tests/testing.md +39 -112
  46. package/docs/cli-usage.md +135 -565
  47. package/docs/codex-switch-command-design.md +3 -0
  48. package/docs/codex-switch-product-overview.md +52 -207
  49. package/docs/codex-switch-technical-architecture.md +3 -0
  50. package/package.json +1 -1
  51. package/dist/app/rollback-latest.js +0 -26
@@ -1,10 +1,46 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.setCopilotCliSpawnImplementation = setCopilotCliSpawnImplementation;
4
37
  exports.resetCopilotCliSpawnImplementation = resetCopilotCliSpawnImplementation;
5
38
  exports.checkCopilotCliAvailable = checkCopilotCliAvailable;
6
39
  exports.runCopilotLogin = runCopilotLogin;
40
+ const fs = __importStar(require("node:fs"));
41
+ const path = __importStar(require("node:path"));
7
42
  const node_child_process_1 = require("node:child_process");
43
+ const copilot_installer_1 = require("./copilot-installer");
8
44
  let spawnImplementation = node_child_process_1.spawnSync;
9
45
  /**
10
46
  * Overrides the spawn implementation for Copilot CLI tests.
@@ -19,22 +55,28 @@ function resetCopilotCliSpawnImplementation() {
19
55
  spawnImplementation = node_child_process_1.spawnSync;
20
56
  }
21
57
  /**
22
- * Checks whether the GitHub Copilot CLI is available on PATH.
58
+ * Checks whether the GitHub Copilot CLI is available either from the bundled runtime or on PATH.
23
59
  */
24
- function checkCopilotCliAvailable() {
25
- const invocation = getCopilotInvocation(["--help"]);
60
+ function checkCopilotCliAvailable(runtimesDir) {
61
+ const invocation = getCopilotInvocation(["--help"], runtimesDir);
26
62
  const result = spawnImplementation(invocation.command, invocation.args, {
27
63
  stdio: "pipe",
28
64
  encoding: "utf8",
29
- shell: false,
65
+ shell: invocation.shell,
30
66
  });
31
67
  if (result.error || result.status !== 0) {
32
68
  return {
33
69
  ok: false,
34
70
  cause: result.error?.message ?? (result.stderr.trim() || "Unknown failure"),
71
+ source: invocation.source,
72
+ command: formatInvocation(invocation),
35
73
  };
36
74
  }
37
- return { ok: true };
75
+ return {
76
+ ok: true,
77
+ source: invocation.source,
78
+ command: formatInvocation(invocation),
79
+ };
38
80
  }
39
81
  /**
40
82
  * Launches the official `copilot login` flow in the current terminal.
@@ -44,27 +86,57 @@ function runCopilotLogin(options) {
44
86
  if (options?.host) {
45
87
  args.push("--hostname", options.host);
46
88
  }
47
- const invocation = getCopilotInvocation(args);
89
+ const invocation = getCopilotInvocation(args, options?.runtimesDir);
48
90
  const result = spawnImplementation(invocation.command, invocation.args, {
49
91
  stdio: "inherit",
50
- shell: false,
92
+ shell: invocation.shell,
51
93
  });
52
94
  if (result.error || result.status !== 0) {
53
- throw new Error(result.error?.message ?? `copilot login exited with status ${String(result.status)}`);
95
+ throw new Error(result.error?.message ??
96
+ `${formatInvocation(invocation)} exited with status ${String(result.status)}`);
54
97
  }
55
98
  }
56
99
  /**
57
100
  * Resolves a cross-platform invocation for the Copilot CLI.
58
101
  */
59
- function getCopilotInvocation(args) {
102
+ function getCopilotInvocation(args, runtimesDir) {
103
+ const bundledCommand = resolveBundledCopilotCommand(runtimesDir);
104
+ const executable = bundledCommand ?? "copilot";
60
105
  if (process.platform === "win32") {
61
106
  return {
62
- command: process.env.ComSpec || "cmd.exe",
63
- args: ["/d", "/s", "/c", ["copilot", ...args].join(" ")],
107
+ command: executable,
108
+ args,
109
+ source: bundledCommand ? "bundled" : "path",
110
+ shell: true,
64
111
  };
65
112
  }
66
113
  return {
67
- command: "copilot",
114
+ command: executable,
68
115
  args,
116
+ source: bundledCommand ? "bundled" : "path",
117
+ shell: false,
69
118
  };
70
119
  }
120
+ /**
121
+ * Resolves the bundled Copilot CLI shim installed alongside the optional runtime.
122
+ */
123
+ function resolveBundledCopilotCommand(runtimesDir) {
124
+ const installDir = (0, copilot_installer_1.getCopilotRuntimeInstallDir)(runtimesDir);
125
+ const candidates = process.platform === "win32"
126
+ ? [path.join(installDir, "node_modules", ".bin", "copilot.cmd")]
127
+ : [path.join(installDir, "node_modules", ".bin", "copilot")];
128
+ for (const candidate of candidates) {
129
+ if (fs.existsSync(candidate)) {
130
+ return candidate;
131
+ }
132
+ }
133
+ return null;
134
+ }
135
+ /**
136
+ * Renders the invocation into a short human-readable string for diagnostics.
137
+ */
138
+ function formatInvocation(invocation) {
139
+ return invocation.command === "copilot"
140
+ ? ["copilot", ...invocation.args].join(" ")
141
+ : [invocation.command, ...invocation.args].join(" ");
142
+ }
@@ -40,10 +40,10 @@ exports.getCopilotSdkPackageName = getCopilotSdkPackageName;
40
40
  exports.probeCopilotSdkInstall = probeCopilotSdkInstall;
41
41
  exports.installCopilotSdk = installCopilotSdk;
42
42
  const fs = __importStar(require("node:fs"));
43
- const os = __importStar(require("node:os"));
44
43
  const path = __importStar(require("node:path"));
45
44
  const node_child_process_1 = require("node:child_process");
46
45
  const errors_1 = require("../domain/errors");
46
+ const codex_paths_1 = require("../storage/codex-paths");
47
47
  const COPILOT_SDK_PACKAGE = "@github/copilot-sdk";
48
48
  const COPILOT_SDK_VERSION = "latest";
49
49
  let spawnImplementation = node_child_process_1.spawnSync;
@@ -60,14 +60,15 @@ function resetCopilotInstallerSpawnImplementation() {
60
60
  spawnImplementation = node_child_process_1.spawnSync;
61
61
  }
62
62
  /**
63
- * Returns the user-level runtime directory used to lazily install the Copilot SDK.
63
+ * Returns the tool-home runtime directory used to lazily install the Copilot SDK.
64
64
  */
65
- function getCopilotRuntimeInstallDir() {
65
+ function getCopilotRuntimeInstallDir(runtimesDir) {
66
66
  const override = process.env.CODEX_SWITCH_COPILOT_RUNTIME_DIR;
67
67
  if (override && override.trim() !== "") {
68
68
  return path.resolve(override);
69
69
  }
70
- return path.join(os.homedir(), ".codex-switch", "runtimes", "copilot");
70
+ const baseRuntimesDir = runtimesDir ? path.resolve(runtimesDir) : path.join((0, codex_paths_1.resolveCodexSwitchHome)(), "runtimes");
71
+ return path.join(baseRuntimesDir, "copilot");
71
72
  }
72
73
  /**
73
74
  * Returns the package name used by the Copilot runtime installer.
@@ -78,8 +79,8 @@ function getCopilotSdkPackageName() {
78
79
  /**
79
80
  * Reports whether the optional Copilot SDK runtime is currently installed.
80
81
  */
81
- function probeCopilotSdkInstall() {
82
- const installDir = getCopilotRuntimeInstallDir();
82
+ function probeCopilotSdkInstall(runtimesDir) {
83
+ const installDir = getCopilotRuntimeInstallDir(runtimesDir);
83
84
  const packageJsonPath = path.join(installDir, "node_modules", "@github", "copilot-sdk", "package.json");
84
85
  if (!fs.existsSync(packageJsonPath)) {
85
86
  return {
@@ -100,8 +101,8 @@ function probeCopilotSdkInstall() {
100
101
  /**
101
102
  * Installs the optional Copilot SDK into the user-level runtime directory.
102
103
  */
103
- function installCopilotSdk() {
104
- const installDir = getCopilotRuntimeInstallDir();
104
+ function installCopilotSdk(runtimesDir) {
105
+ const installDir = getCopilotRuntimeInstallDir(runtimesDir);
105
106
  fs.mkdirSync(installDir, { recursive: true });
106
107
  const packageJsonPath = path.join(installDir, "package.json");
107
108
  if (!fs.existsSync(packageJsonPath)) {
@@ -133,7 +134,7 @@ function installCopilotSdk() {
133
134
  args: installCommand.args,
134
135
  });
135
136
  }
136
- return probeCopilotSdkInstall();
137
+ return probeCopilotSdkInstall(runtimesDir);
137
138
  }
138
139
  /**
139
140
  * Resolves a stable npm install invocation for the optional Copilot SDK runtime.
@@ -41,19 +41,19 @@ const copilot_installer_1 = require("./copilot-installer");
41
41
  /**
42
42
  * Dynamically resolves the lazily installed Copilot SDK entrypoint.
43
43
  */
44
- function getCopilotSdkEntrypoint() {
45
- return path.join((0, copilot_installer_1.getCopilotRuntimeInstallDir)(), "node_modules", "@github", "copilot-sdk");
44
+ function getCopilotSdkEntrypoint(runtimesDir) {
45
+ return path.join((0, copilot_installer_1.getCopilotRuntimeInstallDir)(runtimesDir), "node_modules", "@github", "copilot-sdk");
46
46
  }
47
47
  /**
48
48
  * Loads the Copilot SDK only when a Copilot runtime path is exercised.
49
49
  */
50
- async function loadCopilotSdk() {
51
- const status = (0, copilot_installer_1.probeCopilotSdkInstall)();
50
+ async function loadCopilotSdk(runtimesDir) {
51
+ const status = (0, copilot_installer_1.probeCopilotSdkInstall)(runtimesDir);
52
52
  if (!status.installed) {
53
53
  throw (0, errors_1.cliError)("COPILOT_SDK_MISSING", "The optional Copilot SDK runtime is not installed.", {
54
54
  installDir: status.installDir,
55
55
  packageName: status.packageName,
56
56
  });
57
57
  }
58
- return Promise.resolve(`${getCopilotSdkEntrypoint()}`).then(s => __importStar(require(s)));
58
+ return Promise.resolve(`${getCopilotSdkEntrypoint(runtimesDir)}`).then(s => __importStar(require(s)));
59
59
  }
@@ -47,7 +47,7 @@ const fs_utils_1 = require("./fs-utils");
47
47
  /**
48
48
  * Creates a point-in-time backup for the managed files involved in a mutation.
49
49
  */
50
- function createBackup(codexDir, backupsDir, reason, files) {
50
+ function createBackup(backupsDir, reason, files) {
51
51
  try {
52
52
  const backupDir = path.join(backupsDir, `${createTimestamp()}-${reason}`);
53
53
  (0, fs_utils_1.ensureDir)(backupsDir);
@@ -62,6 +62,7 @@ function createBackup(codexDir, backupsDir, reason, files) {
62
62
  }
63
63
  entries.push({
64
64
  relativePath: file.relativePath,
65
+ restorePath: file.absolutePath,
65
66
  existed: exists,
66
67
  backupFileName,
67
68
  });
@@ -70,7 +71,6 @@ function createBackup(codexDir, backupsDir, reason, files) {
70
71
  version: 1,
71
72
  createdAt: new Date().toISOString(),
72
73
  reason,
73
- rootDir: codexDir,
74
74
  backupDir,
75
75
  files: entries,
76
76
  };
@@ -84,11 +84,11 @@ function createBackup(codexDir, backupsDir, reason, files) {
84
84
  }
85
85
  }
86
86
  /**
87
- * Restores all files described by a backup manifest back into the Codex directory.
87
+ * Restores all files described by a backup manifest back into their original paths.
88
88
  */
89
89
  function restoreManifest(manifest) {
90
90
  for (const entry of manifest.files) {
91
- const targetPath = path.join(manifest.rootDir, entry.relativePath);
91
+ const targetPath = entry.restorePath;
92
92
  if (!entry.existed) {
93
93
  if (fs.existsSync(targetPath)) {
94
94
  // Remove files that were created by the failed mutation but were absent before it.
@@ -33,17 +33,32 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.CODEX_DIR_ENV_NAME = void 0;
36
+ exports.TOOL_HOME_ENV_NAME = exports.CODEX_DIR_ENV_NAME = void 0;
37
+ exports.resolveCodexSwitchHome = resolveCodexSwitchHome;
37
38
  exports.resolveCodexDir = resolveCodexDir;
38
39
  exports.createCodexPaths = createCodexPaths;
39
40
  const os = __importStar(require("node:os"));
40
41
  const path = __importStar(require("node:path"));
41
42
  exports.CODEX_DIR_ENV_NAME = "CODEXS_CODEX_DIR";
43
+ exports.TOOL_HOME_ENV_NAME = "CODEXS_HOME";
42
44
  const DEVELOPMENT_DEFAULT_CODEX_DIR = path.resolve(process.cwd(), "dev-codex", "local-sandbox");
43
45
  /**
44
- * Resolves the working Codex directory, defaulting to `~/.codex`.
46
+ * Resolves the tool home directory, defaulting to `~/.config/codex-switch`.
45
47
  */
46
- function resolveCodexDir(codexDir) {
48
+ function resolveCodexSwitchHome(toolHomeDir) {
49
+ if (toolHomeDir) {
50
+ return path.resolve(toolHomeDir);
51
+ }
52
+ const envToolHome = process.env[exports.TOOL_HOME_ENV_NAME];
53
+ if (envToolHome) {
54
+ return path.resolve(envToolHome);
55
+ }
56
+ return path.join(os.homedir(), ".config", "codex-switch");
57
+ }
58
+ /**
59
+ * Resolves the working Codex directory using the documented precedence order.
60
+ */
61
+ function resolveCodexDir(codexDir, toolConfig) {
47
62
  if (codexDir) {
48
63
  return path.resolve(codexDir);
49
64
  }
@@ -51,21 +66,32 @@ function resolveCodexDir(codexDir) {
51
66
  if (envCodexDir) {
52
67
  return path.resolve(envCodexDir);
53
68
  }
69
+ if (toolConfig?.defaultCodexDir) {
70
+ return path.resolve(toolConfig.defaultCodexDir);
71
+ }
54
72
  if (process.env.NODE_ENV === "development") {
55
73
  return DEVELOPMENT_DEFAULT_CODEX_DIR;
56
74
  }
57
75
  return path.join(os.homedir(), ".codex");
58
76
  }
59
77
  /**
60
- * Expands a Codex home directory into the file paths used by the CLI.
78
+ * Expands the tool home and Codex runtime into the file paths used by the CLI.
61
79
  */
62
- function createCodexPaths(codexDir) {
80
+ function createCodexPaths(args) {
81
+ const input = typeof args === "string" ? { codexDir: args } : args;
82
+ const toolHomeDir = resolveCodexSwitchHome(input.toolHomeDir);
83
+ const codexDir = path.resolve(input.codexDir);
63
84
  return {
85
+ toolHomeDir,
86
+ toolConfigPath: path.join(toolHomeDir, "codex-switch.json"),
87
+ providersPath: path.join(toolHomeDir, "providers.json"),
88
+ backupsDir: path.join(toolHomeDir, "backups"),
89
+ latestBackupPath: path.join(toolHomeDir, "backups", "latest.json"),
90
+ lockPath: path.join(toolHomeDir, ".codex-switch.lock"),
91
+ runtimeDir: path.join(toolHomeDir, "runtime"),
92
+ runtimesDir: path.join(toolHomeDir, "runtimes"),
64
93
  codexDir,
65
94
  configPath: path.join(codexDir, "config.toml"),
66
- providersPath: path.join(codexDir, "providers.json"),
67
95
  authPath: path.join(codexDir, "auth.json"),
68
- backupsDir: path.join(codexDir, "backups"),
69
- latestBackupPath: path.join(codexDir, "backups", "latest.json"),
70
96
  };
71
97
  }
@@ -40,7 +40,6 @@ exports.listConfigProfiles = listConfigProfiles;
40
40
  exports.ensureProfileExists = ensureProfileExists;
41
41
  exports.requireManagedProfileRuntime = requireManagedProfileRuntime;
42
42
  exports.requireModelProviderRuntimeSection = requireModelProviderRuntimeSection;
43
- exports.resolveActiveProviderName = resolveActiveProviderName;
44
43
  exports.updateTopLevelProfile = updateTopLevelProfile;
45
44
  exports.createConfigMutationPlan = createConfigMutationPlan;
46
45
  exports.applyConfigMutation = applyConfigMutation;
@@ -50,7 +49,6 @@ const os = __importStar(require("node:os"));
50
49
  const path = __importStar(require("node:path"));
51
50
  const config_1 = require("../domain/config");
52
51
  const errors_1 = require("../domain/errors");
53
- const providers_1 = require("../domain/providers");
54
52
  const codex_paths_1 = require("./codex-paths");
55
53
  const fs_utils_1 = require("./fs-utils");
56
54
  /**
@@ -163,27 +161,6 @@ function requireModelProviderRuntimeSection(document, profile) {
163
161
  });
164
162
  }
165
163
  }
166
- /**
167
- * Resolves the current active provider and requires the mapping to be unique.
168
- */
169
- function resolveActiveProviderName(document, providers) {
170
- if (!document.activeProfile) {
171
- throw (0, errors_1.cliError)("PROFILE_NOT_FOUND", "No top-level profile is set in config.toml.");
172
- }
173
- const matches = (0, providers_1.findProvidersByProfile)(providers, document.activeProfile);
174
- if (matches.length === 0) {
175
- throw (0, errors_1.cliError)("UNMANAGED_ACTIVE_PROFILE", `Active profile "${document.activeProfile}" is not mapped by providers.json.`, {
176
- profile: document.activeProfile,
177
- });
178
- }
179
- if (matches.length > 1) {
180
- throw (0, errors_1.cliError)("ACTIVE_PROVIDER_UNRESOLVED", `Active profile "${document.activeProfile}" maps to multiple providers, so the active managed provider is ambiguous.`, {
181
- profile: document.activeProfile,
182
- providers: matches,
183
- });
184
- }
185
- return matches[0];
186
- }
187
164
  /**
188
165
  * Rewrites config.toml so the requested profile becomes the active top-level profile.
189
166
  */
@@ -35,15 +35,13 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.withCodexLock = withCodexLock;
37
37
  const fs = __importStar(require("node:fs"));
38
- const path = __importStar(require("node:path"));
39
38
  const errors_1 = require("../domain/errors");
40
39
  const fs_utils_1 = require("./fs-utils");
41
40
  /**
42
41
  * Executes a mutation while holding an exclusive codex-switch lock file.
43
42
  */
44
- function withCodexLock(codexDir, operation, run) {
45
- (0, fs_utils_1.ensureDir)(codexDir);
46
- const lockPath = path.join(codexDir, ".codex-switch.lock");
43
+ function withCodexLock(lockPath, operation, run) {
44
+ (0, fs_utils_1.ensureDir)(require("node:path").dirname(lockPath));
47
45
  acquireLock(lockPath, operation);
48
46
  try {
49
47
  return run();
@@ -39,25 +39,26 @@ exports.inspectCopilotBridgeState = inspectCopilotBridgeState;
39
39
  exports.writeCopilotBridgeState = writeCopilotBridgeState;
40
40
  exports.clearCopilotBridgeState = clearCopilotBridgeState;
41
41
  const fs = __importStar(require("node:fs"));
42
- const os = __importStar(require("node:os"));
43
42
  const path = __importStar(require("node:path"));
44
43
  const errors_1 = require("../domain/errors");
44
+ const codex_paths_1 = require("./codex-paths");
45
45
  const fs_utils_1 = require("./fs-utils");
46
46
  /**
47
- * Returns the user-level runtime state file used by Copilot bridge helpers.
47
+ * Returns the tool-home runtime state file used by Copilot bridge helpers.
48
48
  */
49
- function getCopilotBridgeStatePath() {
49
+ function getCopilotBridgeStatePath(runtimeDir) {
50
50
  const override = process.env.CODEX_SWITCH_RUNTIME_STATE_DIR;
51
51
  if (override && override.trim() !== "") {
52
52
  return path.join(path.resolve(override), "copilot-bridge-state.json");
53
53
  }
54
- return path.join(os.homedir(), ".codex-switch", "runtime", "copilot-bridge-state.json");
54
+ const baseRuntimeDir = runtimeDir ? path.resolve(runtimeDir) : path.join((0, codex_paths_1.resolveCodexSwitchHome)(), "runtime");
55
+ return path.join(baseRuntimeDir, "copilot-bridge-state.json");
55
56
  }
56
57
  /**
57
58
  * Reads the Copilot bridge state manifest when present.
58
59
  */
59
- function readCopilotBridgeState() {
60
- const statePath = getCopilotBridgeStatePath();
60
+ function readCopilotBridgeState(runtimeDir) {
61
+ const statePath = getCopilotBridgeStatePath(runtimeDir);
61
62
  if (!fs.existsSync(statePath)) {
62
63
  return null;
63
64
  }
@@ -66,8 +67,8 @@ function readCopilotBridgeState() {
66
67
  /**
67
68
  * Safely inspects the runtime-state file for status/doctor style read paths.
68
69
  */
69
- function inspectCopilotBridgeState() {
70
- const statePath = getCopilotBridgeStatePath();
70
+ function inspectCopilotBridgeState(runtimeDir) {
71
+ const statePath = getCopilotBridgeStatePath(runtimeDir);
71
72
  if (!fs.existsSync(statePath)) {
72
73
  return {
73
74
  exists: false,
@@ -81,7 +82,7 @@ function inspectCopilotBridgeState() {
81
82
  exists: true,
82
83
  valid: true,
83
84
  parseError: null,
84
- state: readCopilotBridgeState(),
85
+ state: readCopilotBridgeState(runtimeDir),
85
86
  };
86
87
  }
87
88
  catch (error) {
@@ -96,16 +97,16 @@ function inspectCopilotBridgeState() {
96
97
  /**
97
98
  * Persists the Copilot bridge state manifest.
98
99
  */
99
- function writeCopilotBridgeState(state) {
100
- const statePath = getCopilotBridgeStatePath();
100
+ function writeCopilotBridgeState(state, runtimeDir) {
101
+ const statePath = getCopilotBridgeStatePath(runtimeDir);
101
102
  (0, fs_utils_1.ensureDir)(path.dirname(statePath));
102
103
  (0, fs_utils_1.writeTextFileAtomic)(statePath, `${JSON.stringify(state, null, 2)}\n`);
103
104
  }
104
105
  /**
105
106
  * Deletes the Copilot bridge state manifest when present.
106
107
  */
107
- function clearCopilotBridgeState() {
108
- const statePath = getCopilotBridgeStatePath();
108
+ function clearCopilotBridgeState(runtimeDir) {
109
+ const statePath = getCopilotBridgeStatePath(runtimeDir);
109
110
  if (fs.existsSync(statePath)) {
110
111
  fs.rmSync(statePath, { force: true });
111
112
  }
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.readToolConfigIfExists = readToolConfigIfExists;
37
+ exports.ensureToolConfig = ensureToolConfig;
38
+ exports.writeToolConfig = writeToolConfig;
39
+ const fs = __importStar(require("node:fs"));
40
+ const errors_1 = require("../domain/errors");
41
+ const fs_utils_1 = require("./fs-utils");
42
+ /**
43
+ * Reads the optional tool-level codex-switch config file when present.
44
+ */
45
+ function readToolConfigIfExists(toolConfigPath) {
46
+ if (!fs.existsSync(toolConfigPath)) {
47
+ return null;
48
+ }
49
+ try {
50
+ const parsed = JSON.parse(fs.readFileSync(toolConfigPath, "utf8"));
51
+ return validateToolConfig(parsed, toolConfigPath);
52
+ }
53
+ catch (error) {
54
+ throw (0, errors_1.cliError)("INVALID_CONFIG", "codex-switch.json is invalid.", {
55
+ file: toolConfigPath,
56
+ cause: error instanceof Error ? error.message : String(error),
57
+ });
58
+ }
59
+ }
60
+ /**
61
+ * Ensures the tool-level config file exists with the minimum stable fields.
62
+ */
63
+ function ensureToolConfig(toolConfigPath, version, defaultCodexDir) {
64
+ const current = readToolConfigIfExists(toolConfigPath);
65
+ if (current) {
66
+ return {
67
+ created: false,
68
+ config: current,
69
+ };
70
+ }
71
+ const next = {
72
+ version,
73
+ };
74
+ if (defaultCodexDir) {
75
+ next.defaultCodexDir = defaultCodexDir;
76
+ }
77
+ (0, fs_utils_1.ensureDir)(require("node:path").dirname(toolConfigPath));
78
+ (0, fs_utils_1.writeTextFileAtomic)(toolConfigPath, `${JSON.stringify(next, null, 2)}\n`);
79
+ return {
80
+ created: true,
81
+ config: next,
82
+ };
83
+ }
84
+ /**
85
+ * Writes the tool-level config file with a normalized shape.
86
+ */
87
+ function writeToolConfig(toolConfigPath, config) {
88
+ const normalized = validateToolConfig(config, toolConfigPath);
89
+ (0, fs_utils_1.writeTextFileAtomic)(toolConfigPath, `${JSON.stringify(normalized, null, 2)}\n`);
90
+ }
91
+ function validateToolConfig(config, toolConfigPath) {
92
+ if (!config || typeof config !== "object") {
93
+ throw (0, errors_1.cliError)("INVALID_CONFIG", "codex-switch.json must contain a JSON object.", {
94
+ file: toolConfigPath,
95
+ });
96
+ }
97
+ if (typeof config.version !== "string" || config.version.trim() === "") {
98
+ throw (0, errors_1.cliError)("INVALID_CONFIG", "codex-switch.json requires a non-empty version field.", {
99
+ file: toolConfigPath,
100
+ });
101
+ }
102
+ if (config.defaultCodexDir !== undefined && typeof config.defaultCodexDir !== "string") {
103
+ throw (0, errors_1.cliError)("INVALID_CONFIG", "codex-switch.json.defaultCodexDir must be a string when provided.", {
104
+ file: toolConfigPath,
105
+ });
106
+ }
107
+ return {
108
+ version: config.version,
109
+ defaultCodexDir: config.defaultCodexDir,
110
+ };
111
+ }