@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
@@ -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;
@@ -118,10 +117,15 @@ function parseStructuredConfig(configContent) {
118
117
  name: modelProviderHeaderMatch[1],
119
118
  sectionStart: line.start,
120
119
  sectionEnd: configContent.length,
120
+ managedFieldInsertIndex: configContent.length,
121
121
  baseUrlValueRange: null,
122
122
  baseUrl: null,
123
- envKeyValueRange: null,
124
- envKey: null,
123
+ nameValueRange: null,
124
+ providerName: null,
125
+ requiresOpenAiAuthValueRange: null,
126
+ requiresOpenAiAuth: null,
127
+ wireApiValueRange: null,
128
+ wireApi: null,
125
129
  };
126
130
  modelProviders.push(currentModelProvider);
127
131
  inRoot = false;
@@ -176,12 +180,28 @@ function parseStructuredConfig(configContent) {
176
180
  end: line.start + baseUrlMatch.valueEnd,
177
181
  };
178
182
  }
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,
183
+ const nameMatch = matchKeyValueLine(line.content, "name");
184
+ if (nameMatch) {
185
+ currentModelProvider.providerName = nameMatch.value;
186
+ currentModelProvider.nameValueRange = {
187
+ start: line.start + nameMatch.valueStart,
188
+ end: line.start + nameMatch.valueEnd,
189
+ };
190
+ }
191
+ const requiresOpenAiAuthMatch = matchBooleanKeyValueLine(line.content, "requires_openai_auth");
192
+ if (requiresOpenAiAuthMatch) {
193
+ currentModelProvider.requiresOpenAiAuth = requiresOpenAiAuthMatch.value;
194
+ currentModelProvider.requiresOpenAiAuthValueRange = {
195
+ start: line.start + requiresOpenAiAuthMatch.valueStart,
196
+ end: line.start + requiresOpenAiAuthMatch.valueEnd,
197
+ };
198
+ }
199
+ const wireApiMatch = matchKeyValueLine(line.content, "wire_api");
200
+ if (wireApiMatch) {
201
+ currentModelProvider.wireApi = wireApiMatch.value;
202
+ currentModelProvider.wireApiValueRange = {
203
+ start: line.start + wireApiMatch.valueStart,
204
+ end: line.start + wireApiMatch.valueEnd,
185
205
  };
186
206
  }
187
207
  }
@@ -195,7 +215,10 @@ function parseStructuredConfig(configContent) {
195
215
  ...profile,
196
216
  managedFieldInsertIndex: findManagedFieldInsertIndex(configContent, profile.sectionStart, profile.sectionEnd),
197
217
  })),
198
- modelProviders,
218
+ modelProviders: modelProviders.map((provider) => ({
219
+ ...provider,
220
+ managedFieldInsertIndex: findManagedFieldInsertIndex(configContent, provider.sectionStart, provider.sectionEnd),
221
+ })),
199
222
  };
200
223
  }
201
224
  /**
@@ -218,7 +241,6 @@ function buildManagedProfileViews(document, providers) {
218
241
  model: section.model,
219
242
  modelProvider: section.modelProvider,
220
243
  baseUrl: modelProviderSection?.baseUrl ?? null,
221
- envKey: modelProviderSection?.envKey ?? null,
222
244
  managedFields: collectManagedFields(section.model, section.modelProvider),
223
245
  source: linkInfo.managed ? "managed" : "unmanaged",
224
246
  });
@@ -235,7 +257,6 @@ function buildManagedProfileViews(document, providers) {
235
257
  model: null,
236
258
  modelProvider: null,
237
259
  baseUrl: null,
238
- envKey: null,
239
260
  managedFields: [],
240
261
  source: "orphaned-reference",
241
262
  });
@@ -298,28 +319,6 @@ function collectConfigConsistencyIssues(document, providers) {
298
319
  modelProvider: view.modelProvider,
299
320
  });
300
321
  }
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
322
  }
324
323
  }
325
324
  }
@@ -366,17 +365,6 @@ function validateManagedProfileCreation(profile, fields) {
366
365
  modelProvider,
367
366
  };
368
367
  }
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
368
  /**
381
369
  * Computes keep/delete/switch outcomes when a provider leaves or changes profiles.
382
370
  */
@@ -491,26 +479,29 @@ function planConfigMutation(document, args) {
491
479
  const section = modelProviderSectionMap.get(profileName);
492
480
  if (!section) {
493
481
  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.`, {
482
+ const providerName = fields.name?.trim() ?? "";
483
+ if (!baseUrl) {
484
+ throw (0, errors_1.cliError)("MANAGED_PROFILE_FIELDS_MISSING", `Model provider "${profileName}" requires base_url.`, {
497
485
  profile: profileName,
498
486
  modelProvider: profileName,
499
487
  missingFields: [
500
488
  !baseUrl ? "base_url" : null,
501
- !envKey ? "env_key" : null,
502
489
  ].filter((value) => Boolean(value)),
503
490
  });
504
491
  }
505
492
  const prefix = document.rawText.length > 0 && !document.rawText.endsWith(document.lineEnding)
506
493
  ? document.lineEnding
507
494
  : "";
495
+ const requiresOpenAiAuth = fields.requiresOpenAiAuth;
496
+ const wireApi = fields.wireApi?.trim() ?? "";
508
497
  operations.push({
509
498
  kind: "insert-at",
510
499
  index: document.rawText.length,
511
500
  text: `${prefix}[model_providers.${profileName}]${document.lineEnding}` +
512
501
  `base_url = ${JSON.stringify(baseUrl)}${document.lineEnding}` +
513
- `env_key = ${JSON.stringify(envKey)}${document.lineEnding}`,
502
+ (providerName ? `name = ${JSON.stringify(providerName)}${document.lineEnding}` : "") +
503
+ (requiresOpenAiAuth !== undefined ? `requires_openai_auth = ${String(requiresOpenAiAuth)}${document.lineEnding}` : "") +
504
+ (wireApi ? `wire_api = ${JSON.stringify(wireApi)}${document.lineEnding}` : ""),
514
505
  });
515
506
  createdModelProviderSections.push(profileName);
516
507
  continue;
@@ -594,12 +585,14 @@ function planSectionFieldMutation(document, section, fields, operations) {
594
585
  return updated;
595
586
  }
596
587
  /**
597
- * Plans base_url/env_key updates for one model_providers section.
588
+ * Plans managed field updates for one model_providers section.
598
589
  */
599
590
  function planModelProviderFieldMutation(section, fields, operations) {
600
591
  let updated = false;
601
592
  const baseUrlText = fields.baseUrl !== undefined ? JSON.stringify(fields.baseUrl) : null;
602
- const envKeyText = fields.envKey !== undefined ? JSON.stringify(fields.envKey) : null;
593
+ const nameText = fields.name !== undefined ? JSON.stringify(fields.name) : null;
594
+ const requiresOpenAiAuthText = fields.requiresOpenAiAuth !== undefined ? String(fields.requiresOpenAiAuth) : null;
595
+ const wireApiText = fields.wireApi !== undefined ? JSON.stringify(fields.wireApi) : null;
603
596
  const inserts = [];
604
597
  if (baseUrlText !== null && section.baseUrlValueRange) {
605
598
  if (section.baseUrl !== fields.baseUrl) {
@@ -614,28 +607,59 @@ function planModelProviderFieldMutation(section, fields, operations) {
614
607
  }
615
608
  else if (baseUrlText !== null) {
616
609
  inserts.push(`base_url = ${baseUrlText}`);
610
+ updated = true;
617
611
  }
618
- if (envKeyText !== null && section.envKeyValueRange) {
619
- if (section.envKey !== fields.envKey) {
612
+ if (nameText !== null && section.nameValueRange) {
613
+ if (section.providerName !== fields.name) {
620
614
  operations.push({
621
615
  kind: "replace-range",
622
- start: section.envKeyValueRange.start,
623
- end: section.envKeyValueRange.end,
624
- text: envKeyText,
616
+ start: section.nameValueRange.start,
617
+ end: section.nameValueRange.end,
618
+ text: nameText,
625
619
  });
626
620
  updated = true;
627
621
  }
628
622
  }
629
- else if (envKeyText !== null) {
630
- inserts.push(`env_key = ${envKeyText}`);
623
+ else if (nameText !== null) {
624
+ inserts.push(`name = ${nameText}`);
625
+ updated = true;
626
+ }
627
+ if (requiresOpenAiAuthText !== null && section.requiresOpenAiAuthValueRange) {
628
+ if (section.requiresOpenAiAuth !== fields.requiresOpenAiAuth) {
629
+ operations.push({
630
+ kind: "replace-range",
631
+ start: section.requiresOpenAiAuthValueRange.start,
632
+ end: section.requiresOpenAiAuthValueRange.end,
633
+ text: requiresOpenAiAuthText,
634
+ });
635
+ updated = true;
636
+ }
637
+ }
638
+ else if (requiresOpenAiAuthText !== null) {
639
+ inserts.push(`requires_openai_auth = ${requiresOpenAiAuthText}`);
640
+ updated = true;
641
+ }
642
+ if (wireApiText !== null && section.wireApiValueRange) {
643
+ if (section.wireApi !== fields.wireApi) {
644
+ operations.push({
645
+ kind: "replace-range",
646
+ start: section.wireApiValueRange.start,
647
+ end: section.wireApiValueRange.end,
648
+ text: wireApiText,
649
+ });
650
+ updated = true;
651
+ }
652
+ }
653
+ else if (wireApiText !== null) {
654
+ inserts.push(`wire_api = ${wireApiText}`);
655
+ updated = true;
631
656
  }
632
657
  if (inserts.length > 0) {
633
658
  operations.push({
634
659
  kind: "insert-at",
635
- index: section.sectionEnd,
660
+ index: section.managedFieldInsertIndex,
636
661
  text: `${inserts.join("\n")}\n`,
637
662
  });
638
- updated = true;
639
663
  }
640
664
  return updated;
641
665
  }
@@ -680,6 +704,23 @@ function matchKeyValueLine(line, key) {
680
704
  valueEnd,
681
705
  };
682
706
  }
707
+ function matchBooleanKeyValueLine(line, key) {
708
+ const match = line.match(new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*(true|false)\\s*(#.*)?$`));
709
+ if (!match || match.index === undefined) {
710
+ return null;
711
+ }
712
+ const value = match[1] === "true";
713
+ const valueStart = line.indexOf(match[1], match.index);
714
+ if (valueStart === -1) {
715
+ return null;
716
+ }
717
+ const valueEnd = valueStart + match[1].length;
718
+ return {
719
+ value,
720
+ valueStart,
721
+ valueEnd,
722
+ };
723
+ }
683
724
  function findManagedFieldInsertIndex(rawText, sectionStart, sectionEnd) {
684
725
  const sectionText = rawText.slice(sectionStart, sectionEnd);
685
726
  const lines = splitWithOffsets(sectionText);
@@ -9,6 +9,7 @@ exports.maskSecret = maskSecret;
9
9
  exports.isRuntimeBackedProvider = isRuntimeBackedProvider;
10
10
  exports.isCopilotBridgeProvider = isCopilotBridgeProvider;
11
11
  exports.buildCopilotBridgeBaseUrl = buildCopilotBridgeBaseUrl;
12
+ exports.buildCopilotModelProviderProjection = buildCopilotModelProviderProjection;
12
13
  /**
13
14
  * Validates and normalizes unknown JSON into the providers.json domain model.
14
15
  */
@@ -32,9 +33,6 @@ function validateProvidersShape(input) {
32
33
  if (typeof provider.apiKey !== "string" || provider.apiKey.trim() === "") {
33
34
  throw new Error(`Provider "${name}" is missing a valid apiKey.`);
34
35
  }
35
- if (typeof provider.envKey !== "string" || provider.envKey.trim() === "") {
36
- throw new Error(`Provider "${name}" is missing a valid envKey.`);
37
- }
38
36
  if (provider.baseUrl !== undefined && typeof provider.baseUrl !== "string") {
39
37
  throw new Error(`Provider "${name}" has an invalid baseUrl.`);
40
38
  }
@@ -56,7 +54,6 @@ function validateProvidersShape(input) {
56
54
  providers[name] = cleanProviderRecord({
57
55
  profile: provider.profile,
58
56
  apiKey: provider.apiKey,
59
- envKey: provider.envKey,
60
57
  baseUrl: provider.baseUrl,
61
58
  note: provider.note,
62
59
  tags: provider.tags,
@@ -72,7 +69,6 @@ function cleanProviderRecord(record) {
72
69
  const next = {
73
70
  profile: record.profile.trim(),
74
71
  apiKey: record.apiKey.trim(),
75
- envKey: record.envKey.trim(),
76
72
  };
77
73
  if (record.baseUrl && record.baseUrl.trim() !== "") {
78
74
  next.baseUrl = record.baseUrl.trim();
@@ -155,6 +151,17 @@ function isCopilotBridgeProvider(provider) {
155
151
  function buildCopilotBridgeBaseUrl(runtime) {
156
152
  return `http://${runtime.bridgeHost}:${runtime.bridgePort}${runtime.bridgePath}`;
157
153
  }
154
+ /**
155
+ * Builds the Codex-facing custom model_provider projection for the managed Copilot bridge.
156
+ */
157
+ function buildCopilotModelProviderProjection(runtime) {
158
+ return {
159
+ baseUrl: buildCopilotBridgeBaseUrl(runtime),
160
+ name: "copilot",
161
+ requiresOpenAiAuth: true,
162
+ wireApi: "responses",
163
+ };
164
+ }
158
165
  /**
159
166
  * Validates one runtime-backed provider block.
160
167
  */
@@ -1,15 +1,92 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.getStorageRoles = getStorageRoles;
4
37
  exports.inspectLiveStateDrift = inspectLiveStateDrift;
38
+ const path = __importStar(require("node:path"));
5
39
  /**
6
40
  * Returns the stable storage contract used by the CLI.
7
41
  */
8
- function getStorageRoles() {
42
+ function getStorageRoles(args) {
43
+ const toolHomeDir = path.dirname(path.resolve(args.providersPath));
44
+ const backupsDir = path.join(toolHomeDir, "backups");
45
+ const runtimeDir = args.runtimeDir ? path.resolve(args.runtimeDir) : path.join(toolHomeDir, "runtime");
46
+ const runtimesDir = args.runtimesDir ? path.resolve(args.runtimesDir) : path.join(toolHomeDir, "runtimes");
9
47
  return {
10
- managementSSOT: "providers.json",
11
- runtimeMirrors: ["config.toml", "auth.json"],
12
- rollbackState: "backups/latest.json",
48
+ toolHome: {
49
+ root: toolHomeDir,
50
+ toolConfig: path.join(toolHomeDir, "codex-switch.json"),
51
+ providers: path.resolve(args.providersPath),
52
+ backupsDir,
53
+ latestBackup: path.join(backupsDir, "latest.json"),
54
+ runtimeStateDir: runtimeDir,
55
+ runtimeInstallDir: runtimesDir,
56
+ },
57
+ targetRuntime: {
58
+ root: path.resolve(args.codexDir),
59
+ config: path.resolve(args.configPath),
60
+ auth: path.resolve(args.authPath),
61
+ },
62
+ managementSSOT: {
63
+ scope: "toolHome",
64
+ path: path.resolve(args.providersPath),
65
+ },
66
+ runtimeMirrors: [
67
+ {
68
+ scope: "targetRuntime",
69
+ path: path.resolve(args.configPath),
70
+ },
71
+ ],
72
+ authStateFile: {
73
+ scope: "targetRuntime",
74
+ path: path.resolve(args.authPath),
75
+ },
76
+ rollbackState: {
77
+ scope: "toolHome",
78
+ path: path.join(backupsDir, "latest.json"),
79
+ },
80
+ runtimeState: {
81
+ scope: "toolHome",
82
+ path: runtimeDir,
83
+ managedBackup: false,
84
+ },
85
+ runtimeInstall: {
86
+ scope: "toolHome",
87
+ path: runtimesDir,
88
+ managedBackup: false,
89
+ },
13
90
  };
14
91
  }
15
92
  /**
@@ -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
+ }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.COMMON_TAG_CHOICES = void 0;
4
4
  exports.collectAddInput = collectAddInput;
5
+ exports.collectCopilotAddInput = collectCopilotAddInput;
5
6
  exports.createNonInteractiveAddError = createNonInteractiveAddError;
6
7
  exports.promptTags = promptTags;
7
8
  const errors_1 = require("../domain/errors");
@@ -37,10 +38,51 @@ async function collectAddInput(runtime, defaults, providerExists, profileExists)
37
38
  tags,
38
39
  };
39
40
  }
41
+ /**
42
+ * Collects Copilot add command inputs interactively when required values are missing.
43
+ */
44
+ async function collectCopilotAddInput(runtime, defaults, providerExists, profileExists, options) {
45
+ runtime.writeLine("Interactive add mode");
46
+ runtime.writeLine("Provide the missing Copilot provider fields. Press Enter to keep optional values empty.");
47
+ const providerName = defaults.providerName
48
+ ? normalizeRequiredValue(defaults.providerName)
49
+ : await promptProviderName(runtime, providerExists);
50
+ const profile = defaults.profile ? normalizeRequiredValue(defaults.profile) : await promptRequiredValue(runtime, "Profile");
51
+ const createProfile = !profileExists(profile);
52
+ const model = createProfile
53
+ ? defaults.model
54
+ ? normalizeRequiredValue(defaults.model)
55
+ : await promptRequiredValue(runtime, `Model for new profile "${profile}"`)
56
+ : defaults.model ?? null;
57
+ const note = defaults.note ?? normalizeOptionalValue(await runtime.inputText("Note (optional)"));
58
+ const tags = defaults.tags.length > 0 ? defaults.tags : await promptTags(runtime);
59
+ const bridgeHost = options?.bridgeHost ?? normalizeOptionalValue(await runtime.inputText("Bridge host (optional)", { defaultValue: "127.0.0.1" }));
60
+ const bridgePortText = options?.bridgePort !== undefined && options.bridgePort !== null
61
+ ? String(options.bridgePort)
62
+ : await runtime.inputText("Bridge port (optional)", { defaultValue: "41415" });
63
+ const bridgePort = normalizeBridgePort(runtime, bridgePortText);
64
+ const bridgeApiKey = options?.bridgeApiKey ?? normalizeOptionalValue(await runtime.inputSecret("Bridge API key (optional)"));
65
+ return {
66
+ providerName,
67
+ profile,
68
+ createProfile,
69
+ model,
70
+ note,
71
+ tags,
72
+ bridgeApiKey,
73
+ bridgeHost,
74
+ bridgePort,
75
+ };
76
+ }
40
77
  /**
41
78
  * Throws a consistent error when interactive add is unavailable.
42
79
  */
43
- function createNonInteractiveAddError() {
80
+ function createNonInteractiveAddError(options) {
81
+ if (options?.copilot) {
82
+ return (0, errors_1.cliError)("INVALID_ARGUMENT", "add --copilot requires <provider> and --profile when running without an interactive TTY.", {
83
+ suggestion: "Run in a terminal TTY or pass <provider>, --profile, and any optional Copilot bridge flags explicitly.",
84
+ });
85
+ }
44
86
  return (0, errors_1.cliError)("INVALID_ARGUMENT", "add requires <provider>, --profile, and --api-key when running without an interactive TTY.", {
45
87
  suggestion: "Run in a terminal TTY or pass all required values explicitly.",
46
88
  });
@@ -90,6 +132,18 @@ function normalizeOptionalValue(value) {
90
132
  const normalized = value.trim();
91
133
  return normalized === "" ? null : normalized;
92
134
  }
135
+ function normalizeBridgePort(runtime, value) {
136
+ const normalized = value.trim();
137
+ if (normalized === "") {
138
+ return null;
139
+ }
140
+ const parsed = Number(normalized);
141
+ if (Number.isInteger(parsed) && parsed > 0) {
142
+ return parsed;
143
+ }
144
+ runtime.writeLine("Bridge port must be a positive integer. Falling back to the default port.");
145
+ return null;
146
+ }
93
147
  async function promptTags(runtime, defaults = []) {
94
148
  const defaultPresetTags = defaults.filter(isCommonTag);
95
149
  return runtime.selectMany("Select tags (optional)", exports.COMMON_TAG_CHOICES.map((tag) => ({ value: tag, label: tag })), { defaultValues: defaultPresetTags });
@@ -222,7 +222,7 @@ async function chooseSetupProfiles(runtime, profiles) {
222
222
  return runtime.selectMany("Choose unmanaged config profiles to adopt into providers.json.", profiles.map((profile) => ({
223
223
  value: profile.name,
224
224
  label: profile.name,
225
- hint: `${profile.model} | ${profile.baseUrl} | ${profile.envKey}`,
225
+ hint: `${profile.model} | ${profile.baseUrl}`,
226
226
  })));
227
227
  }
228
228
  /**
@@ -235,9 +235,6 @@ async function collectSetupProviderDetails(runtime, profiles, defaultsByProfile
235
235
  const providerName = (await runtime.inputText(`Provider name for profile "${profile}"`, {
236
236
  defaultValue: defaults.providerName ?? profile,
237
237
  })).trim();
238
- if (defaults.envKey) {
239
- runtime.writeLine(`Runtime env key for "${profile}": ${defaults.envKey}`);
240
- }
241
238
  const apiKey = await promptRequiredSecret(runtime, `API key for profile "${profile}"`, defaults.apiKey?.trim() || undefined);
242
239
  const baseUrl = (await runtime.inputText(`Base URL note for profile "${profile}" (optional)`, {
243
240
  defaultValue: defaults.baseUrl ?? "",
@@ -249,7 +246,6 @@ async function collectSetupProviderDetails(runtime, profiles, defaultsByProfile
249
246
  result[profile] = {
250
247
  providerName: providerName || defaults.providerName || profile,
251
248
  apiKey,
252
- envKey: defaults.envKey,
253
249
  baseUrl: baseUrl || defaults.baseUrl || undefined,
254
250
  note: note || defaults.note || undefined,
255
251
  // Empty selections are omitted so downstream setup validation can distinguish unset from explicit data.