@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.
- package/README.AI.md +52 -13
- package/README.CN.md +94 -39
- package/README.md +75 -33
- package/dist/app/add-provider.js +29 -26
- package/dist/app/bridge.js +15 -15
- package/dist/app/edit-provider.js +2 -18
- package/dist/app/get-status.js +35 -13
- package/dist/app/import-providers.js +1 -1
- package/dist/app/init-codex.js +13 -14
- package/dist/app/list-providers.js +0 -1
- package/dist/app/remove-provider.js +1 -1
- package/dist/app/run-doctor.js +21 -39
- package/dist/app/run-mutation.js +3 -2
- package/dist/app/setup-codex.js +30 -18
- package/dist/app/show-config.js +1 -5
- package/dist/app/switch-provider.js +16 -33
- package/dist/cli/output.js +4 -6
- package/dist/cli.js +35 -3
- package/dist/commands/args.js +2 -2
- package/dist/commands/dispatch.js +40 -0
- package/dist/commands/handlers.js +202 -84
- package/dist/commands/help.js +2 -0
- package/dist/commands/registry.js +33 -12
- package/dist/domain/backups.js +4 -4
- package/dist/domain/config.js +102 -61
- package/dist/domain/providers.js +12 -5
- package/dist/domain/runtime-state.js +81 -4
- package/dist/domain/setup.js +58 -3
- package/dist/interaction/add-interactive.js +55 -1
- package/dist/interaction/interactive.js +1 -5
- package/dist/runtime/copilot-adapter.js +56 -13
- package/dist/runtime/copilot-bridge.js +392 -44
- package/dist/runtime/copilot-cli.js +142 -0
- package/dist/runtime/copilot-installer.js +59 -11
- package/dist/runtime/copilot-sdk-loader.js +5 -5
- package/dist/storage/auth-repo.js +28 -77
- package/dist/storage/backup-repo.js +4 -4
- package/dist/storage/codex-paths.js +34 -8
- package/dist/storage/config-repo.js +1 -36
- package/dist/storage/lock-repo.js +2 -4
- package/dist/storage/runtime-state-repo.js +43 -10
- package/dist/storage/tool-config-repo.js +111 -0
- package/docs/Design/codex-switch-copilot-integration-design.md +517 -0
- package/docs/Design/codex-switch-v0.0.10-design.md +669 -0
- package/docs/Design/codex-switch-v0.0.11-design.md +824 -0
- package/docs/PRD/codex-switch-prd-v0.0.10.md +406 -0
- package/docs/PRD/codex-switch-prd-v0.0.11.md +577 -0
- package/docs/cli-usage.md +166 -271
- package/docs/codex-switch-product-overview.md +2 -2
- package/docs/codex-switch-technical-architecture.md +6 -5
- package/package.json +1 -1
|
@@ -55,10 +55,13 @@ const show_provider_1 = require("../app/show-provider");
|
|
|
55
55
|
const switch_provider_1 = require("../app/switch-provider");
|
|
56
56
|
const config_1 = require("../domain/config");
|
|
57
57
|
const errors_1 = require("../domain/errors");
|
|
58
|
+
const setup_1 = require("../domain/setup");
|
|
58
59
|
const providers_1 = require("../domain/providers");
|
|
59
60
|
const add_interactive_1 = require("../interaction/add-interactive");
|
|
60
61
|
const interactive_1 = require("../interaction/interactive");
|
|
61
62
|
const prompt_1 = require("../interaction/prompt");
|
|
63
|
+
const copilot_adapter_1 = require("../runtime/copilot-adapter");
|
|
64
|
+
const copilot_cli_1 = require("../runtime/copilot-cli");
|
|
62
65
|
const copilot_installer_1 = require("../runtime/copilot-installer");
|
|
63
66
|
const config_repo_1 = require("../storage/config-repo");
|
|
64
67
|
const codex_paths_1 = require("../storage/codex-paths");
|
|
@@ -68,6 +71,10 @@ const args_1 = require("./args");
|
|
|
68
71
|
* Executes one command handler selected from the shared command registry.
|
|
69
72
|
*/
|
|
70
73
|
async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.createPromptRuntime)()) {
|
|
74
|
+
const packageVersion = require("../../package.json").version ?? "0.0.0";
|
|
75
|
+
if (!ctx.options.codexDir) {
|
|
76
|
+
throw (0, errors_1.cliError)("CODEX_DIR_NOT_FOUND", "No Codex directory could be resolved.");
|
|
77
|
+
}
|
|
71
78
|
let setupPaths = (0, codex_paths_1.createCodexPaths)(ctx.options.codexDir);
|
|
72
79
|
const paths = setupPaths;
|
|
73
80
|
switch (ctx.command) {
|
|
@@ -90,12 +97,17 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
90
97
|
case "current":
|
|
91
98
|
return (0, get_current_profile_1.getCurrentProfile)(paths.configPath);
|
|
92
99
|
case "status":
|
|
93
|
-
return (0, get_status_1.getStatus)(paths.codexDir, paths.configPath, paths.providersPath, paths.authPath
|
|
100
|
+
return (0, get_status_1.getStatus)(paths.codexDir, paths.configPath, paths.providersPath, paths.authPath, {
|
|
101
|
+
runtimeDir: paths.runtimeDir,
|
|
102
|
+
runtimesDir: paths.runtimesDir,
|
|
103
|
+
});
|
|
94
104
|
case "bridge-start": {
|
|
95
105
|
const providerName = parsed.positionals[0] ?? null;
|
|
96
106
|
return (0, bridge_1.startBridge)({
|
|
97
107
|
providersPath: paths.providersPath,
|
|
98
108
|
configPath: paths.configPath,
|
|
109
|
+
runtimeDir: paths.runtimeDir,
|
|
110
|
+
runtimesDir: paths.runtimesDir,
|
|
99
111
|
providerName,
|
|
100
112
|
runtime,
|
|
101
113
|
json: ctx.options.json,
|
|
@@ -106,6 +118,8 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
106
118
|
return (0, bridge_1.stopBridge)({
|
|
107
119
|
providersPath: paths.providersPath,
|
|
108
120
|
configPath: paths.configPath,
|
|
121
|
+
runtimeDir: paths.runtimeDir,
|
|
122
|
+
runtimesDir: paths.runtimesDir,
|
|
109
123
|
providerName,
|
|
110
124
|
runtime,
|
|
111
125
|
json: ctx.options.json,
|
|
@@ -116,57 +130,103 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
116
130
|
return (0, bridge_1.statusBridge)({
|
|
117
131
|
providersPath: paths.providersPath,
|
|
118
132
|
configPath: paths.configPath,
|
|
133
|
+
runtimeDir: paths.runtimeDir,
|
|
134
|
+
runtimesDir: paths.runtimesDir,
|
|
119
135
|
providerName,
|
|
120
136
|
runtime,
|
|
121
137
|
json: ctx.options.json,
|
|
122
138
|
});
|
|
123
139
|
}
|
|
124
140
|
case "init": {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (!(0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
|
|
138
|
-
throw (0, errors_1.cliError)("CODEX_DIR_NOT_FOUND", "No Codex directory could be found.", {
|
|
139
|
-
codexDir: ctx.options.codexDir,
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
codexDir = await (0, interactive_1.chooseCodexDir)(runtime, candidates);
|
|
143
|
-
}
|
|
144
|
-
else {
|
|
145
|
-
codexDir = candidates[0];
|
|
146
|
-
}
|
|
141
|
+
return (0, init_codex_1.initCodex)({
|
|
142
|
+
toolHomeDir: setupPaths.toolHomeDir,
|
|
143
|
+
toolConfigPath: setupPaths.toolConfigPath,
|
|
144
|
+
providersPath: setupPaths.providersPath,
|
|
145
|
+
version: packageVersion,
|
|
146
|
+
defaultCodexDir: ctx.options.codexDirExplicit ? setupPaths.codexDir : null,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
case "login": {
|
|
150
|
+
const upstream = (parsed.positionals[0] ?? "").toLowerCase();
|
|
151
|
+
if (ctx.options.json || !runtime.isInteractive()) {
|
|
152
|
+
throw (0, errors_1.cliError)("COPILOT_LOGIN_REQUIRES_TTY", "login requires an interactive TTY and does not support --json.");
|
|
147
153
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
+
if (upstream !== "copilot" && upstream !== "github-copilot") {
|
|
155
|
+
throw (0, errors_1.cliError)("INVALID_ARGUMENT", `Unsupported upstream "${parsed.positionals[0] ?? ""}".`, {
|
|
156
|
+
supportedUpstreams: ["copilot", "github-copilot"],
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
const installed = (0, copilot_installer_1.probeCopilotSdkInstall)(paths.runtimesDir);
|
|
160
|
+
let installedNow = false;
|
|
161
|
+
if (!installed.installed) {
|
|
162
|
+
const confirmInstall = await runtime.confirmAction("The Copilot SDK runtime is not installed. Install it now?", {
|
|
163
|
+
defaultValue: true,
|
|
164
|
+
});
|
|
165
|
+
if (!confirmInstall) {
|
|
166
|
+
throw (0, errors_1.cliError)("COPILOT_SDK_MISSING", "The optional Copilot SDK runtime is not installed.", {
|
|
167
|
+
installDir: installed.installDir,
|
|
168
|
+
packageName: installed.packageName,
|
|
154
169
|
});
|
|
155
170
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
171
|
+
runtime.writeLine("Installing Copilot SDK runtime...");
|
|
172
|
+
(0, copilot_installer_1.installCopilotSdk)(paths.runtimesDir);
|
|
173
|
+
installedNow = true;
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
await (0, copilot_adapter_1.readCopilotAuthState)(paths.runtimesDir);
|
|
177
|
+
return {
|
|
178
|
+
data: {
|
|
179
|
+
upstream: "github-copilot",
|
|
180
|
+
sdkInstalled: true,
|
|
181
|
+
sdkInstalledNow: installedNow,
|
|
182
|
+
authReady: true,
|
|
183
|
+
loginLaunched: false,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
const normalized = (0, errors_1.normalizeError)(error);
|
|
189
|
+
if (normalized.code !== "COPILOT_AUTH_REQUIRED") {
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const availability = (0, copilot_cli_1.checkCopilotCliAvailable)(paths.runtimesDir);
|
|
194
|
+
if (!availability.ok) {
|
|
195
|
+
throw (0, errors_1.cliError)("COPILOT_CLI_MISSING", "The official Copilot CLI could not be resolved from the installed runtime or PATH.", {
|
|
196
|
+
cause: availability.cause,
|
|
197
|
+
source: availability.source ?? null,
|
|
198
|
+
command: availability.command ?? null,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
(0, copilot_cli_1.runCopilotLogin)({ runtimesDir: paths.runtimesDir });
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
throw (0, errors_1.cliError)("COPILOT_LOGIN_LAUNCH_FAILED", "Failed to launch `copilot login`.", {
|
|
206
|
+
cause: error instanceof Error ? error.message : String(error),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
await (0, copilot_adapter_1.readCopilotAuthState)(paths.runtimesDir);
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
const normalized = (0, errors_1.normalizeError)(error);
|
|
214
|
+
if (normalized.code === "COPILOT_AUTH_REQUIRED") {
|
|
215
|
+
throw (0, errors_1.cliError)("COPILOT_LOGIN_RECHECK_FAILED", "Copilot login completed but auth readiness recheck still failed.", {
|
|
216
|
+
...(normalized.details ?? {}),
|
|
160
217
|
});
|
|
161
218
|
}
|
|
219
|
+
throw error;
|
|
162
220
|
}
|
|
163
|
-
return
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
221
|
+
return {
|
|
222
|
+
data: {
|
|
223
|
+
upstream: "github-copilot",
|
|
224
|
+
sdkInstalled: true,
|
|
225
|
+
sdkInstalledNow: installedNow,
|
|
226
|
+
authReady: true,
|
|
227
|
+
loginLaunched: true,
|
|
228
|
+
},
|
|
229
|
+
};
|
|
170
230
|
}
|
|
171
231
|
case "config-show":
|
|
172
232
|
return (0, show_config_1.showConfig)({
|
|
@@ -188,15 +248,20 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
188
248
|
throw (0, errors_1.cliError)("PROVIDER_NOT_FOUND", "Missing provider name for switch command.");
|
|
189
249
|
}
|
|
190
250
|
if ((0, args_1.hasFlag)(parsed.commandOptions, "--install-copilot-sdk")) {
|
|
191
|
-
throw (0, errors_1.cliError)("INVALID_ARGUMENT", "--install-copilot-sdk is
|
|
251
|
+
throw (0, errors_1.cliError)("INVALID_ARGUMENT", "--install-copilot-sdk is no longer supported with switch. Run `codexs login copilot` instead.", {
|
|
252
|
+
suggestion: "Run `codexs login copilot` first, then rerun switch without --install-copilot-sdk.",
|
|
253
|
+
});
|
|
192
254
|
}
|
|
193
255
|
return (0, switch_provider_1.switchProvider)({
|
|
194
256
|
codexDir: paths.codexDir,
|
|
257
|
+
lockPath: paths.lockPath,
|
|
195
258
|
backupsDir: paths.backupsDir,
|
|
196
259
|
latestBackupPath: paths.latestBackupPath,
|
|
197
260
|
configPath: paths.configPath,
|
|
198
261
|
providersPath: paths.providersPath,
|
|
199
262
|
authPath: paths.authPath,
|
|
263
|
+
runtimeDir: paths.runtimeDir,
|
|
264
|
+
runtimesDir: paths.runtimesDir,
|
|
200
265
|
providerName,
|
|
201
266
|
});
|
|
202
267
|
}
|
|
@@ -220,6 +285,7 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
220
285
|
}
|
|
221
286
|
return (0, import_providers_1.importProviders)({
|
|
222
287
|
codexDir: paths.codexDir,
|
|
288
|
+
lockPath: paths.lockPath,
|
|
223
289
|
backupsDir: paths.backupsDir,
|
|
224
290
|
latestBackupPath: paths.latestBackupPath,
|
|
225
291
|
providersPath: paths.providersPath,
|
|
@@ -257,43 +323,74 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
257
323
|
let tags = parsed.commandOptions.get("--tag") ?? [];
|
|
258
324
|
let createProfile = (0, args_1.hasFlag)(parsed.commandOptions, "--create-profile");
|
|
259
325
|
const copilot = (0, args_1.hasFlag)(parsed.commandOptions, "--copilot");
|
|
260
|
-
|
|
326
|
+
let bridgeHost = (0, args_1.getSingleOption)(parsed.commandOptions, "--bridge-host", false);
|
|
261
327
|
const bridgePortValue = (0, args_1.getSingleOption)(parsed.commandOptions, "--bridge-port", false);
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
328
|
+
let bridgeApiKey = (0, args_1.getSingleOption)(parsed.commandOptions, "--bridge-api-key", false);
|
|
329
|
+
const installCopilotSdk = (0, args_1.hasFlag)(parsed.commandOptions, "--install-copilot-sdk");
|
|
330
|
+
let bridgePort = bridgePortValue ? Number(bridgePortValue) : null;
|
|
265
331
|
if (copilot && apiKey) {
|
|
266
332
|
throw (0, errors_1.cliError)("INVALID_ARGUMENT", "--copilot does not allow --api-key. Use --bridge-api-key for the local bridge secret.");
|
|
267
333
|
}
|
|
334
|
+
if (copilot && installCopilotSdk) {
|
|
335
|
+
throw (0, errors_1.cliError)("INVALID_ARGUMENT", "--install-copilot-sdk is no longer supported with add --copilot. Run `codexs login copilot` instead.", {
|
|
336
|
+
suggestion: "Run `codexs login copilot` first, then rerun add --copilot.",
|
|
337
|
+
});
|
|
338
|
+
}
|
|
268
339
|
if (bridgePortValue && (!Number.isInteger(bridgePort) || bridgePort === null || bridgePort <= 0)) {
|
|
269
340
|
throw (0, errors_1.cliError)("INVALID_ARGUMENT", "--bridge-port must be a positive integer.");
|
|
270
341
|
}
|
|
271
|
-
if (copilot && !installCopilotSdk && (0, interactive_1.canPrompt)(runtime, ctx.options.json) && !(0, copilot_installer_1.probeCopilotSdkInstall)().installed) {
|
|
272
|
-
installCopilotSdk = await runtime.confirmAction("The optional Copilot SDK runtime is not installed. Install it now?");
|
|
273
|
-
}
|
|
274
342
|
if (!providerName || !profile || (!apiKey && !copilot)) {
|
|
275
343
|
if (ctx.options.json || !runtime.isInteractive()) {
|
|
276
|
-
throw (0, add_interactive_1.createNonInteractiveAddError)();
|
|
344
|
+
throw (0, add_interactive_1.createNonInteractiveAddError)({ copilot });
|
|
345
|
+
}
|
|
346
|
+
if (copilot) {
|
|
347
|
+
const prompted = await (0, add_interactive_1.collectCopilotAddInput)(runtime, {
|
|
348
|
+
providerName,
|
|
349
|
+
profile,
|
|
350
|
+
model,
|
|
351
|
+
note,
|
|
352
|
+
tags,
|
|
353
|
+
}, (candidate) => Boolean((0, providers_repo_1.readProvidersFileIfExists)(paths.providersPath).providers[candidate]), (candidate) => Boolean((0, config_repo_1.readStructuredConfig)(paths.configPath).profiles.find((profileView) => profileView.name === candidate)), {
|
|
354
|
+
bridgeHost,
|
|
355
|
+
bridgePort,
|
|
356
|
+
bridgeApiKey,
|
|
357
|
+
});
|
|
358
|
+
providerName = prompted.providerName;
|
|
359
|
+
profile = prompted.profile;
|
|
360
|
+
model = prompted.model ?? null;
|
|
361
|
+
note = prompted.note ?? null;
|
|
362
|
+
tags = prompted.tags;
|
|
363
|
+
createProfile = createProfile || prompted.createProfile;
|
|
364
|
+
baseUrl = null;
|
|
365
|
+
bridgeHost = prompted.bridgeHost ?? bridgeHost;
|
|
366
|
+
bridgePort = prompted.bridgePort ?? bridgePort;
|
|
367
|
+
bridgeApiKey = prompted.bridgeApiKey ?? bridgeApiKey;
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
const prompted = await (0, add_interactive_1.collectAddInput)(runtime, {
|
|
371
|
+
providerName,
|
|
372
|
+
profile,
|
|
373
|
+
apiKey,
|
|
374
|
+
model,
|
|
375
|
+
baseUrl,
|
|
376
|
+
note,
|
|
377
|
+
tags,
|
|
378
|
+
}, (candidate) => Boolean((0, providers_repo_1.readProvidersFileIfExists)(paths.providersPath).providers[candidate]), (candidate) => Boolean((0, config_repo_1.readStructuredConfig)(paths.configPath).profiles.find((profileView) => profileView.name === candidate)));
|
|
379
|
+
providerName = prompted.providerName;
|
|
380
|
+
profile = prompted.profile;
|
|
381
|
+
apiKey = prompted.apiKey;
|
|
382
|
+
model = prompted.model ?? null;
|
|
383
|
+
baseUrl = prompted.baseUrl ?? null;
|
|
384
|
+
note = prompted.note ?? null;
|
|
385
|
+
tags = prompted.tags;
|
|
386
|
+
createProfile = createProfile || prompted.createProfile;
|
|
277
387
|
}
|
|
278
|
-
const prompted = await (0, add_interactive_1.collectAddInput)(runtime, {
|
|
279
|
-
providerName,
|
|
280
|
-
profile,
|
|
281
|
-
apiKey,
|
|
282
|
-
baseUrl,
|
|
283
|
-
note,
|
|
284
|
-
tags,
|
|
285
|
-
}, (candidate) => Boolean((0, providers_repo_1.readProvidersFileIfExists)(paths.providersPath).providers[candidate]), (candidate) => Boolean((0, config_repo_1.readStructuredConfig)(paths.configPath).profiles.find((profileView) => profileView.name === candidate)));
|
|
286
|
-
providerName = prompted.providerName;
|
|
287
|
-
profile = prompted.profile;
|
|
288
|
-
apiKey = prompted.apiKey;
|
|
289
|
-
model = prompted.model ?? null;
|
|
290
|
-
baseUrl = prompted.baseUrl ?? null;
|
|
291
|
-
note = prompted.note ?? null;
|
|
292
|
-
tags = prompted.tags;
|
|
293
|
-
createProfile = createProfile || prompted.createProfile;
|
|
294
388
|
}
|
|
295
389
|
return (0, add_provider_1.addProvider)({
|
|
296
390
|
codexDir: paths.codexDir,
|
|
391
|
+
toolHomeDir: paths.toolHomeDir,
|
|
392
|
+
lockPath: paths.lockPath,
|
|
393
|
+
runtimesDir: paths.runtimesDir,
|
|
297
394
|
backupsDir: paths.backupsDir,
|
|
298
395
|
latestBackupPath: paths.latestBackupPath,
|
|
299
396
|
providersPath: paths.providersPath,
|
|
@@ -311,8 +408,6 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
311
408
|
bridgeHost,
|
|
312
409
|
bridgePort,
|
|
313
410
|
bridgeApiKey,
|
|
314
|
-
installCopilotSdk,
|
|
315
|
-
interactive: (0, interactive_1.canPrompt)(runtime, ctx.options.json),
|
|
316
411
|
});
|
|
317
412
|
}
|
|
318
413
|
case "edit": {
|
|
@@ -355,6 +450,7 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
355
450
|
}
|
|
356
451
|
return (0, edit_provider_1.editProvider)({
|
|
357
452
|
codexDir: paths.codexDir,
|
|
453
|
+
lockPath: paths.lockPath,
|
|
358
454
|
backupsDir: paths.backupsDir,
|
|
359
455
|
latestBackupPath: paths.latestBackupPath,
|
|
360
456
|
providersPath: paths.providersPath,
|
|
@@ -389,6 +485,7 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
389
485
|
}
|
|
390
486
|
return (0, remove_provider_1.removeProvider)({
|
|
391
487
|
codexDir: paths.codexDir,
|
|
488
|
+
lockPath: paths.lockPath,
|
|
392
489
|
backupsDir: paths.backupsDir,
|
|
393
490
|
latestBackupPath: paths.latestBackupPath,
|
|
394
491
|
providersPath: paths.providersPath,
|
|
@@ -403,6 +500,8 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
403
500
|
configPath: paths.configPath,
|
|
404
501
|
providersPath: paths.providersPath,
|
|
405
502
|
authPath: paths.authPath,
|
|
503
|
+
runtimeDir: paths.runtimeDir,
|
|
504
|
+
runtimesDir: paths.runtimesDir,
|
|
406
505
|
});
|
|
407
506
|
case "migrate": {
|
|
408
507
|
let codexDir = ctx.options.codexDir;
|
|
@@ -433,9 +532,25 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
433
532
|
if (overwrite && merge) {
|
|
434
533
|
throw (0, errors_1.cliError)("INVALID_ARGUMENT", "migrate does not allow both --merge and --overwrite.");
|
|
435
534
|
}
|
|
436
|
-
let strategy = overwrite ? "overwrite" : merge ? "merge" : null;
|
|
437
535
|
const providersExists = fs.existsSync(setupPaths.providersPath);
|
|
438
|
-
|
|
536
|
+
const document = (0, config_repo_1.readStructuredConfig)(setupPaths.configPath);
|
|
537
|
+
const currentProviders = providersExists ? (0, providers_1.validateProvidersShape)((0, providers_repo_1.readProvidersFileIfExists)(setupPaths.providersPath)) : null;
|
|
538
|
+
const adoptability = (0, setup_1.collectMigrateAdoptability)(document, currentProviders);
|
|
539
|
+
if (adoptability.availableProfiles.length === 0) {
|
|
540
|
+
throw (0, errors_1.cliError)("PROFILE_NOT_FOUND", "No profiles were found in config.toml.", {
|
|
541
|
+
file: setupPaths.configPath,
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
if (adoptability.adoptableProfiles.length === 0) {
|
|
545
|
+
throw (0, errors_1.cliError)("MIGRATE_NO_ADOPTABLE_PROFILES", "No adoptable profiles were found for migrate.", {
|
|
546
|
+
availableProfiles: adoptability.availableProfiles,
|
|
547
|
+
adoptableProfiles: adoptability.adoptableProfiles,
|
|
548
|
+
blockingReasonsByProfile: adoptability.blockingReasonsByProfile,
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
let strategy = overwrite ? "overwrite" : merge ? "merge" : null;
|
|
552
|
+
const registryIsEmpty = !currentProviders || Object.keys(currentProviders.providers).length === 0;
|
|
553
|
+
if (providersExists && strategy === null && !registryIsEmpty) {
|
|
439
554
|
if (!(0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
|
|
440
555
|
throw (0, errors_1.cliError)("PROVIDERS_ALREADY_EXISTS", "providers.json already exists. Pass --merge or --overwrite.", {
|
|
441
556
|
file: setupPaths.providersPath,
|
|
@@ -447,44 +562,47 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
447
562
|
}
|
|
448
563
|
strategy = selected;
|
|
449
564
|
}
|
|
450
|
-
const
|
|
451
|
-
const adoptableProfiles = (0, config_1.buildManagedProfileViews)(document, null)
|
|
452
|
-
.filter((view) => view.source === "unmanaged" && view.model && view.modelProvider === view.name && view.baseUrl && view.envKey)
|
|
453
|
-
.map((view) => ({
|
|
454
|
-
name: view.name,
|
|
455
|
-
model: view.model,
|
|
456
|
-
baseUrl: view.baseUrl,
|
|
457
|
-
envKey: view.envKey,
|
|
458
|
-
}))
|
|
459
|
-
.sort((left, right) => left.name.localeCompare(right.name));
|
|
460
|
-
const selectedProfiles = Array.from((0, config_repo_1.listConfigProfiles)(setupPaths.configPath)).sort();
|
|
565
|
+
const adoptableProfiles = adoptability.adoptableProfileDetails;
|
|
461
566
|
let adoptProfiles = [];
|
|
462
567
|
let providerDetailsByProfile = {};
|
|
463
568
|
if ((0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
|
|
464
569
|
adoptProfiles = await (0, interactive_1.chooseSetupProfiles)(runtime, adoptableProfiles);
|
|
465
570
|
// Defaults are derived from config.toml so interactive setup only asks for missing provider metadata.
|
|
466
|
-
|
|
571
|
+
const collectedDetails = await (0, interactive_1.collectSetupProviderDetails)(runtime, adoptProfiles, adoptableProfiles.reduce((accumulator, profile) => {
|
|
467
572
|
accumulator[profile.name] = {
|
|
468
573
|
providerName: profile.name,
|
|
469
|
-
envKey: profile.envKey,
|
|
470
574
|
baseUrl: profile.baseUrl,
|
|
471
575
|
};
|
|
472
576
|
return accumulator;
|
|
473
577
|
}, {}));
|
|
578
|
+
providerDetailsByProfile = Object.fromEntries(Object.entries(collectedDetails).map(([profile, detail]) => [
|
|
579
|
+
profile,
|
|
580
|
+
{
|
|
581
|
+
providerName: detail.providerName,
|
|
582
|
+
apiKey: detail.apiKey,
|
|
583
|
+
baseUrl: detail.baseUrl,
|
|
584
|
+
note: detail.note,
|
|
585
|
+
tags: detail.tags,
|
|
586
|
+
},
|
|
587
|
+
]));
|
|
474
588
|
}
|
|
475
589
|
else {
|
|
476
590
|
throw (0, errors_1.cliError)("INVALID_ARGUMENT", "migrate currently requires an interactive TTY to choose adoptable profiles and collect provider details.", {
|
|
477
|
-
|
|
478
|
-
|
|
591
|
+
availableProfiles: adoptability.availableProfiles,
|
|
592
|
+
adoptableProfiles: adoptability.adoptableProfiles,
|
|
593
|
+
blockingReasonsByProfile: adoptability.blockingReasonsByProfile,
|
|
479
594
|
suggestion: "Run `codexs migrate` in an interactive terminal. Non-interactive migrate flags for profile selection and provider secrets are not available in this release.",
|
|
480
595
|
});
|
|
481
596
|
}
|
|
482
597
|
return (0, setup_codex_1.migrateCodex)({
|
|
483
598
|
codexDirOption: ctx.options.codexDir,
|
|
484
599
|
codexDir: setupPaths.codexDir,
|
|
600
|
+
lockPath: setupPaths.lockPath,
|
|
485
601
|
configPath: setupPaths.configPath,
|
|
486
602
|
providersPath: setupPaths.providersPath,
|
|
487
603
|
authPath: setupPaths.authPath,
|
|
604
|
+
runtimeDir: setupPaths.runtimeDir,
|
|
605
|
+
runtimesDir: setupPaths.runtimesDir,
|
|
488
606
|
backupsDir: setupPaths.backupsDir,
|
|
489
607
|
latestBackupPath: setupPaths.latestBackupPath,
|
|
490
608
|
strategy: strategy ?? "overwrite",
|
package/dist/commands/help.js
CHANGED
|
@@ -44,6 +44,7 @@ function buildHelpText(commandName) {
|
|
|
44
44
|
" --version Print the current CLI version.",
|
|
45
45
|
"",
|
|
46
46
|
"Environment:",
|
|
47
|
+
" CODEXS_HOME Override the codex-switch tool home directory.",
|
|
47
48
|
" CODEXS_CODEX_DIR Default Codex directory when --codex-dir is not passed.",
|
|
48
49
|
" NODE_ENV=development defaults to ./dev-codex/local-sandbox when no override is set.",
|
|
49
50
|
"",
|
|
@@ -60,6 +61,7 @@ function buildHelpText(commandName) {
|
|
|
60
61
|
"",
|
|
61
62
|
"Examples:",
|
|
62
63
|
" codexs init",
|
|
64
|
+
" codexs login copilot",
|
|
63
65
|
" codexs migrate",
|
|
64
66
|
" codexs list",
|
|
65
67
|
" codexs switch",
|
|
@@ -64,7 +64,7 @@ exports.COMMANDS = [
|
|
|
64
64
|
usage: ["codexs bridge stop [provider] [--json] [--codex-dir <path>]"],
|
|
65
65
|
details: [
|
|
66
66
|
"Prefers the runtime-state instance when present and uses an explicit provider as a guard.",
|
|
67
|
-
"Clears the runtime-state manifest without mutating providers.json or auth.
|
|
67
|
+
"Clears the runtime-state manifest without mutating providers.json or Codex auth state.",
|
|
68
68
|
"Is idempotent when no managed bridge is currently running.",
|
|
69
69
|
],
|
|
70
70
|
examples: ["codexs bridge stop", "codexs bridge stop copilot-main"],
|
|
@@ -88,16 +88,32 @@ exports.COMMANDS = [
|
|
|
88
88
|
tokens: ["init"],
|
|
89
89
|
handler: handlers_1.handleRegisteredCommand,
|
|
90
90
|
group: "write",
|
|
91
|
-
summary: "Initialize
|
|
91
|
+
summary: "Initialize the codex-switch tool home and registry files.",
|
|
92
92
|
usage: ["codexs init [--json] [--codex-dir <path>]"],
|
|
93
93
|
details: [
|
|
94
|
-
"Creates providers.json
|
|
95
|
-
"Does not
|
|
96
|
-
"
|
|
97
|
-
"
|
|
94
|
+
"Creates codex-switch.json and providers.json under the tool home when they do not exist yet.",
|
|
95
|
+
"Does not create or validate config.toml, auth.json, or the target Codex directory.",
|
|
96
|
+
"When --codex-dir is passed explicitly and codex-switch.json does not exist yet, init persists it as defaultCodexDir.",
|
|
97
|
+
"Otherwise init stays scoped to tool-home state and does not persist fallback Codex directory resolution.",
|
|
98
98
|
],
|
|
99
99
|
examples: ["codexs init", "codexs init --json --codex-dir ~/.codex"],
|
|
100
100
|
},
|
|
101
|
+
{
|
|
102
|
+
id: "login",
|
|
103
|
+
tokens: ["login"],
|
|
104
|
+
handler: handlers_1.handleRegisteredCommand,
|
|
105
|
+
group: "write",
|
|
106
|
+
summary: "Complete upstream onboarding for interactive providers such as GitHub Copilot.",
|
|
107
|
+
usage: ["codexs login <upstream>"],
|
|
108
|
+
details: [
|
|
109
|
+
"Currently supports copilot and github-copilot as the same upstream.",
|
|
110
|
+
"Installs the local Copilot SDK under the tool home when needed, then checks login readiness.",
|
|
111
|
+
"When login is not ready, launches the bundled Copilot CLI from the runtime when available, otherwise falls back to PATH, then rechecks before succeeding.",
|
|
112
|
+
"Copilot login is shared across the local Copilot runtime, so logging into a different GitHub account replaces the upstream auth used by all Copilot providers.",
|
|
113
|
+
"Requires an interactive TTY and does not support --json.",
|
|
114
|
+
],
|
|
115
|
+
examples: ["codexs login copilot", "codexs login github-copilot"],
|
|
116
|
+
},
|
|
101
117
|
{
|
|
102
118
|
id: "migrate",
|
|
103
119
|
tokens: ["migrate"],
|
|
@@ -108,7 +124,7 @@ exports.COMMANDS = [
|
|
|
108
124
|
details: [
|
|
109
125
|
"Reads config.toml profiles, collects complete provider records, then writes providers.json under managed backup flow.",
|
|
110
126
|
"TTY mode can collect missing provider details and choose merge or overwrite when providers.json already exists.",
|
|
111
|
-
"Migrate adopts only runtime profiles that already expose model, model_provider, matching base_url
|
|
127
|
+
"Migrate adopts only runtime profiles that already expose model, model_provider, and matching base_url.",
|
|
112
128
|
"Non-TTY and --json runs still fail fast because migrate profile selection and provider details remain interactive in this release.",
|
|
113
129
|
],
|
|
114
130
|
examples: ["codexs migrate", "codexs migrate --overwrite --json --codex-dir ~/.codex"],
|
|
@@ -213,12 +229,16 @@ exports.COMMANDS = [
|
|
|
213
229
|
"Confirm API key when prompted interactively because the hidden prompt asks twice before writing.",
|
|
214
230
|
"Interactive tags use preset multi-select only.",
|
|
215
231
|
"Automation and non-TTY environments must pass all required values explicitly.",
|
|
216
|
-
"Creating a missing profile section requires --create-profile together with --model and --base-url.",
|
|
232
|
+
"Creating a missing direct-provider profile section requires --create-profile together with --model and --base-url.",
|
|
233
|
+
"Creating a missing Copilot profile section requires --create-profile together with --model; the local bridge base_url is derived automatically.",
|
|
217
234
|
"Use --copilot to create a GitHub Copilot bridge provider backed by the official SDK.",
|
|
235
|
+
"Copilot providers require SDK install and login readiness to already be satisfied via codexs login copilot.",
|
|
236
|
+
"For Copilot providers, provider apiKey stores only the local bridge secret; upstream GitHub Copilot auth stays shared in the official runtime login.",
|
|
237
|
+
"--install-copilot-sdk is kept only as a rejected compatibility flag that points to codexs login copilot.",
|
|
218
238
|
],
|
|
219
239
|
examples: [
|
|
220
240
|
"codexs add packycode --profile packycode --api-key sk-xxx",
|
|
221
|
-
"codexs add copilot-main --copilot --profile copilot-main
|
|
241
|
+
"codexs add copilot-main --copilot --profile copilot-main",
|
|
222
242
|
"codexs add",
|
|
223
243
|
],
|
|
224
244
|
},
|
|
@@ -227,14 +247,15 @@ exports.COMMANDS = [
|
|
|
227
247
|
tokens: ["switch"],
|
|
228
248
|
handler: handlers_1.handleRegisteredCommand,
|
|
229
249
|
group: "write",
|
|
230
|
-
summary: "Switch
|
|
250
|
+
summary: "Switch the active config profile to a provider.",
|
|
231
251
|
usage: ["codexs switch <provider> [--json] [--codex-dir <path>]"],
|
|
232
252
|
details: [
|
|
233
253
|
"When <provider> is omitted in a TTY, an interactive provider selector is shown.",
|
|
234
254
|
"When <provider> is passed explicitly, switch proceeds directly without extra confirmation.",
|
|
235
|
-
"
|
|
255
|
+
"Direct providers update the active config profile and rewrite auth.json with auth_mode=apikey plus OPENAI_API_KEY.",
|
|
256
|
+
"Copilot bridge providers also rewrite OPENAI_API_KEY to the local bridge secret while managing runtime routing and bridge state.",
|
|
236
257
|
"Copilot bridge providers probe the optional official SDK before switching and fail fast if it is missing.",
|
|
237
|
-
"Backs up config.toml and auth.json
|
|
258
|
+
"Backs up config.toml and auth.json and rolls back on failure.",
|
|
238
259
|
],
|
|
239
260
|
examples: ["codexs switch freemodel", "codexs switch packycode --json"],
|
|
240
261
|
},
|
package/dist/domain/backups.js
CHANGED
|
@@ -61,9 +61,6 @@ function validateBackupManifest(input) {
|
|
|
61
61
|
if (typeof manifest.reason !== "string" || manifest.reason.trim() === "") {
|
|
62
62
|
throw new Error("Backup manifest is missing reason.");
|
|
63
63
|
}
|
|
64
|
-
if (typeof manifest.rootDir !== "string" || manifest.rootDir.trim() === "") {
|
|
65
|
-
throw new Error("Backup manifest is missing rootDir.");
|
|
66
|
-
}
|
|
67
64
|
if (typeof manifest.backupDir !== "string" || manifest.backupDir.trim() === "") {
|
|
68
65
|
throw new Error("Backup manifest is missing backupDir.");
|
|
69
66
|
}
|
|
@@ -74,7 +71,10 @@ function validateBackupManifest(input) {
|
|
|
74
71
|
if (!entry || typeof entry !== "object") {
|
|
75
72
|
throw new Error("Backup manifest contains an invalid file entry.");
|
|
76
73
|
}
|
|
77
|
-
if (typeof entry.relativePath !== "string" ||
|
|
74
|
+
if (typeof entry.relativePath !== "string" ||
|
|
75
|
+
typeof entry.restorePath !== "string" ||
|
|
76
|
+
entry.restorePath.trim() === "" ||
|
|
77
|
+
typeof entry.existed !== "boolean") {
|
|
78
78
|
throw new Error("Backup manifest contains an invalid file entry.");
|
|
79
79
|
}
|
|
80
80
|
if (entry.backupFileName !== null && typeof entry.backupFileName !== "string") {
|