@minniexcode/codex-switch 0.0.8 → 0.0.10

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 (44) hide show
  1. package/README.AI.md +5 -3
  2. package/README.CN.md +25 -3
  3. package/README.md +3 -2
  4. package/dist/app/add-provider.js +1 -12
  5. package/dist/app/bridge.js +295 -0
  6. package/dist/app/edit-provider.js +1 -17
  7. package/dist/app/get-status.js +32 -2
  8. package/dist/app/list-providers.js +0 -1
  9. package/dist/app/run-doctor.js +45 -38
  10. package/dist/app/setup-codex.js +27 -17
  11. package/dist/app/show-config.js +1 -5
  12. package/dist/app/switch-provider.js +33 -20
  13. package/dist/cli/output.js +4 -6
  14. package/dist/cli.js +1 -1
  15. package/dist/commands/handlers.js +223 -39
  16. package/dist/commands/help.js +1 -0
  17. package/dist/commands/registry.js +48 -4
  18. package/dist/domain/config.js +4 -68
  19. package/dist/domain/providers.js +0 -5
  20. package/dist/domain/runtime-state.js +2 -1
  21. package/dist/domain/setup.js +58 -3
  22. package/dist/interaction/add-interactive.js +55 -1
  23. package/dist/interaction/interactive.js +1 -5
  24. package/dist/runtime/copilot-adapter.js +44 -1
  25. package/dist/runtime/copilot-bridge-worker.js +1 -1
  26. package/dist/runtime/copilot-bridge.js +60 -19
  27. package/dist/runtime/copilot-cli.js +70 -0
  28. package/dist/runtime/copilot-installer.js +49 -2
  29. package/dist/storage/auth-repo.js +28 -77
  30. package/dist/storage/config-repo.js +1 -36
  31. package/dist/storage/runtime-state-repo.js +32 -0
  32. package/docs/Design/codex-switch-copilot-integration-design.md +517 -0
  33. package/docs/Design/codex-switch-v0.0.10-design.md +669 -0
  34. package/docs/Design/codex-switch-v0.0.9-design.md +182 -0
  35. package/docs/PRD/codex-switch-prd-v0.0.10.md +406 -0
  36. package/docs/PRD/codex-switch-prd-v0.0.9.md +166 -0
  37. package/docs/Tests/testing-bridge-v0.0.9.md +367 -0
  38. package/docs/cli-usage.md +38 -14
  39. package/docs/codex-switch-product-overview.md +2 -2
  40. package/docs/codex-switch-technical-architecture.md +6 -5
  41. package/package.json +1 -1
  42. /package/docs/{test-report-0.0.5.md → Tests/test-report-0.0.5.md} +0 -0
  43. /package/docs/{test-report-0.0.7.md → Tests/test-report-0.0.7.md} +0 -0
  44. /package/docs/{testing.md → Tests/testing.md} +0 -0
@@ -48,16 +48,20 @@ const list_providers_1 = require("../app/list-providers");
48
48
  const remove_provider_1 = require("../app/remove-provider");
49
49
  const rollback_backup_1 = require("../app/rollback-backup");
50
50
  const run_doctor_1 = require("../app/run-doctor");
51
+ const bridge_1 = require("../app/bridge");
51
52
  const setup_codex_1 = require("../app/setup-codex");
52
53
  const show_config_1 = require("../app/show-config");
53
54
  const show_provider_1 = require("../app/show-provider");
54
55
  const switch_provider_1 = require("../app/switch-provider");
55
56
  const config_1 = require("../domain/config");
56
57
  const errors_1 = require("../domain/errors");
58
+ const setup_1 = require("../domain/setup");
57
59
  const providers_1 = require("../domain/providers");
58
60
  const add_interactive_1 = require("../interaction/add-interactive");
59
61
  const interactive_1 = require("../interaction/interactive");
60
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");
61
65
  const copilot_installer_1 = require("../runtime/copilot-installer");
62
66
  const config_repo_1 = require("../storage/config-repo");
63
67
  const codex_paths_1 = require("../storage/codex-paths");
@@ -90,6 +94,36 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
90
94
  return (0, get_current_profile_1.getCurrentProfile)(paths.configPath);
91
95
  case "status":
92
96
  return (0, get_status_1.getStatus)(paths.codexDir, paths.configPath, paths.providersPath, paths.authPath);
97
+ case "bridge-start": {
98
+ const providerName = parsed.positionals[0] ?? null;
99
+ return (0, bridge_1.startBridge)({
100
+ providersPath: paths.providersPath,
101
+ configPath: paths.configPath,
102
+ providerName,
103
+ runtime,
104
+ json: ctx.options.json,
105
+ });
106
+ }
107
+ case "bridge-stop": {
108
+ const providerName = parsed.positionals[0] ?? null;
109
+ return (0, bridge_1.stopBridge)({
110
+ providersPath: paths.providersPath,
111
+ configPath: paths.configPath,
112
+ providerName,
113
+ runtime,
114
+ json: ctx.options.json,
115
+ });
116
+ }
117
+ case "bridge-status": {
118
+ const providerName = parsed.positionals[0] ?? null;
119
+ return (0, bridge_1.statusBridge)({
120
+ providersPath: paths.providersPath,
121
+ configPath: paths.configPath,
122
+ providerName,
123
+ runtime,
124
+ json: ctx.options.json,
125
+ });
126
+ }
93
127
  case "init": {
94
128
  let codexDir = ctx.options.codexDir;
95
129
  const candidates = (0, config_repo_1.findCodexDirCandidates)(ctx.options.codexDirExplicit ? ctx.options.codexDir : null);
@@ -226,40 +260,70 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
226
260
  let tags = parsed.commandOptions.get("--tag") ?? [];
227
261
  let createProfile = (0, args_1.hasFlag)(parsed.commandOptions, "--create-profile");
228
262
  const copilot = (0, args_1.hasFlag)(parsed.commandOptions, "--copilot");
229
- const bridgeHost = (0, args_1.getSingleOption)(parsed.commandOptions, "--bridge-host", false);
263
+ let bridgeHost = (0, args_1.getSingleOption)(parsed.commandOptions, "--bridge-host", false);
230
264
  const bridgePortValue = (0, args_1.getSingleOption)(parsed.commandOptions, "--bridge-port", false);
231
- const bridgeApiKey = (0, args_1.getSingleOption)(parsed.commandOptions, "--bridge-api-key", false);
265
+ let bridgeApiKey = (0, args_1.getSingleOption)(parsed.commandOptions, "--bridge-api-key", false);
232
266
  let installCopilotSdk = (0, args_1.hasFlag)(parsed.commandOptions, "--install-copilot-sdk");
233
- const bridgePort = bridgePortValue ? Number(bridgePortValue) : null;
267
+ let bridgePort = bridgePortValue ? Number(bridgePortValue) : null;
234
268
  if (copilot && apiKey) {
235
269
  throw (0, errors_1.cliError)("INVALID_ARGUMENT", "--copilot does not allow --api-key. Use --bridge-api-key for the local bridge secret.");
236
270
  }
237
271
  if (bridgePortValue && (!Number.isInteger(bridgePort) || bridgePort === null || bridgePort <= 0)) {
238
272
  throw (0, errors_1.cliError)("INVALID_ARGUMENT", "--bridge-port must be a positive integer.");
239
273
  }
240
- if (copilot && !installCopilotSdk && (0, interactive_1.canPrompt)(runtime, ctx.options.json) && !(0, copilot_installer_1.probeCopilotSdkInstall)().installed) {
241
- installCopilotSdk = await runtime.confirmAction("The optional Copilot SDK runtime is not installed. Install it now?");
274
+ if (copilot) {
275
+ installCopilotSdk = await ensureCopilotReadyForAdd({
276
+ runtime,
277
+ json: ctx.options.json,
278
+ installCopilotSdk,
279
+ });
242
280
  }
243
281
  if (!providerName || !profile || (!apiKey && !copilot)) {
244
282
  if (ctx.options.json || !runtime.isInteractive()) {
245
- throw (0, add_interactive_1.createNonInteractiveAddError)();
283
+ throw (0, add_interactive_1.createNonInteractiveAddError)({ copilot });
284
+ }
285
+ if (copilot) {
286
+ const prompted = await (0, add_interactive_1.collectCopilotAddInput)(runtime, {
287
+ providerName,
288
+ profile,
289
+ model,
290
+ note,
291
+ tags,
292
+ }, (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)), {
293
+ bridgeHost,
294
+ bridgePort,
295
+ bridgeApiKey,
296
+ });
297
+ providerName = prompted.providerName;
298
+ profile = prompted.profile;
299
+ model = prompted.model ?? null;
300
+ note = prompted.note ?? null;
301
+ tags = prompted.tags;
302
+ createProfile = createProfile || prompted.createProfile;
303
+ baseUrl = null;
304
+ bridgeHost = prompted.bridgeHost ?? bridgeHost;
305
+ bridgePort = prompted.bridgePort ?? bridgePort;
306
+ bridgeApiKey = prompted.bridgeApiKey ?? bridgeApiKey;
307
+ }
308
+ else {
309
+ const prompted = await (0, add_interactive_1.collectAddInput)(runtime, {
310
+ providerName,
311
+ profile,
312
+ apiKey,
313
+ model,
314
+ baseUrl,
315
+ note,
316
+ tags,
317
+ }, (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)));
318
+ providerName = prompted.providerName;
319
+ profile = prompted.profile;
320
+ apiKey = prompted.apiKey;
321
+ model = prompted.model ?? null;
322
+ baseUrl = prompted.baseUrl ?? null;
323
+ note = prompted.note ?? null;
324
+ tags = prompted.tags;
325
+ createProfile = createProfile || prompted.createProfile;
246
326
  }
247
- const prompted = await (0, add_interactive_1.collectAddInput)(runtime, {
248
- providerName,
249
- profile,
250
- apiKey,
251
- baseUrl,
252
- note,
253
- tags,
254
- }, (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)));
255
- providerName = prompted.providerName;
256
- profile = prompted.profile;
257
- apiKey = prompted.apiKey;
258
- model = prompted.model ?? null;
259
- baseUrl = prompted.baseUrl ?? null;
260
- note = prompted.note ?? null;
261
- tags = prompted.tags;
262
- createProfile = createProfile || prompted.createProfile;
263
327
  }
264
328
  return (0, add_provider_1.addProvider)({
265
329
  codexDir: paths.codexDir,
@@ -402,9 +466,25 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
402
466
  if (overwrite && merge) {
403
467
  throw (0, errors_1.cliError)("INVALID_ARGUMENT", "migrate does not allow both --merge and --overwrite.");
404
468
  }
405
- let strategy = overwrite ? "overwrite" : merge ? "merge" : null;
406
469
  const providersExists = fs.existsSync(setupPaths.providersPath);
407
- if (providersExists && strategy === null) {
470
+ const document = (0, config_repo_1.readStructuredConfig)(setupPaths.configPath);
471
+ const currentProviders = providersExists ? (0, providers_1.validateProvidersShape)((0, providers_repo_1.readProvidersFileIfExists)(setupPaths.providersPath)) : null;
472
+ const adoptability = (0, setup_1.collectMigrateAdoptability)(document, currentProviders);
473
+ if (adoptability.availableProfiles.length === 0) {
474
+ throw (0, errors_1.cliError)("PROFILE_NOT_FOUND", "No profiles were found in config.toml.", {
475
+ file: setupPaths.configPath,
476
+ });
477
+ }
478
+ if (adoptability.adoptableProfiles.length === 0) {
479
+ throw (0, errors_1.cliError)("MIGRATE_NO_ADOPTABLE_PROFILES", "No adoptable profiles were found for migrate.", {
480
+ availableProfiles: adoptability.availableProfiles,
481
+ adoptableProfiles: adoptability.adoptableProfiles,
482
+ blockingReasonsByProfile: adoptability.blockingReasonsByProfile,
483
+ });
484
+ }
485
+ let strategy = overwrite ? "overwrite" : merge ? "merge" : null;
486
+ const registryIsEmpty = !currentProviders || Object.keys(currentProviders.providers).length === 0;
487
+ if (providersExists && strategy === null && !registryIsEmpty) {
408
488
  if (!(0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
409
489
  throw (0, errors_1.cliError)("PROVIDERS_ALREADY_EXISTS", "providers.json already exists. Pass --merge or --overwrite.", {
410
490
  file: setupPaths.providersPath,
@@ -416,35 +496,35 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
416
496
  }
417
497
  strategy = selected;
418
498
  }
419
- const document = (0, config_repo_1.readStructuredConfig)(setupPaths.configPath);
420
- const adoptableProfiles = (0, config_1.buildManagedProfileViews)(document, null)
421
- .filter((view) => view.source === "unmanaged" && view.model && view.modelProvider === view.name && view.baseUrl && view.envKey)
422
- .map((view) => ({
423
- name: view.name,
424
- model: view.model,
425
- baseUrl: view.baseUrl,
426
- envKey: view.envKey,
427
- }))
428
- .sort((left, right) => left.name.localeCompare(right.name));
429
- const selectedProfiles = Array.from((0, config_repo_1.listConfigProfiles)(setupPaths.configPath)).sort();
499
+ const adoptableProfiles = adoptability.adoptableProfileDetails;
430
500
  let adoptProfiles = [];
431
501
  let providerDetailsByProfile = {};
432
502
  if ((0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
433
503
  adoptProfiles = await (0, interactive_1.chooseSetupProfiles)(runtime, adoptableProfiles);
434
504
  // Defaults are derived from config.toml so interactive setup only asks for missing provider metadata.
435
- providerDetailsByProfile = await (0, interactive_1.collectSetupProviderDetails)(runtime, adoptProfiles, adoptableProfiles.reduce((accumulator, profile) => {
505
+ const collectedDetails = await (0, interactive_1.collectSetupProviderDetails)(runtime, adoptProfiles, adoptableProfiles.reduce((accumulator, profile) => {
436
506
  accumulator[profile.name] = {
437
507
  providerName: profile.name,
438
- envKey: profile.envKey,
439
508
  baseUrl: profile.baseUrl,
440
509
  };
441
510
  return accumulator;
442
511
  }, {}));
512
+ providerDetailsByProfile = Object.fromEntries(Object.entries(collectedDetails).map(([profile, detail]) => [
513
+ profile,
514
+ {
515
+ providerName: detail.providerName,
516
+ apiKey: detail.apiKey,
517
+ baseUrl: detail.baseUrl,
518
+ note: detail.note,
519
+ tags: detail.tags,
520
+ },
521
+ ]));
443
522
  }
444
523
  else {
445
524
  throw (0, errors_1.cliError)("INVALID_ARGUMENT", "migrate currently requires an interactive TTY to choose adoptable profiles and collect provider details.", {
446
- adoptableProfiles,
447
- availableProfiles: selectedProfiles,
525
+ availableProfiles: adoptability.availableProfiles,
526
+ adoptableProfiles: adoptability.adoptableProfiles,
527
+ blockingReasonsByProfile: adoptability.blockingReasonsByProfile,
448
528
  suggestion: "Run `codexs migrate` in an interactive terminal. Non-interactive migrate flags for profile selection and provider secrets are not available in this release.",
449
529
  });
450
530
  }
@@ -483,3 +563,107 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
483
563
  throw (0, errors_1.cliError)("UNKNOWN_COMMAND", `Unknown command: ${ctx.command}`);
484
564
  }
485
565
  }
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
+ }
@@ -63,6 +63,7 @@ function buildHelpText(commandName) {
63
63
  " codexs migrate",
64
64
  " codexs list",
65
65
  " codexs switch",
66
+ " codexs bridge start",
66
67
  " codexs add packycode --profile packycode --api-key sk-xxx",
67
68
  " codexs config show",
68
69
  " codexs remove freemodel",
@@ -41,6 +41,48 @@ exports.COMMANDS = [
41
41
  ],
42
42
  examples: ["codexs config list-profiles", "codexs config list-profiles --json"],
43
43
  },
44
+ {
45
+ id: "bridge-start",
46
+ tokens: ["bridge", "start"],
47
+ handler: handlers_1.handleRegisteredCommand,
48
+ group: "write",
49
+ summary: "Start or reuse the managed Copilot bridge.",
50
+ usage: ["codexs bridge start [provider] [--json] [--codex-dir <path>]"],
51
+ details: [
52
+ "Resolves a Copilot bridge provider by explicit name, active provider, sole provider, or TTY selection.",
53
+ "Reuses a healthy bridge for the same provider and replaces a different managed provider when needed.",
54
+ "If the preferred port is occupied, automatically selects another free 5-digit port and persists it.",
55
+ ],
56
+ examples: ["codexs bridge start", "codexs bridge start copilot-main"],
57
+ },
58
+ {
59
+ id: "bridge-stop",
60
+ tokens: ["bridge", "stop"],
61
+ handler: handlers_1.handleRegisteredCommand,
62
+ group: "recovery",
63
+ summary: "Stop the managed Copilot bridge.",
64
+ usage: ["codexs bridge stop [provider] [--json] [--codex-dir <path>]"],
65
+ details: [
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 Codex auth state.",
68
+ "Is idempotent when no managed bridge is currently running.",
69
+ ],
70
+ examples: ["codexs bridge stop", "codexs bridge stop copilot-main"],
71
+ },
72
+ {
73
+ id: "bridge-status",
74
+ tokens: ["bridge", "status"],
75
+ handler: handlers_1.handleRegisteredCommand,
76
+ group: "read",
77
+ summary: "Inspect the managed Copilot bridge.",
78
+ usage: ["codexs bridge status [provider] [--json] [--codex-dir <path>]"],
79
+ details: [
80
+ "Reports runtime-state, provider binding, and whether the live worker matches the expected provider.",
81
+ "Prefers the runtime-state instance when one is present.",
82
+ "Uses an explicit provider as a guard instead of silently switching targets.",
83
+ ],
84
+ examples: ["codexs bridge status", "codexs bridge status copilot-main"],
85
+ },
44
86
  {
45
87
  id: "init",
46
88
  tokens: ["init"],
@@ -66,7 +108,7 @@ exports.COMMANDS = [
66
108
  details: [
67
109
  "Reads config.toml profiles, collects complete provider records, then writes providers.json under managed backup flow.",
68
110
  "TTY mode can collect missing provider details and choose merge or overwrite when providers.json already exists.",
69
- "Migrate adopts only runtime profiles that already expose model, model_provider, matching base_url, and env_key.",
111
+ "Migrate adopts only runtime profiles that already expose model, model_provider, and matching base_url.",
70
112
  "Non-TTY and --json runs still fail fast because migrate profile selection and provider details remain interactive in this release.",
71
113
  ],
72
114
  examples: ["codexs migrate", "codexs migrate --overwrite --json --codex-dir ~/.codex"],
@@ -173,6 +215,7 @@ exports.COMMANDS = [
173
215
  "Automation and non-TTY environments must pass all required values explicitly.",
174
216
  "Creating a missing profile section requires --create-profile together with --model and --base-url.",
175
217
  "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.",
176
219
  ],
177
220
  examples: [
178
221
  "codexs add packycode --profile packycode --api-key sk-xxx",
@@ -185,14 +228,15 @@ exports.COMMANDS = [
185
228
  tokens: ["switch"],
186
229
  handler: handlers_1.handleRegisteredCommand,
187
230
  group: "write",
188
- summary: "Switch to a provider and rewrite the managed auth mirror.",
231
+ summary: "Switch the active config profile to a provider.",
189
232
  usage: ["codexs switch <provider> [--json] [--codex-dir <path>]"],
190
233
  details: [
191
234
  "When <provider> is omitted in a TTY, an interactive provider selector is shown.",
192
235
  "When <provider> is passed explicitly, switch proceeds directly without extra confirmation.",
193
- "Switch updates the active config profile and rewrites auth.json from the provider envKey/apiKey pair.",
236
+ "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.",
194
238
  "Copilot bridge providers probe the optional official SDK before switching and fail fast if it is missing.",
195
- "Backs up config.toml and auth.json, then rolls back on failure.",
239
+ "Backs up config.toml and auth.json and rolls back on failure.",
196
240
  ],
197
241
  examples: ["codexs switch freemodel", "codexs switch packycode --json"],
198
242
  },
@@ -41,7 +41,6 @@ exports.parseStructuredConfig = parseStructuredConfig;
41
41
  exports.buildManagedProfileViews = buildManagedProfileViews;
42
42
  exports.collectConfigConsistencyIssues = collectConfigConsistencyIssues;
43
43
  exports.validateManagedProfileCreation = validateManagedProfileCreation;
44
- exports.buildManagedProfileEnvKey = buildManagedProfileEnvKey;
45
44
  exports.planProfileLifecycleOutcome = planProfileLifecycleOutcome;
46
45
  exports.planConfigMutation = planConfigMutation;
47
46
  exports.applyPatchOperations = applyPatchOperations;
@@ -120,8 +119,6 @@ function parseStructuredConfig(configContent) {
120
119
  sectionEnd: configContent.length,
121
120
  baseUrlValueRange: null,
122
121
  baseUrl: null,
123
- envKeyValueRange: null,
124
- envKey: null,
125
122
  };
126
123
  modelProviders.push(currentModelProvider);
127
124
  inRoot = false;
@@ -176,14 +173,6 @@ function parseStructuredConfig(configContent) {
176
173
  end: line.start + baseUrlMatch.valueEnd,
177
174
  };
178
175
  }
179
- const envKeyMatch = matchKeyValueLine(line.content, "env_key");
180
- if (envKeyMatch) {
181
- currentModelProvider.envKey = envKeyMatch.value;
182
- currentModelProvider.envKeyValueRange = {
183
- start: line.start + envKeyMatch.valueStart,
184
- end: line.start + envKeyMatch.valueEnd,
185
- };
186
- }
187
176
  }
188
177
  }
189
178
  return {
@@ -218,7 +207,6 @@ function buildManagedProfileViews(document, providers) {
218
207
  model: section.model,
219
208
  modelProvider: section.modelProvider,
220
209
  baseUrl: modelProviderSection?.baseUrl ?? null,
221
- envKey: modelProviderSection?.envKey ?? null,
222
210
  managedFields: collectManagedFields(section.model, section.modelProvider),
223
211
  source: linkInfo.managed ? "managed" : "unmanaged",
224
212
  });
@@ -235,7 +223,6 @@ function buildManagedProfileViews(document, providers) {
235
223
  model: null,
236
224
  modelProvider: null,
237
225
  baseUrl: null,
238
- envKey: null,
239
226
  managedFields: [],
240
227
  source: "orphaned-reference",
241
228
  });
@@ -298,28 +285,6 @@ function collectConfigConsistencyIssues(document, providers) {
298
285
  modelProvider: view.modelProvider,
299
286
  });
300
287
  }
301
- else if (!modelProviderSection.envKey) {
302
- issues.push({
303
- code: "MODEL_PROVIDER_ENV_KEY_MISSING",
304
- profile: view.name,
305
- modelProvider: view.modelProvider,
306
- });
307
- }
308
- }
309
- for (const providerName of view.linkedProviders) {
310
- const provider = providers?.providers[providerName];
311
- if (!provider) {
312
- continue;
313
- }
314
- if (provider.envKey !== view.envKey) {
315
- issues.push({
316
- code: "PROVIDER_ENV_KEY_MISMATCH",
317
- provider: providerName,
318
- profile: view.name,
319
- providerEnvKey: provider.envKey,
320
- runtimeEnvKey: view.envKey,
321
- });
322
- }
323
288
  }
324
289
  }
325
290
  }
@@ -366,17 +331,6 @@ function validateManagedProfileCreation(profile, fields) {
366
331
  modelProvider,
367
332
  };
368
333
  }
369
- /**
370
- * Normalizes a profile name into the default env_key used for generated runtime sections.
371
- */
372
- function buildManagedProfileEnvKey(profile) {
373
- const normalized = profile
374
- .trim()
375
- .replace(/[^A-Za-z0-9]+/g, "_")
376
- .replace(/^_+|_+$/g, "")
377
- .toUpperCase();
378
- return `${normalized || "PROVIDER"}_API_KEY`;
379
- }
380
334
  /**
381
335
  * Computes keep/delete/switch outcomes when a provider leaves or changes profiles.
382
336
  */
@@ -491,14 +445,12 @@ function planConfigMutation(document, args) {
491
445
  const section = modelProviderSectionMap.get(profileName);
492
446
  if (!section) {
493
447
  const baseUrl = fields.baseUrl?.trim() ?? "";
494
- const envKey = fields.envKey?.trim() ?? "";
495
- if (!baseUrl || !envKey) {
496
- throw (0, errors_1.cliError)("MANAGED_PROFILE_FIELDS_MISSING", `Model provider "${profileName}" requires both base_url and env_key.`, {
448
+ if (!baseUrl) {
449
+ throw (0, errors_1.cliError)("MANAGED_PROFILE_FIELDS_MISSING", `Model provider "${profileName}" requires base_url.`, {
497
450
  profile: profileName,
498
451
  modelProvider: profileName,
499
452
  missingFields: [
500
453
  !baseUrl ? "base_url" : null,
501
- !envKey ? "env_key" : null,
502
454
  ].filter((value) => Boolean(value)),
503
455
  });
504
456
  }
@@ -509,8 +461,7 @@ function planConfigMutation(document, args) {
509
461
  kind: "insert-at",
510
462
  index: document.rawText.length,
511
463
  text: `${prefix}[model_providers.${profileName}]${document.lineEnding}` +
512
- `base_url = ${JSON.stringify(baseUrl)}${document.lineEnding}` +
513
- `env_key = ${JSON.stringify(envKey)}${document.lineEnding}`,
464
+ `base_url = ${JSON.stringify(baseUrl)}${document.lineEnding}`,
514
465
  });
515
466
  createdModelProviderSections.push(profileName);
516
467
  continue;
@@ -594,12 +545,11 @@ function planSectionFieldMutation(document, section, fields, operations) {
594
545
  return updated;
595
546
  }
596
547
  /**
597
- * Plans base_url/env_key updates for one model_providers section.
548
+ * Plans base_url updates for one model_providers section.
598
549
  */
599
550
  function planModelProviderFieldMutation(section, fields, operations) {
600
551
  let updated = false;
601
552
  const baseUrlText = fields.baseUrl !== undefined ? JSON.stringify(fields.baseUrl) : null;
602
- const envKeyText = fields.envKey !== undefined ? JSON.stringify(fields.envKey) : null;
603
553
  const inserts = [];
604
554
  if (baseUrlText !== null && section.baseUrlValueRange) {
605
555
  if (section.baseUrl !== fields.baseUrl) {
@@ -615,20 +565,6 @@ function planModelProviderFieldMutation(section, fields, operations) {
615
565
  else if (baseUrlText !== null) {
616
566
  inserts.push(`base_url = ${baseUrlText}`);
617
567
  }
618
- if (envKeyText !== null && section.envKeyValueRange) {
619
- if (section.envKey !== fields.envKey) {
620
- operations.push({
621
- kind: "replace-range",
622
- start: section.envKeyValueRange.start,
623
- end: section.envKeyValueRange.end,
624
- text: envKeyText,
625
- });
626
- updated = true;
627
- }
628
- }
629
- else if (envKeyText !== null) {
630
- inserts.push(`env_key = ${envKeyText}`);
631
- }
632
568
  if (inserts.length > 0) {
633
569
  operations.push({
634
570
  kind: "insert-at",
@@ -32,9 +32,6 @@ function validateProvidersShape(input) {
32
32
  if (typeof provider.apiKey !== "string" || provider.apiKey.trim() === "") {
33
33
  throw new Error(`Provider "${name}" is missing a valid apiKey.`);
34
34
  }
35
- if (typeof provider.envKey !== "string" || provider.envKey.trim() === "") {
36
- throw new Error(`Provider "${name}" is missing a valid envKey.`);
37
- }
38
35
  if (provider.baseUrl !== undefined && typeof provider.baseUrl !== "string") {
39
36
  throw new Error(`Provider "${name}" has an invalid baseUrl.`);
40
37
  }
@@ -56,7 +53,6 @@ function validateProvidersShape(input) {
56
53
  providers[name] = cleanProviderRecord({
57
54
  profile: provider.profile,
58
55
  apiKey: provider.apiKey,
59
- envKey: provider.envKey,
60
56
  baseUrl: provider.baseUrl,
61
57
  note: provider.note,
62
58
  tags: provider.tags,
@@ -72,7 +68,6 @@ function cleanProviderRecord(record) {
72
68
  const next = {
73
69
  profile: record.profile.trim(),
74
70
  apiKey: record.apiKey.trim(),
75
- envKey: record.envKey.trim(),
76
71
  };
77
72
  if (record.baseUrl && record.baseUrl.trim() !== "") {
78
73
  next.baseUrl = record.baseUrl.trim();
@@ -8,7 +8,8 @@ exports.inspectLiveStateDrift = inspectLiveStateDrift;
8
8
  function getStorageRoles() {
9
9
  return {
10
10
  managementSSOT: "providers.json",
11
- runtimeMirrors: ["config.toml", "auth.json"],
11
+ runtimeMirrors: ["config.toml"],
12
+ authStateFile: "auth.json",
12
13
  rollbackState: "backups/latest.json",
13
14
  };
14
15
  }