@minniexcode/codex-switch 0.0.9 → 0.0.11

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 +52 -13
  2. package/README.CN.md +94 -39
  3. package/README.md +75 -33
  4. package/dist/app/add-provider.js +29 -26
  5. package/dist/app/bridge.js +15 -15
  6. package/dist/app/edit-provider.js +2 -18
  7. package/dist/app/get-status.js +35 -13
  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 +0 -1
  11. package/dist/app/remove-provider.js +1 -1
  12. package/dist/app/run-doctor.js +21 -39
  13. package/dist/app/run-mutation.js +3 -2
  14. package/dist/app/setup-codex.js +30 -18
  15. package/dist/app/show-config.js +1 -5
  16. package/dist/app/switch-provider.js +16 -33
  17. package/dist/cli/output.js +4 -6
  18. package/dist/cli.js +35 -3
  19. package/dist/commands/args.js +2 -2
  20. package/dist/commands/dispatch.js +40 -0
  21. package/dist/commands/handlers.js +202 -84
  22. package/dist/commands/help.js +2 -0
  23. package/dist/commands/registry.js +33 -12
  24. package/dist/domain/backups.js +4 -4
  25. package/dist/domain/config.js +102 -61
  26. package/dist/domain/providers.js +12 -5
  27. package/dist/domain/runtime-state.js +81 -4
  28. package/dist/domain/setup.js +58 -3
  29. package/dist/interaction/add-interactive.js +55 -1
  30. package/dist/interaction/interactive.js +1 -5
  31. package/dist/runtime/copilot-adapter.js +56 -13
  32. package/dist/runtime/copilot-bridge.js +392 -44
  33. package/dist/runtime/copilot-cli.js +142 -0
  34. package/dist/runtime/copilot-installer.js +59 -11
  35. package/dist/runtime/copilot-sdk-loader.js +5 -5
  36. package/dist/storage/auth-repo.js +28 -77
  37. package/dist/storage/backup-repo.js +4 -4
  38. package/dist/storage/codex-paths.js +34 -8
  39. package/dist/storage/config-repo.js +1 -36
  40. package/dist/storage/lock-repo.js +2 -4
  41. package/dist/storage/runtime-state-repo.js +43 -10
  42. package/dist/storage/tool-config-repo.js +111 -0
  43. package/docs/Design/codex-switch-copilot-integration-design.md +517 -0
  44. package/docs/Design/codex-switch-v0.0.10-design.md +669 -0
  45. package/docs/Design/codex-switch-v0.0.11-design.md +824 -0
  46. package/docs/PRD/codex-switch-prd-v0.0.10.md +406 -0
  47. package/docs/PRD/codex-switch-prd-v0.0.11.md +577 -0
  48. package/docs/cli-usage.md +166 -271
  49. package/docs/codex-switch-product-overview.md +2 -2
  50. package/docs/codex-switch-technical-architecture.md +6 -5
  51. package/package.json +1 -1
@@ -0,0 +1,142 @@
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.setCopilotCliSpawnImplementation = setCopilotCliSpawnImplementation;
37
+ exports.resetCopilotCliSpawnImplementation = resetCopilotCliSpawnImplementation;
38
+ exports.checkCopilotCliAvailable = checkCopilotCliAvailable;
39
+ exports.runCopilotLogin = runCopilotLogin;
40
+ const fs = __importStar(require("node:fs"));
41
+ const path = __importStar(require("node:path"));
42
+ const node_child_process_1 = require("node:child_process");
43
+ const copilot_installer_1 = require("./copilot-installer");
44
+ let spawnImplementation = node_child_process_1.spawnSync;
45
+ /**
46
+ * Overrides the spawn implementation for Copilot CLI tests.
47
+ */
48
+ function setCopilotCliSpawnImplementation(spawnLike) {
49
+ spawnImplementation = spawnLike;
50
+ }
51
+ /**
52
+ * Restores the default spawn implementation after tests.
53
+ */
54
+ function resetCopilotCliSpawnImplementation() {
55
+ spawnImplementation = node_child_process_1.spawnSync;
56
+ }
57
+ /**
58
+ * Checks whether the GitHub Copilot CLI is available either from the bundled runtime or on PATH.
59
+ */
60
+ function checkCopilotCliAvailable(runtimesDir) {
61
+ const invocation = getCopilotInvocation(["--help"], runtimesDir);
62
+ const result = spawnImplementation(invocation.command, invocation.args, {
63
+ stdio: "pipe",
64
+ encoding: "utf8",
65
+ shell: invocation.shell,
66
+ });
67
+ if (result.error || result.status !== 0) {
68
+ return {
69
+ ok: false,
70
+ cause: result.error?.message ?? (result.stderr.trim() || "Unknown failure"),
71
+ source: invocation.source,
72
+ command: formatInvocation(invocation),
73
+ };
74
+ }
75
+ return {
76
+ ok: true,
77
+ source: invocation.source,
78
+ command: formatInvocation(invocation),
79
+ };
80
+ }
81
+ /**
82
+ * Launches the official `copilot login` flow in the current terminal.
83
+ */
84
+ function runCopilotLogin(options) {
85
+ const args = ["login"];
86
+ if (options?.host) {
87
+ args.push("--hostname", options.host);
88
+ }
89
+ const invocation = getCopilotInvocation(args, options?.runtimesDir);
90
+ const result = spawnImplementation(invocation.command, invocation.args, {
91
+ stdio: "inherit",
92
+ shell: invocation.shell,
93
+ });
94
+ if (result.error || result.status !== 0) {
95
+ throw new Error(result.error?.message ??
96
+ `${formatInvocation(invocation)} exited with status ${String(result.status)}`);
97
+ }
98
+ }
99
+ /**
100
+ * Resolves a cross-platform invocation for the Copilot CLI.
101
+ */
102
+ function getCopilotInvocation(args, runtimesDir) {
103
+ const bundledCommand = resolveBundledCopilotCommand(runtimesDir);
104
+ const executable = bundledCommand ?? "copilot";
105
+ if (process.platform === "win32") {
106
+ return {
107
+ command: executable,
108
+ args,
109
+ source: bundledCommand ? "bundled" : "path",
110
+ shell: true,
111
+ };
112
+ }
113
+ return {
114
+ command: executable,
115
+ args,
116
+ source: bundledCommand ? "bundled" : "path",
117
+ shell: false,
118
+ };
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,26 +101,73 @@ 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)) {
108
109
  fs.writeFileSync(packageJsonPath, `${JSON.stringify({ name: "codex-switch-copilot-runtime", private: true, version: "0.0.0" }, null, 2)}\n`, "utf8");
109
110
  }
110
- const command = process.platform === "win32" ? "npm.cmd" : "npm";
111
- const result = spawnImplementation(command, ["install", "--no-save", `${COPILOT_SDK_PACKAGE}@${COPILOT_SDK_VERSION}`], {
111
+ const installCommand = resolveNpmInstallCommand();
112
+ const result = spawnImplementation(installCommand.command, installCommand.args, {
112
113
  cwd: installDir,
113
114
  stdio: "pipe",
114
115
  encoding: "utf8",
115
116
  shell: false,
116
117
  });
118
+ if (result.error) {
119
+ throw (0, errors_1.cliError)("COPILOT_SDK_INSTALL_FAILED", "Failed to install the optional Copilot SDK runtime.", {
120
+ installDir,
121
+ packageName: COPILOT_SDK_PACKAGE,
122
+ cause: result.error.message,
123
+ errorCode: result.error.code ?? null,
124
+ command: installCommand.command,
125
+ args: installCommand.args,
126
+ });
127
+ }
117
128
  if (result.status !== 0) {
118
129
  throw (0, errors_1.cliError)("COPILOT_SDK_INSTALL_FAILED", "Failed to install the optional Copilot SDK runtime.", {
119
130
  installDir,
120
131
  packageName: COPILOT_SDK_PACKAGE,
121
132
  cause: result.stderr || result.stdout || `npm exited with status ${String(result.status)}`,
133
+ command: installCommand.command,
134
+ args: installCommand.args,
122
135
  });
123
136
  }
124
- return probeCopilotSdkInstall();
137
+ return probeCopilotSdkInstall(runtimesDir);
138
+ }
139
+ /**
140
+ * Resolves a stable npm install invocation for the optional Copilot SDK runtime.
141
+ */
142
+ function resolveNpmInstallCommand() {
143
+ const installArgs = ["install", "--no-save", `${COPILOT_SDK_PACKAGE}@${COPILOT_SDK_VERSION}`];
144
+ const npmCliPath = resolveNpmCliPath();
145
+ if (npmCliPath) {
146
+ return {
147
+ command: process.execPath,
148
+ args: [npmCliPath, ...installArgs],
149
+ };
150
+ }
151
+ return {
152
+ command: process.platform === "win32" ? "npm.cmd" : "npm",
153
+ args: installArgs,
154
+ };
155
+ }
156
+ /**
157
+ * Finds a locally available npm CLI script near the active Node runtime.
158
+ */
159
+ function resolveNpmCliPath() {
160
+ const execDir = path.dirname(process.execPath);
161
+ const candidates = [
162
+ process.env.npm_execpath,
163
+ path.join(execDir, "node_modules", "npm", "bin", "npm-cli.js"),
164
+ path.join(execDir, "..", "node_modules", "npm", "bin", "npm-cli.js"),
165
+ path.join(execDir, "..", "..", "node_modules", "npm", "bin", "npm-cli.js"),
166
+ ];
167
+ for (const candidate of candidates) {
168
+ if (candidate && fs.existsSync(candidate)) {
169
+ return path.resolve(candidate);
170
+ }
171
+ }
172
+ return null;
125
173
  }
@@ -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
  }
@@ -34,15 +34,10 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.readAuthFileIfExists = readAuthFileIfExists;
37
- exports.buildManagedAuthPayload = buildManagedAuthPayload;
38
- exports.buildManagedAuthJson = buildManagedAuthJson;
39
- exports.writeAuthFile = writeAuthFile;
40
- exports.extractManagedAuthFingerprint = extractManagedAuthFingerprint;
41
- exports.readManagedAuthState = readManagedAuthState;
37
+ exports.readAuthFileState = readAuthFileState;
38
+ exports.writeOpenAiApiKeyAuth = writeOpenAiApiKeyAuth;
42
39
  const fs = __importStar(require("node:fs"));
43
40
  const errors_1 = require("../domain/errors");
44
- const fs_utils_1 = require("./fs-utils");
45
- const LEGACY_MANAGED_SECRET_KEYS = new Set(["api_key"]);
46
41
  /**
47
42
  * Reads auth.json when it exists and returns null otherwise.
48
43
  */
@@ -61,84 +56,27 @@ function readAuthFileIfExists(authPath) {
61
56
  }
62
57
  }
63
58
  /**
64
- * Builds the stable managed auth payload for one provider.
59
+ * Reads auth.json into a neutral file-state summary for status and doctor.
65
60
  */
66
- function buildManagedAuthPayload(provider) {
67
- return {
68
- auth_mode: "apikey",
69
- [provider.envKey]: provider.apiKey,
70
- };
71
- }
72
- /**
73
- * Builds the next auth.json object while preserving unmanaged metadata.
74
- */
75
- function buildManagedAuthJson(provider, existingAuthJson) {
76
- const nextManaged = buildManagedAuthPayload(provider);
77
- const result = {};
78
- if (existingAuthJson && typeof existingAuthJson === "object" && !Array.isArray(existingAuthJson)) {
79
- for (const [key, value] of Object.entries(existingAuthJson)) {
80
- if (key === "auth_mode" || LEGACY_MANAGED_SECRET_KEYS.has(key) || looksLikeManagedSecretKey(key)) {
81
- continue;
82
- }
83
- result[key] = value;
84
- }
85
- }
86
- result.auth_mode = nextManaged.auth_mode;
87
- result[provider.envKey] = provider.apiKey;
88
- return result;
89
- }
90
- /**
91
- * Writes auth.json atomically using the managed mirror strategy.
92
- */
93
- function writeAuthFile(authPath, provider, existingAuthJson) {
94
- (0, fs_utils_1.writeTextFileAtomic)(authPath, `${JSON.stringify(buildManagedAuthJson(provider, existingAuthJson), null, 2)}\n`);
95
- }
96
- /**
97
- * Extracts a lightweight fingerprint used by doctor/status.
98
- */
99
- function extractManagedAuthFingerprint(input) {
100
- if (!input || typeof input !== "object" || Array.isArray(input)) {
101
- return {
102
- authMode: null,
103
- managedSecretKeys: [],
104
- payload: null,
105
- };
106
- }
107
- const payload = input;
108
- const authMode = typeof payload.auth_mode === "string" ? payload.auth_mode : null;
109
- const managedSecretKeys = Object.keys(payload)
110
- .filter((key) => key !== "auth_mode" && looksLikeManagedSecretKey(key))
111
- .sort();
112
- return {
113
- authMode,
114
- managedSecretKeys,
115
- payload,
116
- };
117
- }
118
- /**
119
- * Reads auth.json into a doctor-friendly managed state summary.
120
- */
121
- function readManagedAuthState(authPath) {
61
+ function readAuthFileState(authPath) {
122
62
  if (!fs.existsSync(authPath)) {
123
63
  return {
124
64
  exists: false,
125
65
  valid: false,
126
66
  parseError: null,
127
67
  authMode: null,
128
- managedSecretKeys: [],
129
- payload: null,
130
68
  };
131
69
  }
132
70
  try {
133
71
  const payload = readAuthFileIfExists(authPath);
134
- const fingerprint = extractManagedAuthFingerprint(payload);
72
+ const authMode = payload && typeof payload === "object" && !Array.isArray(payload) && typeof payload.auth_mode === "string"
73
+ ? String(payload.auth_mode)
74
+ : null;
135
75
  return {
136
76
  exists: true,
137
- valid: Boolean(fingerprint.payload),
77
+ valid: Boolean(payload && typeof payload === "object" && !Array.isArray(payload)),
138
78
  parseError: null,
139
- authMode: fingerprint.authMode,
140
- managedSecretKeys: fingerprint.managedSecretKeys,
141
- payload: fingerprint.payload,
79
+ authMode,
142
80
  };
143
81
  }
144
82
  catch (error) {
@@ -147,14 +85,27 @@ function readManagedAuthState(authPath) {
147
85
  valid: false,
148
86
  parseError: (0, errors_1.normalizeError)(error).message,
149
87
  authMode: null,
150
- managedSecretKeys: [],
151
- payload: null,
152
88
  };
153
89
  }
154
90
  }
155
- function looksLikeManagedSecretKey(key) {
156
- if (LEGACY_MANAGED_SECRET_KEYS.has(key)) {
157
- return true;
91
+ /**
92
+ * Writes the active direct-provider auth projection expected by Codex.
93
+ * Invalid or missing existing auth.json content is replaced with a minimal valid object.
94
+ */
95
+ function writeOpenAiApiKeyAuth(authPath, apiKey) {
96
+ let next = {};
97
+ if (fs.existsSync(authPath)) {
98
+ try {
99
+ const payload = JSON.parse(fs.readFileSync(authPath, "utf8"));
100
+ if (payload && typeof payload === "object" && !Array.isArray(payload)) {
101
+ next = { ...payload };
102
+ }
103
+ }
104
+ catch {
105
+ next = {};
106
+ }
158
107
  }
159
- return /^[A-Z0-9_]+$/.test(key);
108
+ next.auth_mode = "apikey";
109
+ next.OPENAI_API_KEY = apiKey;
110
+ fs.writeFileSync(authPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
160
111
  }
@@ -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.requireRuntimeEnvKey = requireRuntimeEnvKey;
44
43
  exports.resolveActiveProviderName = resolveActiveProviderName;
45
44
  exports.updateTopLevelProfile = updateTopLevelProfile;
46
45
  exports.createConfigMutationPlan = createConfigMutationPlan;
@@ -143,13 +142,6 @@ function requireManagedProfileRuntime(document, providers, profile) {
143
142
  missingFields: ["base_url"],
144
143
  });
145
144
  }
146
- if (!modelProviderSection.envKey) {
147
- throw (0, errors_1.cliError)("MODEL_PROVIDER_ENV_KEY_MISSING", `Model provider "${view.modelProvider}" requires env_key.`, {
148
- profile,
149
- modelProvider: view.modelProvider,
150
- missingFields: ["env_key"],
151
- });
152
- }
153
145
  return view;
154
146
  }
155
147
  /**
@@ -170,33 +162,6 @@ function requireModelProviderRuntimeSection(document, profile) {
170
162
  missingFields: ["base_url"],
171
163
  });
172
164
  }
173
- if (!modelProviderSection.envKey) {
174
- throw (0, errors_1.cliError)("MODEL_PROVIDER_ENV_KEY_MISSING", `Model provider "${profile}" requires env_key.`, {
175
- profile,
176
- modelProvider: profile,
177
- missingFields: ["env_key"],
178
- });
179
- }
180
- }
181
- /**
182
- * Returns the runtime env_key for one profile or throws a typed error.
183
- */
184
- function requireRuntimeEnvKey(document, profile) {
185
- const modelProviderSection = document.modelProviders.find((entry) => entry.name === profile);
186
- if (!modelProviderSection) {
187
- throw (0, errors_1.cliError)("PROFILE_NOT_FOUND", `Model provider "${profile}" does not exist in config.toml.`, {
188
- profile,
189
- modelProvider: profile,
190
- });
191
- }
192
- if (!modelProviderSection.envKey) {
193
- throw (0, errors_1.cliError)("MODEL_PROVIDER_ENV_KEY_MISSING", `Model provider "${profile}" requires env_key.`, {
194
- profile,
195
- modelProvider: profile,
196
- missingFields: ["env_key"],
197
- });
198
- }
199
- return modelProviderSection.envKey;
200
165
  }
201
166
  /**
202
167
  * Resolves the current active provider and requires the mapping to be unique.
@@ -212,7 +177,7 @@ function resolveActiveProviderName(document, providers) {
212
177
  });
213
178
  }
214
179
  if (matches.length > 1) {
215
- throw (0, errors_1.cliError)("ACTIVE_PROVIDER_UNRESOLVED", `Active profile "${document.activeProfile}" maps to multiple providers.`, {
180
+ throw (0, errors_1.cliError)("ACTIVE_PROVIDER_UNRESOLVED", `Active profile "${document.activeProfile}" maps to multiple providers, so the active managed provider is ambiguous.`, {
216
181
  profile: document.activeProfile,
217
182
  providers: matches,
218
183
  });
@@ -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();