@minniexcode/codex-switch 0.0.9 → 0.0.11

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