@minniexcode/codex-switch 0.0.10 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.AI.md +68 -73
  2. package/README.CN.md +108 -111
  3. package/README.md +87 -80
  4. package/dist/app/add-provider.js +29 -15
  5. package/dist/app/bridge.js +15 -14
  6. package/dist/app/edit-provider.js +1 -1
  7. package/dist/app/get-status.js +21 -9
  8. package/dist/app/import-providers.js +1 -1
  9. package/dist/app/init-codex.js +13 -14
  10. package/dist/app/list-providers.js +48 -1
  11. package/dist/app/remove-provider.js +1 -1
  12. package/dist/app/run-doctor.js +12 -5
  13. package/dist/app/run-mutation.js +3 -2
  14. package/dist/app/setup-codex.js +3 -1
  15. package/dist/app/switch-provider.js +11 -13
  16. package/dist/cli/output.js +145 -18
  17. package/dist/cli.js +34 -2
  18. package/dist/commands/args.js +2 -2
  19. package/dist/commands/dispatch.js +40 -0
  20. package/dist/commands/handlers.js +130 -161
  21. package/dist/commands/help.js +11 -5
  22. package/dist/commands/registry.js +42 -20
  23. package/dist/domain/backups.js +4 -4
  24. package/dist/domain/config.js +110 -5
  25. package/dist/domain/providers.js +12 -0
  26. package/dist/domain/runtime-state.js +111 -13
  27. package/dist/infra/config-repo.js +16 -206
  28. package/dist/interaction/interactive.js +16 -6
  29. package/dist/runtime/copilot-adapter.js +12 -12
  30. package/dist/runtime/copilot-bridge.js +394 -45
  31. package/dist/runtime/copilot-cli.js +84 -12
  32. package/dist/runtime/copilot-installer.js +10 -9
  33. package/dist/runtime/copilot-sdk-loader.js +5 -5
  34. package/dist/storage/backup-repo.js +4 -4
  35. package/dist/storage/codex-paths.js +34 -8
  36. package/dist/storage/config-repo.js +0 -23
  37. package/dist/storage/lock-repo.js +2 -4
  38. package/dist/storage/runtime-state-repo.js +14 -13
  39. package/dist/storage/tool-config-repo.js +111 -0
  40. package/docs/Design/codex-switch-v0.0.11-design.md +824 -0
  41. package/docs/Design/codex-switch-v0.0.12-design.md +343 -0
  42. package/docs/PRD/codex-switch-prd-v0.0.11.md +577 -0
  43. package/docs/PRD/codex-switch-prd-v0.0.12.md +279 -0
  44. package/docs/PRD/codex-switch-prd-v0.1.0.md +125 -237
  45. package/docs/Tests/testing.md +39 -112
  46. package/docs/cli-usage.md +135 -565
  47. package/docs/codex-switch-command-design.md +3 -0
  48. package/docs/codex-switch-product-overview.md +52 -207
  49. package/docs/codex-switch-technical-architecture.md +3 -0
  50. package/package.json +1 -1
  51. package/dist/app/rollback-latest.js +0 -26
@@ -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
- let codexDir = ctx.options.codexDir;
129
- const candidates = (0, config_repo_1.findCodexDirCandidates)(ctx.options.codexDirExplicit ? ctx.options.codexDir : null);
130
- if (!ctx.options.codexDirExplicit) {
131
- if (candidates.length > 1) {
132
- if (!(0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
133
- throw (0, errors_1.cliError)("CODEX_DIR_AMBIGUOUS", "Multiple Codex directories were found.", {
134
- candidates,
135
- });
136
- }
137
- codexDir = await (0, interactive_1.chooseCodexDir)(runtime, candidates);
138
- }
139
- else if (candidates.length === 0) {
140
- if (!(0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
141
- throw (0, errors_1.cliError)("CODEX_DIR_NOT_FOUND", "No Codex directory could be found.", {
142
- codexDir: ctx.options.codexDir,
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
- setupPaths = (0, codex_paths_1.createCodexPaths)(codexDir);
152
- let createCodexDir = false;
153
- if (!fs.existsSync(setupPaths.codexDir)) {
154
- if (!(0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
155
- throw (0, errors_1.cliError)("CODEX_DIR_NOT_FOUND", "The requested Codex directory does not exist.", {
156
- codexDir: setupPaths.codexDir,
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
- createCodexDir = await (0, interactive_1.confirmCreateCodexDir)(runtime, setupPaths.codexDir);
160
- if (!createCodexDir) {
161
- throw (0, errors_1.cliError)("CODEX_DIR_NOT_FOUND", "The requested Codex directory does not exist.", {
162
- codexDir: setupPaths.codexDir,
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
- return (0, init_codex_1.initCodex)({
167
- codexDir: setupPaths.codexDir,
168
- providersPath: setupPaths.providersPath,
169
- configPath: setupPaths.configPath,
170
- authPath: setupPaths.authPath,
171
- createCodexDir,
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 only supported with add --copilot.");
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
- let installCopilotSdk = (0, args_1.hasFlag)(parsed.commandOptions, "--install-copilot-sdk");
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
- }
@@ -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 a Codex directory with an empty managed providers registry.",
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 with an empty providers object when it does not exist yet.",
95
- "Does not require codex CLI, config.toml, or auth.json, and does not run doctor.",
96
- "TTY mode can resolve ambiguous Codex directories and confirm creating a missing directory.",
97
- "Non-TTY and --json runs never prompt and fail when the target directory is missing.",
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 unmanaged Codex config profiles into providers.json.",
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. Use init or migrate instead.",
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 AI-friendly idempotent providers.json initialization.",
126
- "Use migrate for interactive adoption from existing config.toml profiles.",
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 a quick status summary for the local Codex directory.",
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 file presence, current profile, and whether the live profile is mapped.",
173
- "When the active provider uses a local runtime bridge, status also reports bridge and SDK state.",
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 with explicit flags or progressive TTY prompts.",
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
- "TTY add --copilot checks SDK install and GitHub Copilot login before it asks for Copilot provider fields.",
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 --install-copilot-sdk",
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 config profile to a provider.",
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 still manage runtime routing and bridge state instead of rewriting OPENAI_API_KEY.",
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 configuration and environment diagnostics.",
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.",
@@ -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" || typeof entry.existed !== "boolean") {
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") {