@minniexcode/codex-switch 0.0.9 → 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 (36) 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 +0 -11
  5. package/dist/app/bridge.js +0 -1
  6. package/dist/app/edit-provider.js +1 -17
  7. package/dist/app/get-status.js +24 -9
  8. package/dist/app/list-providers.js +0 -1
  9. package/dist/app/run-doctor.js +11 -36
  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 +5 -20
  13. package/dist/cli/output.js +4 -6
  14. package/dist/cli.js +1 -1
  15. package/dist/commands/handlers.js +192 -39
  16. package/dist/commands/registry.js +7 -5
  17. package/dist/domain/config.js +4 -68
  18. package/dist/domain/providers.js +0 -5
  19. package/dist/domain/runtime-state.js +2 -1
  20. package/dist/domain/setup.js +58 -3
  21. package/dist/interaction/add-interactive.js +55 -1
  22. package/dist/interaction/interactive.js +1 -5
  23. package/dist/runtime/copilot-adapter.js +44 -1
  24. package/dist/runtime/copilot-bridge.js +2 -2
  25. package/dist/runtime/copilot-cli.js +70 -0
  26. package/dist/runtime/copilot-installer.js +49 -2
  27. package/dist/storage/auth-repo.js +28 -77
  28. package/dist/storage/config-repo.js +1 -36
  29. package/dist/storage/runtime-state-repo.js +32 -0
  30. package/docs/Design/codex-switch-copilot-integration-design.md +517 -0
  31. package/docs/Design/codex-switch-v0.0.10-design.md +669 -0
  32. package/docs/PRD/codex-switch-prd-v0.0.10.md +406 -0
  33. package/docs/cli-usage.md +38 -14
  34. package/docs/codex-switch-product-overview.md +2 -2
  35. package/docs/codex-switch-technical-architecture.md +6 -5
  36. 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");
@@ -257,40 +260,70 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
257
260
  let tags = parsed.commandOptions.get("--tag") ?? [];
258
261
  let createProfile = (0, args_1.hasFlag)(parsed.commandOptions, "--create-profile");
259
262
  const copilot = (0, args_1.hasFlag)(parsed.commandOptions, "--copilot");
260
- const bridgeHost = (0, args_1.getSingleOption)(parsed.commandOptions, "--bridge-host", false);
263
+ let bridgeHost = (0, args_1.getSingleOption)(parsed.commandOptions, "--bridge-host", false);
261
264
  const bridgePortValue = (0, args_1.getSingleOption)(parsed.commandOptions, "--bridge-port", false);
262
- 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);
263
266
  let installCopilotSdk = (0, args_1.hasFlag)(parsed.commandOptions, "--install-copilot-sdk");
264
- const bridgePort = bridgePortValue ? Number(bridgePortValue) : null;
267
+ let bridgePort = bridgePortValue ? Number(bridgePortValue) : null;
265
268
  if (copilot && apiKey) {
266
269
  throw (0, errors_1.cliError)("INVALID_ARGUMENT", "--copilot does not allow --api-key. Use --bridge-api-key for the local bridge secret.");
267
270
  }
268
271
  if (bridgePortValue && (!Number.isInteger(bridgePort) || bridgePort === null || bridgePort <= 0)) {
269
272
  throw (0, errors_1.cliError)("INVALID_ARGUMENT", "--bridge-port must be a positive integer.");
270
273
  }
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?");
274
+ if (copilot) {
275
+ installCopilotSdk = await ensureCopilotReadyForAdd({
276
+ runtime,
277
+ json: ctx.options.json,
278
+ installCopilotSdk,
279
+ });
273
280
  }
274
281
  if (!providerName || !profile || (!apiKey && !copilot)) {
275
282
  if (ctx.options.json || !runtime.isInteractive()) {
276
- 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;
277
326
  }
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
327
  }
295
328
  return (0, add_provider_1.addProvider)({
296
329
  codexDir: paths.codexDir,
@@ -433,9 +466,25 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
433
466
  if (overwrite && merge) {
434
467
  throw (0, errors_1.cliError)("INVALID_ARGUMENT", "migrate does not allow both --merge and --overwrite.");
435
468
  }
436
- let strategy = overwrite ? "overwrite" : merge ? "merge" : null;
437
469
  const providersExists = fs.existsSync(setupPaths.providersPath);
438
- 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) {
439
488
  if (!(0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
440
489
  throw (0, errors_1.cliError)("PROVIDERS_ALREADY_EXISTS", "providers.json already exists. Pass --merge or --overwrite.", {
441
490
  file: setupPaths.providersPath,
@@ -447,35 +496,35 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
447
496
  }
448
497
  strategy = selected;
449
498
  }
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();
499
+ const adoptableProfiles = adoptability.adoptableProfileDetails;
461
500
  let adoptProfiles = [];
462
501
  let providerDetailsByProfile = {};
463
502
  if ((0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
464
503
  adoptProfiles = await (0, interactive_1.chooseSetupProfiles)(runtime, adoptableProfiles);
465
504
  // 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) => {
505
+ const collectedDetails = await (0, interactive_1.collectSetupProviderDetails)(runtime, adoptProfiles, adoptableProfiles.reduce((accumulator, profile) => {
467
506
  accumulator[profile.name] = {
468
507
  providerName: profile.name,
469
- envKey: profile.envKey,
470
508
  baseUrl: profile.baseUrl,
471
509
  };
472
510
  return accumulator;
473
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
+ ]));
474
522
  }
475
523
  else {
476
524
  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,
525
+ availableProfiles: adoptability.availableProfiles,
526
+ adoptableProfiles: adoptability.adoptableProfiles,
527
+ blockingReasonsByProfile: adoptability.blockingReasonsByProfile,
479
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.",
480
529
  });
481
530
  }
@@ -514,3 +563,107 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
514
563
  throw (0, errors_1.cliError)("UNKNOWN_COMMAND", `Unknown command: ${ctx.command}`);
515
564
  }
516
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
+ }
@@ -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"],
@@ -108,7 +108,7 @@ exports.COMMANDS = [
108
108
  details: [
109
109
  "Reads config.toml profiles, collects complete provider records, then writes providers.json under managed backup flow.",
110
110
  "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.",
111
+ "Migrate adopts only runtime profiles that already expose model, model_provider, and matching base_url.",
112
112
  "Non-TTY and --json runs still fail fast because migrate profile selection and provider details remain interactive in this release.",
113
113
  ],
114
114
  examples: ["codexs migrate", "codexs migrate --overwrite --json --codex-dir ~/.codex"],
@@ -215,6 +215,7 @@ exports.COMMANDS = [
215
215
  "Automation and non-TTY environments must pass all required values explicitly.",
216
216
  "Creating a missing profile section requires --create-profile together with --model and --base-url.",
217
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.",
218
219
  ],
219
220
  examples: [
220
221
  "codexs add packycode --profile packycode --api-key sk-xxx",
@@ -227,14 +228,15 @@ exports.COMMANDS = [
227
228
  tokens: ["switch"],
228
229
  handler: handlers_1.handleRegisteredCommand,
229
230
  group: "write",
230
- summary: "Switch to a provider and rewrite the managed auth mirror.",
231
+ summary: "Switch the active config profile to a provider.",
231
232
  usage: ["codexs switch <provider> [--json] [--codex-dir <path>]"],
232
233
  details: [
233
234
  "When <provider> is omitted in a TTY, an interactive provider selector is shown.",
234
235
  "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.",
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.",
236
238
  "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.",
239
+ "Backs up config.toml and auth.json and rolls back on failure.",
238
240
  ],
239
241
  examples: ["codexs switch freemodel", "codexs switch packycode --json"],
240
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
  }
@@ -2,21 +2,23 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildSetupDrafts = buildSetupDrafts;
4
4
  exports.findIncompleteSetupProfiles = findIncompleteSetupProfiles;
5
+ exports.collectMigrateAdoptability = collectMigrateAdoptability;
6
+ const config_1 = require("./config");
5
7
  const providers_1 = require("./providers");
6
8
  /**
7
9
  * Creates initial provider drafts from config profile names.
8
10
  */
9
- function buildSetupDrafts(profiles, detailsByProfile) {
11
+ function buildSetupDrafts(profiles, detailsByProfile, runtimeByProfile) {
10
12
  return profiles.map((profile) => {
11
13
  const detail = detailsByProfile[profile] ?? {};
14
+ const runtime = runtimeByProfile[profile];
12
15
  const providerName = (detail.providerName ?? profile).trim();
13
16
  return {
14
17
  providerName,
15
18
  record: (0, providers_1.cleanProviderRecord)({
16
19
  profile,
17
20
  apiKey: detail.apiKey ?? "",
18
- envKey: detail.envKey ?? "",
19
- baseUrl: detail.baseUrl,
21
+ baseUrl: detail.baseUrl ?? runtime?.baseUrl,
20
22
  note: detail.note,
21
23
  tags: detail.tags,
22
24
  }),
@@ -29,3 +31,56 @@ function buildSetupDrafts(profiles, detailsByProfile) {
29
31
  function findIncompleteSetupProfiles(drafts) {
30
32
  return drafts.filter((draft) => draft.record.apiKey.trim() === "").map((draft) => draft.record.profile);
31
33
  }
34
+ /**
35
+ * Collects the unmanaged profiles that can be safely adopted by migrate.
36
+ */
37
+ function collectMigrateAdoptability(document, providers) {
38
+ const views = (0, config_1.buildManagedProfileViews)(document, providers)
39
+ .filter((view) => view.source !== "orphaned-reference")
40
+ .sort((left, right) => left.name.localeCompare(right.name));
41
+ const modelProvidersByName = new Map(document.modelProviders.map((provider) => [provider.name, provider]));
42
+ const availableProfiles = views.map((view) => view.name);
43
+ const adoptableProfileDetails = [];
44
+ const blockingReasonsByProfile = {};
45
+ for (const view of views) {
46
+ const reasons = [];
47
+ if (!view.model) {
48
+ reasons.push("model is missing.");
49
+ }
50
+ if (!view.modelProvider) {
51
+ reasons.push("model_provider is missing.");
52
+ }
53
+ else {
54
+ if (view.modelProvider !== view.name) {
55
+ reasons.push(`model_provider must match the profile name "${view.name}".`);
56
+ }
57
+ const modelProviderSection = modelProvidersByName.get(view.modelProvider);
58
+ if (!modelProviderSection) {
59
+ reasons.push(`model_providers.${view.modelProvider} section is missing.`);
60
+ }
61
+ else {
62
+ if (!modelProviderSection.baseUrl) {
63
+ reasons.push(`model_providers.${view.modelProvider}.base_url is missing.`);
64
+ }
65
+ }
66
+ }
67
+ if (view.source !== "unmanaged") {
68
+ reasons.push("profile is already managed by providers.json.");
69
+ }
70
+ if (reasons.length === 0) {
71
+ adoptableProfileDetails.push({
72
+ name: view.name,
73
+ model: view.model,
74
+ baseUrl: view.baseUrl,
75
+ });
76
+ continue;
77
+ }
78
+ blockingReasonsByProfile[view.name] = reasons;
79
+ }
80
+ return {
81
+ availableProfiles,
82
+ adoptableProfiles: adoptableProfileDetails.map((profile) => profile.name),
83
+ blockingReasonsByProfile,
84
+ adoptableProfileDetails,
85
+ };
86
+ }