@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.
- package/README.AI.md +68 -73
- package/README.CN.md +108 -111
- package/README.md +87 -80
- package/dist/app/add-provider.js +29 -15
- package/dist/app/bridge.js +15 -14
- package/dist/app/edit-provider.js +1 -1
- package/dist/app/get-status.js +21 -9
- package/dist/app/import-providers.js +1 -1
- package/dist/app/init-codex.js +13 -14
- package/dist/app/list-providers.js +48 -1
- package/dist/app/remove-provider.js +1 -1
- package/dist/app/run-doctor.js +12 -5
- package/dist/app/run-mutation.js +3 -2
- package/dist/app/setup-codex.js +3 -1
- package/dist/app/switch-provider.js +11 -13
- package/dist/cli/output.js +145 -18
- package/dist/cli.js +34 -2
- package/dist/commands/args.js +2 -2
- package/dist/commands/dispatch.js +40 -0
- package/dist/commands/handlers.js +130 -161
- package/dist/commands/help.js +11 -5
- package/dist/commands/registry.js +42 -20
- package/dist/domain/backups.js +4 -4
- package/dist/domain/config.js +110 -5
- package/dist/domain/providers.js +12 -0
- package/dist/domain/runtime-state.js +111 -13
- package/dist/infra/config-repo.js +16 -206
- package/dist/interaction/interactive.js +16 -6
- package/dist/runtime/copilot-adapter.js +12 -12
- package/dist/runtime/copilot-bridge.js +394 -45
- package/dist/runtime/copilot-cli.js +84 -12
- package/dist/runtime/copilot-installer.js +10 -9
- package/dist/runtime/copilot-sdk-loader.js +5 -5
- package/dist/storage/backup-repo.js +4 -4
- package/dist/storage/codex-paths.js +34 -8
- package/dist/storage/config-repo.js +0 -23
- package/dist/storage/lock-repo.js +2 -4
- package/dist/storage/runtime-state-repo.js +14 -13
- package/dist/storage/tool-config-repo.js +111 -0
- package/docs/Design/codex-switch-v0.0.11-design.md +824 -0
- package/docs/Design/codex-switch-v0.0.12-design.md +343 -0
- package/docs/PRD/codex-switch-prd-v0.0.11.md +577 -0
- package/docs/PRD/codex-switch-prd-v0.0.12.md +279 -0
- package/docs/PRD/codex-switch-prd-v0.1.0.md +125 -237
- package/docs/Tests/testing.md +39 -112
- package/docs/cli-usage.md +135 -565
- package/docs/codex-switch-command-design.md +3 -0
- package/docs/codex-switch-product-overview.md +52 -207
- package/docs/codex-switch-technical-architecture.md +3 -0
- package/package.json +1 -1
- package/dist/app/rollback-latest.js +0 -26
|
@@ -71,15 +71,19 @@ const args_1 = require("./args");
|
|
|
71
71
|
* Executes one command handler selected from the shared command registry.
|
|
72
72
|
*/
|
|
73
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
|
+
}
|
|
74
78
|
let setupPaths = (0, codex_paths_1.createCodexPaths)(ctx.options.codexDir);
|
|
75
79
|
const paths = setupPaths;
|
|
76
80
|
switch (ctx.command) {
|
|
77
81
|
case "list":
|
|
78
|
-
return (0, list_providers_1.listProviders)(paths.providersPath);
|
|
82
|
+
return (0, list_providers_1.listProviders)(paths.providersPath, paths.configPath);
|
|
79
83
|
case "show": {
|
|
80
84
|
let providerName = parsed.positionals[0] ?? null;
|
|
81
85
|
if (!providerName && (0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
|
|
82
|
-
providerName = await (0, interactive_1.promptForProviderSelection)(runtime, paths.providersPath, "Choose a provider to show");
|
|
86
|
+
providerName = await (0, interactive_1.promptForProviderSelection)(runtime, paths.providersPath, paths.configPath, "Choose a provider to show");
|
|
83
87
|
}
|
|
84
88
|
if (!providerName) {
|
|
85
89
|
throw (0, errors_1.cliError)("INVALID_ARGUMENT", "Missing provider name for show command.");
|
|
@@ -93,12 +97,17 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
93
97
|
case "current":
|
|
94
98
|
return (0, get_current_profile_1.getCurrentProfile)(paths.configPath);
|
|
95
99
|
case "status":
|
|
96
|
-
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
|
+
});
|
|
97
104
|
case "bridge-start": {
|
|
98
105
|
const providerName = parsed.positionals[0] ?? null;
|
|
99
106
|
return (0, bridge_1.startBridge)({
|
|
100
107
|
providersPath: paths.providersPath,
|
|
101
108
|
configPath: paths.configPath,
|
|
109
|
+
runtimeDir: paths.runtimeDir,
|
|
110
|
+
runtimesDir: paths.runtimesDir,
|
|
102
111
|
providerName,
|
|
103
112
|
runtime,
|
|
104
113
|
json: ctx.options.json,
|
|
@@ -109,6 +118,8 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
109
118
|
return (0, bridge_1.stopBridge)({
|
|
110
119
|
providersPath: paths.providersPath,
|
|
111
120
|
configPath: paths.configPath,
|
|
121
|
+
runtimeDir: paths.runtimeDir,
|
|
122
|
+
runtimesDir: paths.runtimesDir,
|
|
112
123
|
providerName,
|
|
113
124
|
runtime,
|
|
114
125
|
json: ctx.options.json,
|
|
@@ -119,57 +130,107 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
119
130
|
return (0, bridge_1.statusBridge)({
|
|
120
131
|
providersPath: paths.providersPath,
|
|
121
132
|
configPath: paths.configPath,
|
|
133
|
+
runtimeDir: paths.runtimeDir,
|
|
134
|
+
runtimesDir: paths.runtimesDir,
|
|
122
135
|
providerName,
|
|
123
136
|
runtime,
|
|
124
137
|
json: ctx.options.json,
|
|
125
138
|
});
|
|
126
139
|
}
|
|
127
140
|
case "init": {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
codexDir = await (0, interactive_1.chooseCodexDir)(runtime, candidates);
|
|
146
|
-
}
|
|
147
|
-
else {
|
|
148
|
-
codexDir = candidates[0];
|
|
149
|
-
}
|
|
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.");
|
|
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
|
+
});
|
|
150
158
|
}
|
|
151
|
-
|
|
152
|
-
let
|
|
153
|
-
if (!
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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,
|
|
157
169
|
});
|
|
158
170
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
171
|
+
runtime.writeLine("Installing Copilot SDK runtime...");
|
|
172
|
+
(0, copilot_installer_1.installCopilotSdk)(paths.runtimesDir);
|
|
173
|
+
installedNow = true;
|
|
174
|
+
}
|
|
175
|
+
const availability = (0, copilot_cli_1.checkCopilotCliAvailable)(paths.runtimesDir);
|
|
176
|
+
try {
|
|
177
|
+
await (0, copilot_adapter_1.readCopilotAuthState)(paths.runtimesDir);
|
|
178
|
+
return {
|
|
179
|
+
data: {
|
|
180
|
+
upstream: "github-copilot",
|
|
181
|
+
sdkInstalled: true,
|
|
182
|
+
sdkInstalledNow: installedNow,
|
|
183
|
+
authReady: true,
|
|
184
|
+
loginLaunched: false,
|
|
185
|
+
cliSource: availability.ok ? availability.source ?? null : null,
|
|
186
|
+
cliCommand: availability.command ?? null,
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
const normalized = (0, errors_1.normalizeError)(error);
|
|
192
|
+
if (normalized.code !== "COPILOT_AUTH_REQUIRED") {
|
|
193
|
+
throw error;
|
|
164
194
|
}
|
|
165
195
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
196
|
+
if (!availability.ok) {
|
|
197
|
+
throw (0, errors_1.cliError)("COPILOT_CLI_MISSING", "The official Copilot CLI could not be resolved from the installed runtime or PATH.", {
|
|
198
|
+
cause: availability.cause,
|
|
199
|
+
source: availability.source ?? null,
|
|
200
|
+
command: availability.command ?? null,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
(0, copilot_cli_1.runCopilotLogin)({ runtimesDir: paths.runtimesDir });
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
throw (0, errors_1.cliError)("COPILOT_LOGIN_LAUNCH_FAILED", "Failed to launch `copilot login`.", {
|
|
208
|
+
cause: error instanceof Error ? error.message : String(error),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
await (0, copilot_adapter_1.readCopilotAuthState)(paths.runtimesDir);
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
const normalized = (0, errors_1.normalizeError)(error);
|
|
216
|
+
if (normalized.code === "COPILOT_AUTH_REQUIRED") {
|
|
217
|
+
throw (0, errors_1.cliError)("COPILOT_LOGIN_RECHECK_FAILED", "Copilot login completed but auth readiness recheck still failed.", {
|
|
218
|
+
...(normalized.details ?? {}),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
data: {
|
|
225
|
+
upstream: "github-copilot",
|
|
226
|
+
sdkInstalled: true,
|
|
227
|
+
sdkInstalledNow: installedNow,
|
|
228
|
+
authReady: true,
|
|
229
|
+
loginLaunched: true,
|
|
230
|
+
cliSource: availability.source ?? null,
|
|
231
|
+
cliCommand: availability.command ?? null,
|
|
232
|
+
},
|
|
233
|
+
};
|
|
173
234
|
}
|
|
174
235
|
case "config-show":
|
|
175
236
|
return (0, show_config_1.showConfig)({
|
|
@@ -185,21 +246,26 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
185
246
|
case "switch": {
|
|
186
247
|
let providerName = parsed.positionals[0] ?? null;
|
|
187
248
|
if (!providerName && (0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
|
|
188
|
-
providerName = await (0, interactive_1.promptForProviderSelection)(runtime, paths.providersPath, "Choose a provider to switch to");
|
|
249
|
+
providerName = await (0, interactive_1.promptForProviderSelection)(runtime, paths.providersPath, paths.configPath, "Choose a provider to switch to");
|
|
189
250
|
}
|
|
190
251
|
if (!providerName) {
|
|
191
252
|
throw (0, errors_1.cliError)("PROVIDER_NOT_FOUND", "Missing provider name for switch command.");
|
|
192
253
|
}
|
|
193
254
|
if ((0, args_1.hasFlag)(parsed.commandOptions, "--install-copilot-sdk")) {
|
|
194
|
-
throw (0, errors_1.cliError)("INVALID_ARGUMENT", "--install-copilot-sdk is
|
|
255
|
+
throw (0, errors_1.cliError)("INVALID_ARGUMENT", "--install-copilot-sdk is no longer supported with switch. Run `codexs login copilot` instead.", {
|
|
256
|
+
suggestion: "Run `codexs login copilot` first, then rerun switch without --install-copilot-sdk.",
|
|
257
|
+
});
|
|
195
258
|
}
|
|
196
259
|
return (0, switch_provider_1.switchProvider)({
|
|
197
260
|
codexDir: paths.codexDir,
|
|
261
|
+
lockPath: paths.lockPath,
|
|
198
262
|
backupsDir: paths.backupsDir,
|
|
199
263
|
latestBackupPath: paths.latestBackupPath,
|
|
200
264
|
configPath: paths.configPath,
|
|
201
265
|
providersPath: paths.providersPath,
|
|
202
266
|
authPath: paths.authPath,
|
|
267
|
+
runtimeDir: paths.runtimeDir,
|
|
268
|
+
runtimesDir: paths.runtimesDir,
|
|
203
269
|
providerName,
|
|
204
270
|
});
|
|
205
271
|
}
|
|
@@ -223,6 +289,7 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
223
289
|
}
|
|
224
290
|
return (0, import_providers_1.importProviders)({
|
|
225
291
|
codexDir: paths.codexDir,
|
|
292
|
+
lockPath: paths.lockPath,
|
|
226
293
|
backupsDir: paths.backupsDir,
|
|
227
294
|
latestBackupPath: paths.latestBackupPath,
|
|
228
295
|
providersPath: paths.providersPath,
|
|
@@ -263,21 +330,19 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
263
330
|
let bridgeHost = (0, args_1.getSingleOption)(parsed.commandOptions, "--bridge-host", false);
|
|
264
331
|
const bridgePortValue = (0, args_1.getSingleOption)(parsed.commandOptions, "--bridge-port", false);
|
|
265
332
|
let bridgeApiKey = (0, args_1.getSingleOption)(parsed.commandOptions, "--bridge-api-key", false);
|
|
266
|
-
|
|
333
|
+
const installCopilotSdk = (0, args_1.hasFlag)(parsed.commandOptions, "--install-copilot-sdk");
|
|
267
334
|
let bridgePort = bridgePortValue ? Number(bridgePortValue) : null;
|
|
268
335
|
if (copilot && apiKey) {
|
|
269
336
|
throw (0, errors_1.cliError)("INVALID_ARGUMENT", "--copilot does not allow --api-key. Use --bridge-api-key for the local bridge secret.");
|
|
270
337
|
}
|
|
338
|
+
if (copilot && installCopilotSdk) {
|
|
339
|
+
throw (0, errors_1.cliError)("INVALID_ARGUMENT", "--install-copilot-sdk is no longer supported with add --copilot. Run `codexs login copilot` instead.", {
|
|
340
|
+
suggestion: "Run `codexs login copilot` first, then rerun add --copilot.",
|
|
341
|
+
});
|
|
342
|
+
}
|
|
271
343
|
if (bridgePortValue && (!Number.isInteger(bridgePort) || bridgePort === null || bridgePort <= 0)) {
|
|
272
344
|
throw (0, errors_1.cliError)("INVALID_ARGUMENT", "--bridge-port must be a positive integer.");
|
|
273
345
|
}
|
|
274
|
-
if (copilot) {
|
|
275
|
-
installCopilotSdk = await ensureCopilotReadyForAdd({
|
|
276
|
-
runtime,
|
|
277
|
-
json: ctx.options.json,
|
|
278
|
-
installCopilotSdk,
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
346
|
if (!providerName || !profile || (!apiKey && !copilot)) {
|
|
282
347
|
if (ctx.options.json || !runtime.isInteractive()) {
|
|
283
348
|
throw (0, add_interactive_1.createNonInteractiveAddError)({ copilot });
|
|
@@ -327,6 +392,9 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
327
392
|
}
|
|
328
393
|
return (0, add_provider_1.addProvider)({
|
|
329
394
|
codexDir: paths.codexDir,
|
|
395
|
+
toolHomeDir: paths.toolHomeDir,
|
|
396
|
+
lockPath: paths.lockPath,
|
|
397
|
+
runtimesDir: paths.runtimesDir,
|
|
330
398
|
backupsDir: paths.backupsDir,
|
|
331
399
|
latestBackupPath: paths.latestBackupPath,
|
|
332
400
|
providersPath: paths.providersPath,
|
|
@@ -344,14 +412,12 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
344
412
|
bridgeHost,
|
|
345
413
|
bridgePort,
|
|
346
414
|
bridgeApiKey,
|
|
347
|
-
installCopilotSdk,
|
|
348
|
-
interactive: (0, interactive_1.canPrompt)(runtime, ctx.options.json),
|
|
349
415
|
});
|
|
350
416
|
}
|
|
351
417
|
case "edit": {
|
|
352
418
|
let providerName = parsed.positionals[0] ?? null;
|
|
353
419
|
if (!providerName && (0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
|
|
354
|
-
providerName = await (0, interactive_1.promptForProviderSelection)(runtime, paths.providersPath, "Choose a provider to edit");
|
|
420
|
+
providerName = await (0, interactive_1.promptForProviderSelection)(runtime, paths.providersPath, paths.configPath, "Choose a provider to edit");
|
|
355
421
|
}
|
|
356
422
|
if (!providerName) {
|
|
357
423
|
throw (0, errors_1.cliError)("INVALID_ARGUMENT", "Missing provider name for edit command.");
|
|
@@ -388,6 +454,7 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
388
454
|
}
|
|
389
455
|
return (0, edit_provider_1.editProvider)({
|
|
390
456
|
codexDir: paths.codexDir,
|
|
457
|
+
lockPath: paths.lockPath,
|
|
391
458
|
backupsDir: paths.backupsDir,
|
|
392
459
|
latestBackupPath: paths.latestBackupPath,
|
|
393
460
|
providersPath: paths.providersPath,
|
|
@@ -409,7 +476,7 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
409
476
|
const force = (0, args_1.hasFlag)(parsed.commandOptions, "--force");
|
|
410
477
|
const switchToProfile = (0, args_1.getSingleOption)(parsed.commandOptions, "--switch-to", false) ?? undefined;
|
|
411
478
|
if (!providerName && (0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
|
|
412
|
-
providerName = await (0, interactive_1.promptForProviderSelection)(runtime, paths.providersPath, "Choose a provider to remove");
|
|
479
|
+
providerName = await (0, interactive_1.promptForProviderSelection)(runtime, paths.providersPath, paths.configPath, "Choose a provider to remove");
|
|
413
480
|
}
|
|
414
481
|
if (!providerName) {
|
|
415
482
|
throw (0, errors_1.cliError)("PROVIDER_NOT_FOUND", "Missing provider name for remove command.");
|
|
@@ -422,6 +489,7 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
422
489
|
}
|
|
423
490
|
return (0, remove_provider_1.removeProvider)({
|
|
424
491
|
codexDir: paths.codexDir,
|
|
492
|
+
lockPath: paths.lockPath,
|
|
425
493
|
backupsDir: paths.backupsDir,
|
|
426
494
|
latestBackupPath: paths.latestBackupPath,
|
|
427
495
|
providersPath: paths.providersPath,
|
|
@@ -436,6 +504,8 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
436
504
|
configPath: paths.configPath,
|
|
437
505
|
providersPath: paths.providersPath,
|
|
438
506
|
authPath: paths.authPath,
|
|
507
|
+
runtimeDir: paths.runtimeDir,
|
|
508
|
+
runtimesDir: paths.runtimesDir,
|
|
439
509
|
});
|
|
440
510
|
case "migrate": {
|
|
441
511
|
let codexDir = ctx.options.codexDir;
|
|
@@ -531,9 +601,12 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
531
601
|
return (0, setup_codex_1.migrateCodex)({
|
|
532
602
|
codexDirOption: ctx.options.codexDir,
|
|
533
603
|
codexDir: setupPaths.codexDir,
|
|
604
|
+
lockPath: setupPaths.lockPath,
|
|
534
605
|
configPath: setupPaths.configPath,
|
|
535
606
|
providersPath: setupPaths.providersPath,
|
|
536
607
|
authPath: setupPaths.authPath,
|
|
608
|
+
runtimeDir: setupPaths.runtimeDir,
|
|
609
|
+
runtimesDir: setupPaths.runtimesDir,
|
|
537
610
|
backupsDir: setupPaths.backupsDir,
|
|
538
611
|
latestBackupPath: setupPaths.latestBackupPath,
|
|
539
612
|
strategy: strategy ?? "overwrite",
|
|
@@ -563,107 +636,3 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
563
636
|
throw (0, errors_1.cliError)("UNKNOWN_COMMAND", `Unknown command: ${ctx.command}`);
|
|
564
637
|
}
|
|
565
638
|
}
|
|
566
|
-
/**
|
|
567
|
-
* Runs the deterministic Copilot onboarding preflight before any provider persistence.
|
|
568
|
-
*/
|
|
569
|
-
async function ensureCopilotReadyForAdd(args) {
|
|
570
|
-
let installCopilotSdk = args.installCopilotSdk;
|
|
571
|
-
const interactive = (0, interactive_1.canPrompt)(args.runtime, args.json);
|
|
572
|
-
if (interactive) {
|
|
573
|
-
args.runtime.writeLine("Checking Copilot SDK runtime...");
|
|
574
|
-
}
|
|
575
|
-
if (!(0, copilot_installer_1.probeCopilotSdkInstall)().installed) {
|
|
576
|
-
if (!interactive) {
|
|
577
|
-
if (!installCopilotSdk) {
|
|
578
|
-
const installStatus = (0, copilot_installer_1.probeCopilotSdkInstall)();
|
|
579
|
-
throw (0, errors_1.cliError)("COPILOT_SDK_INSTALL_REQUIRES_TTY", "The optional Copilot SDK runtime is not installed. Pass --install-copilot-sdk when running non-interactively.", {
|
|
580
|
-
installDir: installStatus.installDir,
|
|
581
|
-
packageName: installStatus.packageName,
|
|
582
|
-
});
|
|
583
|
-
}
|
|
584
|
-
(0, copilot_installer_1.installCopilotSdk)();
|
|
585
|
-
}
|
|
586
|
-
else {
|
|
587
|
-
if (!installCopilotSdk) {
|
|
588
|
-
installCopilotSdk = await args.runtime.confirmAction("The optional Copilot SDK runtime is not installed. Install it now?");
|
|
589
|
-
}
|
|
590
|
-
if (!installCopilotSdk) {
|
|
591
|
-
const installStatus = (0, copilot_installer_1.probeCopilotSdkInstall)();
|
|
592
|
-
throw (0, errors_1.cliError)("COPILOT_SDK_MISSING", "The optional Copilot SDK runtime is not installed. Re-run with --install-copilot-sdk or confirm installation interactively.", {
|
|
593
|
-
installDir: installStatus.installDir,
|
|
594
|
-
packageName: installStatus.packageName,
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
if (interactive) {
|
|
598
|
-
args.runtime.writeLine("Installing Copilot SDK runtime...");
|
|
599
|
-
}
|
|
600
|
-
(0, copilot_installer_1.installCopilotSdk)();
|
|
601
|
-
if (interactive) {
|
|
602
|
-
args.runtime.writeLine("Copilot SDK runtime installed.");
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
if (interactive) {
|
|
607
|
-
args.runtime.writeLine("Checking GitHub Copilot login...");
|
|
608
|
-
}
|
|
609
|
-
try {
|
|
610
|
-
await (0, copilot_adapter_1.readCopilotAuthState)();
|
|
611
|
-
return installCopilotSdk;
|
|
612
|
-
}
|
|
613
|
-
catch (error) {
|
|
614
|
-
const normalized = (0, errors_1.normalizeError)(error);
|
|
615
|
-
if (normalized.code !== "COPILOT_AUTH_REQUIRED") {
|
|
616
|
-
throw error;
|
|
617
|
-
}
|
|
618
|
-
if (!interactive) {
|
|
619
|
-
throw (0, errors_1.cliError)("COPILOT_AUTH_REQUIRED", "Copilot authentication is required before the local bridge can be added.", {
|
|
620
|
-
...(normalized.details ?? {}),
|
|
621
|
-
manualLoginCommand: "copilot login",
|
|
622
|
-
suggestion: "Run `copilot login` manually or provide supported Copilot SDK credentials, then rerun add --copilot.",
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
|
-
args.runtime.writeLine("GitHub Copilot login is required. Starting official copilot login...");
|
|
626
|
-
let loginLaunchCause;
|
|
627
|
-
try {
|
|
628
|
-
const availability = (0, copilot_cli_1.checkCopilotCliAvailable)();
|
|
629
|
-
if (!availability.ok) {
|
|
630
|
-
throw new Error(availability.cause ?? "copilot CLI is unavailable");
|
|
631
|
-
}
|
|
632
|
-
(0, copilot_cli_1.runCopilotLogin)();
|
|
633
|
-
}
|
|
634
|
-
catch (launchError) {
|
|
635
|
-
loginLaunchCause = launchError instanceof Error ? launchError.message : String(launchError);
|
|
636
|
-
args.runtime.writeLine("Unable to launch the official Copilot login automatically.");
|
|
637
|
-
args.runtime.writeLine("Run this command in the current terminal: copilot login");
|
|
638
|
-
args.runtime.writeLine("GitHub's official device flow should open the browser or show the verification URL and code.");
|
|
639
|
-
}
|
|
640
|
-
args.runtime.writeLine("GitHub Copilot login completed. Rechecking session...");
|
|
641
|
-
const retry = await args.runtime.confirmAction("Recheck GitHub Copilot login now?", {
|
|
642
|
-
defaultValue: true,
|
|
643
|
-
});
|
|
644
|
-
if (!retry) {
|
|
645
|
-
throw (0, errors_1.cliError)("COPILOT_AUTH_REQUIRED", "Copilot authentication is required before the local bridge can be added.", {
|
|
646
|
-
...(normalized.details ?? {}),
|
|
647
|
-
manualLoginCommand: "copilot login",
|
|
648
|
-
loginLaunchCause,
|
|
649
|
-
suggestion: "Complete GitHub Copilot login with the official tooling, then rerun add --copilot.",
|
|
650
|
-
});
|
|
651
|
-
}
|
|
652
|
-
try {
|
|
653
|
-
await (0, copilot_adapter_1.readCopilotAuthState)();
|
|
654
|
-
return installCopilotSdk;
|
|
655
|
-
}
|
|
656
|
-
catch (recheckError) {
|
|
657
|
-
const rechecked = (0, errors_1.normalizeError)(recheckError);
|
|
658
|
-
if (rechecked.code !== "COPILOT_AUTH_REQUIRED") {
|
|
659
|
-
throw recheckError;
|
|
660
|
-
}
|
|
661
|
-
throw (0, errors_1.cliError)("COPILOT_AUTH_REQUIRED", "Copilot authentication is required before the local bridge can be added.", {
|
|
662
|
-
...(rechecked.details ?? {}),
|
|
663
|
-
manualLoginCommand: "copilot login",
|
|
664
|
-
loginLaunchCause,
|
|
665
|
-
suggestion: "Complete GitHub Copilot login with the official tooling, then rerun add --copilot.",
|
|
666
|
-
});
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
}
|
package/dist/commands/help.js
CHANGED
|
@@ -30,6 +30,10 @@ function buildHelpText(commandName) {
|
|
|
30
30
|
"codex-switch",
|
|
31
31
|
"",
|
|
32
32
|
"Manage and switch local Codex provider/profile configuration safely.",
|
|
33
|
+
"Primary workflows: direct providers use init -> add -> switch -> status -> doctor.",
|
|
34
|
+
"Primary workflows: Copilot providers use init -> login copilot -> add --copilot -> switch -> status -> doctor.",
|
|
35
|
+
"Advanced adopt flows use migrate only when you already have Codex runtime state to import.",
|
|
36
|
+
"Deprecated entry: setup still exists only to point callers to init or migrate.",
|
|
33
37
|
"",
|
|
34
38
|
"Usage:",
|
|
35
39
|
" codexs <command> [options]",
|
|
@@ -44,6 +48,7 @@ function buildHelpText(commandName) {
|
|
|
44
48
|
" --version Print the current CLI version.",
|
|
45
49
|
"",
|
|
46
50
|
"Environment:",
|
|
51
|
+
" CODEXS_HOME Override the codex-switch tool home directory.",
|
|
47
52
|
" CODEXS_CODEX_DIR Default Codex directory when --codex-dir is not passed.",
|
|
48
53
|
" NODE_ENV=development defaults to ./dev-codex/local-sandbox when no override is set.",
|
|
49
54
|
"",
|
|
@@ -60,13 +65,14 @@ function buildHelpText(commandName) {
|
|
|
60
65
|
"",
|
|
61
66
|
"Examples:",
|
|
62
67
|
" codexs init",
|
|
63
|
-
" codexs migrate",
|
|
64
|
-
" codexs list",
|
|
65
|
-
" codexs switch",
|
|
66
|
-
" codexs bridge start",
|
|
67
68
|
" codexs add packycode --profile packycode --api-key sk-xxx",
|
|
69
|
+
" codexs switch packycode",
|
|
70
|
+
" codexs status",
|
|
71
|
+
" codexs doctor",
|
|
72
|
+
" codexs login copilot",
|
|
73
|
+
" codexs add copilot-main --copilot --profile copilot-main",
|
|
74
|
+
" codexs migrate",
|
|
68
75
|
" codexs config show",
|
|
69
|
-
" codexs remove freemodel",
|
|
70
76
|
" codexs backups list",
|
|
71
77
|
" codexs rollback",
|
|
72
78
|
" codexs help add",
|
|
@@ -88,28 +88,46 @@ 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 for the primary workflow.",
|
|
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
|
+
"Use init first for fresh direct-provider or Copilot setups.",
|
|
98
99
|
],
|
|
99
100
|
examples: ["codexs init", "codexs init --json --codex-dir ~/.codex"],
|
|
100
101
|
},
|
|
102
|
+
{
|
|
103
|
+
id: "login",
|
|
104
|
+
tokens: ["login"],
|
|
105
|
+
handler: handlers_1.handleRegisteredCommand,
|
|
106
|
+
group: "write",
|
|
107
|
+
summary: "Complete upstream onboarding for interactive providers such as GitHub Copilot.",
|
|
108
|
+
usage: ["codexs login <upstream>"],
|
|
109
|
+
details: [
|
|
110
|
+
"Currently supports copilot and github-copilot as the same upstream.",
|
|
111
|
+
"Installs the local Copilot SDK under the tool home when needed, then checks login readiness.",
|
|
112
|
+
"When login is not ready, launches the bundled Copilot CLI from the runtime when available, otherwise falls back to PATH, then rechecks before succeeding.",
|
|
113
|
+
"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.",
|
|
114
|
+
"Requires an interactive TTY and does not support --json.",
|
|
115
|
+
],
|
|
116
|
+
examples: ["codexs login copilot", "codexs login github-copilot"],
|
|
117
|
+
},
|
|
101
118
|
{
|
|
102
119
|
id: "migrate",
|
|
103
120
|
tokens: ["migrate"],
|
|
104
121
|
handler: handlers_1.handleRegisteredCommand,
|
|
105
122
|
group: "write",
|
|
106
|
-
summary: "Adopt
|
|
123
|
+
summary: "Adopt existing Codex runtime profiles into managed providers.json state.",
|
|
107
124
|
usage: ["codexs migrate [--json] [--codex-dir <path>] [--merge|--overwrite]"],
|
|
108
125
|
details: [
|
|
109
126
|
"Reads config.toml profiles, collects complete provider records, then writes providers.json under managed backup flow.",
|
|
110
127
|
"TTY mode can collect missing provider details and choose merge or overwrite when providers.json already exists.",
|
|
111
128
|
"Migrate adopts only runtime profiles that already expose model, model_provider, and matching base_url.",
|
|
112
129
|
"Non-TTY and --json runs still fail fast because migrate profile selection and provider details remain interactive in this release.",
|
|
130
|
+
"Treat migrate as an advanced adopt helper for existing runtime state, not the default first step for fresh installs.",
|
|
113
131
|
],
|
|
114
132
|
examples: ["codexs migrate", "codexs migrate --overwrite --json --codex-dir ~/.codex"],
|
|
115
133
|
},
|
|
@@ -118,12 +136,12 @@ exports.COMMANDS = [
|
|
|
118
136
|
tokens: ["setup"],
|
|
119
137
|
handler: handlers_1.handleRegisteredCommand,
|
|
120
138
|
group: "write",
|
|
121
|
-
summary: "Deprecated.
|
|
139
|
+
summary: "Deprecated. Kept only to point callers to init or migrate.",
|
|
122
140
|
usage: ["codexs setup"],
|
|
123
141
|
details: [
|
|
124
142
|
"setup no longer performs initialization or migration work.",
|
|
125
|
-
"Use init for
|
|
126
|
-
"Use migrate
|
|
143
|
+
"Use init for the primary fresh-install workflow.",
|
|
144
|
+
"Use migrate only when adopting from existing config.toml profiles.",
|
|
127
145
|
],
|
|
128
146
|
examples: ["codexs help init", "codexs help migrate"],
|
|
129
147
|
},
|
|
@@ -166,12 +184,12 @@ exports.COMMANDS = [
|
|
|
166
184
|
tokens: ["status"],
|
|
167
185
|
handler: handlers_1.handleRegisteredCommand,
|
|
168
186
|
group: "read",
|
|
169
|
-
summary: "Show
|
|
187
|
+
summary: "Show tool-home, target-runtime, provider, and runtime-health status.",
|
|
170
188
|
usage: ["codexs status [--json] [--codex-dir <path>]"],
|
|
171
189
|
details: [
|
|
172
|
-
"Reports
|
|
173
|
-
"When the active provider uses a local runtime bridge, status also reports bridge
|
|
174
|
-
"Surfaces config consistency signals without mutating any files.",
|
|
190
|
+
"Reports the target Codex runtime, tool-home storage roles, current profile, and whether the live profile is mapped.",
|
|
191
|
+
"When the active provider uses a local runtime bridge, status also reports bridge, Copilot SDK, and upstream auth state.",
|
|
192
|
+
"Surfaces dual-path config consistency signals without mutating any files.",
|
|
175
193
|
"Use doctor for deeper diagnostics.",
|
|
176
194
|
],
|
|
177
195
|
examples: ["codexs status", "codexs status --json --codex-dir ./.tmp-codex"],
|
|
@@ -200,7 +218,7 @@ exports.COMMANDS = [
|
|
|
200
218
|
tokens: ["add"],
|
|
201
219
|
handler: handlers_1.handleRegisteredCommand,
|
|
202
220
|
group: "write",
|
|
203
|
-
summary: "Add a provider
|
|
221
|
+
summary: "Add a managed provider for the primary direct or Copilot workflows.",
|
|
204
222
|
usage: [
|
|
205
223
|
"codexs add <provider> --profile <name> --api-key <key> [--base-url <url>] [--note <text>] [--tag <tag> ...]",
|
|
206
224
|
"codexs add <provider> --copilot --profile <name> [--bridge-host <host>] [--bridge-port <port>] [--bridge-api-key <secret>] [--install-copilot-sdk]",
|
|
@@ -213,13 +231,16 @@ exports.COMMANDS = [
|
|
|
213
231
|
"Confirm API key when prompted interactively because the hidden prompt asks twice before writing.",
|
|
214
232
|
"Interactive tags use preset multi-select only.",
|
|
215
233
|
"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.",
|
|
234
|
+
"Creating a missing direct-provider profile section requires --create-profile together with --model and --base-url.",
|
|
235
|
+
"Creating a missing Copilot profile section requires --create-profile together with --model; the local bridge base_url is derived automatically.",
|
|
217
236
|
"Use --copilot to create a GitHub Copilot bridge provider backed by the official SDK.",
|
|
218
|
-
"
|
|
237
|
+
"Copilot providers require SDK install and login readiness to already be satisfied via codexs login copilot.",
|
|
238
|
+
"For Copilot providers, provider apiKey stores only the local bridge secret; upstream GitHub Copilot auth stays shared in the official runtime login.",
|
|
239
|
+
"--install-copilot-sdk is kept only as a rejected compatibility flag that points to codexs login copilot.",
|
|
219
240
|
],
|
|
220
241
|
examples: [
|
|
221
242
|
"codexs add packycode --profile packycode --api-key sk-xxx",
|
|
222
|
-
"codexs add copilot-main --copilot --profile copilot-main
|
|
243
|
+
"codexs add copilot-main --copilot --profile copilot-main",
|
|
223
244
|
"codexs add",
|
|
224
245
|
],
|
|
225
246
|
},
|
|
@@ -228,14 +249,15 @@ exports.COMMANDS = [
|
|
|
228
249
|
tokens: ["switch"],
|
|
229
250
|
handler: handlers_1.handleRegisteredCommand,
|
|
230
251
|
group: "write",
|
|
231
|
-
summary: "Switch the active
|
|
252
|
+
summary: "Switch the active runtime to a managed provider.",
|
|
232
253
|
usage: ["codexs switch <provider> [--json] [--codex-dir <path>]"],
|
|
233
254
|
details: [
|
|
234
255
|
"When <provider> is omitted in a TTY, an interactive provider selector is shown.",
|
|
235
256
|
"When <provider> is passed explicitly, switch proceeds directly without extra confirmation.",
|
|
236
257
|
"Direct providers update the active config profile and rewrite auth.json with auth_mode=apikey plus OPENAI_API_KEY.",
|
|
237
|
-
"Copilot bridge providers
|
|
258
|
+
"Copilot bridge providers also rewrite OPENAI_API_KEY to the local bridge secret while managing runtime routing and bridge state.",
|
|
238
259
|
"Copilot bridge providers probe the optional official SDK before switching and fail fast if it is missing.",
|
|
260
|
+
"Switch succeeds only after the managed profile projection is written to the target runtime.",
|
|
239
261
|
"Backs up config.toml and auth.json and rolls back on failure.",
|
|
240
262
|
],
|
|
241
263
|
examples: ["codexs switch freemodel", "codexs switch packycode --json"],
|
|
@@ -302,7 +324,7 @@ exports.COMMANDS = [
|
|
|
302
324
|
tokens: ["doctor"],
|
|
303
325
|
handler: handlers_1.handleRegisteredCommand,
|
|
304
326
|
group: "recovery",
|
|
305
|
-
summary: "Run
|
|
327
|
+
summary: "Run repair-oriented diagnostics across tool-home and target-runtime state.",
|
|
306
328
|
usage: ["codexs doctor [--json] [--codex-dir <path>]"],
|
|
307
329
|
details: [
|
|
308
330
|
"Checks the expected config files, provider/profile consistency, and Codex CLI availability.",
|
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") {
|