@minniexcode/codex-switch 0.0.10 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.AI.md +52 -15
  2. package/README.CN.md +81 -48
  3. package/README.md +75 -34
  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 +13 -6
  8. package/dist/app/import-providers.js +1 -1
  9. package/dist/app/init-codex.js +13 -14
  10. package/dist/app/remove-provider.js +1 -1
  11. package/dist/app/run-doctor.js +12 -5
  12. package/dist/app/run-mutation.js +3 -2
  13. package/dist/app/setup-codex.js +3 -1
  14. package/dist/app/switch-provider.js +11 -13
  15. package/dist/cli.js +34 -2
  16. package/dist/commands/args.js +2 -2
  17. package/dist/commands/dispatch.js +40 -0
  18. package/dist/commands/handlers.js +121 -156
  19. package/dist/commands/help.js +2 -0
  20. package/dist/commands/registry.js +28 -9
  21. package/dist/domain/backups.js +4 -4
  22. package/dist/domain/config.js +110 -5
  23. package/dist/domain/providers.js +12 -0
  24. package/dist/domain/runtime-state.js +81 -5
  25. package/dist/runtime/copilot-adapter.js +12 -12
  26. package/dist/runtime/copilot-bridge.js +392 -44
  27. package/dist/runtime/copilot-cli.js +84 -12
  28. package/dist/runtime/copilot-installer.js +10 -9
  29. package/dist/runtime/copilot-sdk-loader.js +5 -5
  30. package/dist/storage/backup-repo.js +4 -4
  31. package/dist/storage/codex-paths.js +34 -8
  32. package/dist/storage/lock-repo.js +2 -4
  33. package/dist/storage/runtime-state-repo.js +14 -13
  34. package/dist/storage/tool-config-repo.js +111 -0
  35. package/docs/Design/codex-switch-v0.0.11-design.md +824 -0
  36. package/docs/PRD/codex-switch-prd-v0.0.11.md +577 -0
  37. package/docs/cli-usage.md +166 -295
  38. package/package.json +1 -1
@@ -71,6 +71,10 @@ 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) {
@@ -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,103 @@ 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
+ 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;
164
191
  }
165
192
  }
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
- });
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 ?? {}),
217
+ });
218
+ }
219
+ throw error;
220
+ }
221
+ return {
222
+ data: {
223
+ upstream: "github-copilot",
224
+ sdkInstalled: true,
225
+ sdkInstalledNow: installedNow,
226
+ authReady: true,
227
+ loginLaunched: true,
228
+ },
229
+ };
173
230
  }
174
231
  case "config-show":
175
232
  return (0, show_config_1.showConfig)({
@@ -191,15 +248,20 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
191
248
  throw (0, errors_1.cliError)("PROVIDER_NOT_FOUND", "Missing provider name for switch command.");
192
249
  }
193
250
  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.");
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
+ });
195
254
  }
196
255
  return (0, switch_provider_1.switchProvider)({
197
256
  codexDir: paths.codexDir,
257
+ lockPath: paths.lockPath,
198
258
  backupsDir: paths.backupsDir,
199
259
  latestBackupPath: paths.latestBackupPath,
200
260
  configPath: paths.configPath,
201
261
  providersPath: paths.providersPath,
202
262
  authPath: paths.authPath,
263
+ runtimeDir: paths.runtimeDir,
264
+ runtimesDir: paths.runtimesDir,
203
265
  providerName,
204
266
  });
205
267
  }
@@ -223,6 +285,7 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
223
285
  }
224
286
  return (0, import_providers_1.importProviders)({
225
287
  codexDir: paths.codexDir,
288
+ lockPath: paths.lockPath,
226
289
  backupsDir: paths.backupsDir,
227
290
  latestBackupPath: paths.latestBackupPath,
228
291
  providersPath: paths.providersPath,
@@ -263,21 +326,19 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
263
326
  let bridgeHost = (0, args_1.getSingleOption)(parsed.commandOptions, "--bridge-host", false);
264
327
  const bridgePortValue = (0, args_1.getSingleOption)(parsed.commandOptions, "--bridge-port", false);
265
328
  let bridgeApiKey = (0, args_1.getSingleOption)(parsed.commandOptions, "--bridge-api-key", false);
266
- let installCopilotSdk = (0, args_1.hasFlag)(parsed.commandOptions, "--install-copilot-sdk");
329
+ const installCopilotSdk = (0, args_1.hasFlag)(parsed.commandOptions, "--install-copilot-sdk");
267
330
  let bridgePort = bridgePortValue ? Number(bridgePortValue) : null;
268
331
  if (copilot && apiKey) {
269
332
  throw (0, errors_1.cliError)("INVALID_ARGUMENT", "--copilot does not allow --api-key. Use --bridge-api-key for the local bridge secret.");
270
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
+ }
271
339
  if (bridgePortValue && (!Number.isInteger(bridgePort) || bridgePort === null || bridgePort <= 0)) {
272
340
  throw (0, errors_1.cliError)("INVALID_ARGUMENT", "--bridge-port must be a positive integer.");
273
341
  }
274
- if (copilot) {
275
- installCopilotSdk = await ensureCopilotReadyForAdd({
276
- runtime,
277
- json: ctx.options.json,
278
- installCopilotSdk,
279
- });
280
- }
281
342
  if (!providerName || !profile || (!apiKey && !copilot)) {
282
343
  if (ctx.options.json || !runtime.isInteractive()) {
283
344
  throw (0, add_interactive_1.createNonInteractiveAddError)({ copilot });
@@ -327,6 +388,9 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
327
388
  }
328
389
  return (0, add_provider_1.addProvider)({
329
390
  codexDir: paths.codexDir,
391
+ toolHomeDir: paths.toolHomeDir,
392
+ lockPath: paths.lockPath,
393
+ runtimesDir: paths.runtimesDir,
330
394
  backupsDir: paths.backupsDir,
331
395
  latestBackupPath: paths.latestBackupPath,
332
396
  providersPath: paths.providersPath,
@@ -344,8 +408,6 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
344
408
  bridgeHost,
345
409
  bridgePort,
346
410
  bridgeApiKey,
347
- installCopilotSdk,
348
- interactive: (0, interactive_1.canPrompt)(runtime, ctx.options.json),
349
411
  });
350
412
  }
351
413
  case "edit": {
@@ -388,6 +450,7 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
388
450
  }
389
451
  return (0, edit_provider_1.editProvider)({
390
452
  codexDir: paths.codexDir,
453
+ lockPath: paths.lockPath,
391
454
  backupsDir: paths.backupsDir,
392
455
  latestBackupPath: paths.latestBackupPath,
393
456
  providersPath: paths.providersPath,
@@ -422,6 +485,7 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
422
485
  }
423
486
  return (0, remove_provider_1.removeProvider)({
424
487
  codexDir: paths.codexDir,
488
+ lockPath: paths.lockPath,
425
489
  backupsDir: paths.backupsDir,
426
490
  latestBackupPath: paths.latestBackupPath,
427
491
  providersPath: paths.providersPath,
@@ -436,6 +500,8 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
436
500
  configPath: paths.configPath,
437
501
  providersPath: paths.providersPath,
438
502
  authPath: paths.authPath,
503
+ runtimeDir: paths.runtimeDir,
504
+ runtimesDir: paths.runtimesDir,
439
505
  });
440
506
  case "migrate": {
441
507
  let codexDir = ctx.options.codexDir;
@@ -531,9 +597,12 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
531
597
  return (0, setup_codex_1.migrateCodex)({
532
598
  codexDirOption: ctx.options.codexDir,
533
599
  codexDir: setupPaths.codexDir,
600
+ lockPath: setupPaths.lockPath,
534
601
  configPath: setupPaths.configPath,
535
602
  providersPath: setupPaths.providersPath,
536
603
  authPath: setupPaths.authPath,
604
+ runtimeDir: setupPaths.runtimeDir,
605
+ runtimesDir: setupPaths.runtimesDir,
537
606
  backupsDir: setupPaths.backupsDir,
538
607
  latestBackupPath: setupPaths.latestBackupPath,
539
608
  strategy: strategy ?? "overwrite",
@@ -563,107 +632,3 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
563
632
  throw (0, errors_1.cliError)("UNKNOWN_COMMAND", `Unknown command: ${ctx.command}`);
564
633
  }
565
634
  }
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
- }
@@ -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",
@@ -88,16 +88,32 @@ 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 and registry files.",
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
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"],
@@ -213,13 +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.",
218
- "TTY add --copilot checks SDK install and GitHub Copilot login before it asks for Copilot provider fields.",
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.",
219
238
  ],
220
239
  examples: [
221
240
  "codexs add packycode --profile packycode --api-key sk-xxx",
222
- "codexs add copilot-main --copilot --profile copilot-main --install-copilot-sdk",
241
+ "codexs add copilot-main --copilot --profile copilot-main",
223
242
  "codexs add",
224
243
  ],
225
244
  },
@@ -234,7 +253,7 @@ exports.COMMANDS = [
234
253
  "When <provider> is omitted in a TTY, an interactive provider selector is shown.",
235
254
  "When <provider> is passed explicitly, switch proceeds directly without extra confirmation.",
236
255
  "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.",
256
+ "Copilot bridge providers also rewrite OPENAI_API_KEY to the local bridge secret while managing runtime routing and bridge state.",
238
257
  "Copilot bridge providers probe the optional official SDK before switching and fail fast if it is missing.",
239
258
  "Backs up config.toml and auth.json and rolls back on failure.",
240
259
  ],
@@ -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") {