@minniexcode/codex-switch 0.0.5 → 0.0.7

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 (71) hide show
  1. package/README.AI.md +5 -2
  2. package/README.md +44 -100
  3. package/dist/app/add-provider.js +28 -4
  4. package/dist/app/edit-provider.js +47 -19
  5. package/dist/app/export-providers.js +2 -2
  6. package/dist/app/get-current-profile.js +1 -1
  7. package/dist/app/get-status.js +10 -3
  8. package/dist/app/import-providers.js +15 -7
  9. package/dist/app/init-codex.js +68 -0
  10. package/dist/app/list-backups.js +1 -1
  11. package/dist/app/list-config-profiles.js +3 -2
  12. package/dist/app/list-providers.js +2 -1
  13. package/dist/app/remove-provider.js +2 -2
  14. package/dist/app/rollback-backup.js +1 -1
  15. package/dist/app/rollback-latest.js +1 -1
  16. package/dist/app/run-doctor.js +83 -6
  17. package/dist/app/run-mutation.js +2 -2
  18. package/dist/app/setup-codex.js +21 -12
  19. package/dist/app/show-config.js +11 -3
  20. package/dist/app/show-provider.js +1 -1
  21. package/dist/app/switch-provider.js +16 -9
  22. package/dist/cli/add-interactive.js +7 -104
  23. package/dist/cli/args.js +6 -135
  24. package/dist/cli/help.js +8 -313
  25. package/dist/cli/interactive.js +17 -225
  26. package/dist/cli/output.js +21 -6
  27. package/dist/cli/prompt.js +4 -106
  28. package/dist/cli.js +10 -404
  29. package/dist/commands/args.js +132 -0
  30. package/dist/commands/dispatch.js +16 -0
  31. package/dist/commands/handlers.js +460 -0
  32. package/dist/commands/help.js +120 -0
  33. package/dist/commands/registry.js +351 -0
  34. package/dist/commands/types.js +2 -0
  35. package/dist/domain/config.js +235 -21
  36. package/dist/domain/providers.js +16 -2
  37. package/dist/domain/setup.js +1 -0
  38. package/dist/infra/backup-repo.js +9 -206
  39. package/dist/infra/codex-cli.js +9 -126
  40. package/dist/infra/codex-paths.js +6 -67
  41. package/dist/infra/config-repo.js +59 -0
  42. package/dist/infra/fs-utils.js +8 -93
  43. package/dist/infra/lock-repo.js +4 -95
  44. package/dist/infra/providers-repo.js +8 -94
  45. package/dist/interaction/add-interactive.js +99 -0
  46. package/dist/interaction/interactive.js +289 -0
  47. package/dist/interaction/prompt.js +110 -0
  48. package/dist/runtime/codex-cli.js +130 -0
  49. package/dist/runtime/codex-probe.js +57 -0
  50. package/dist/runtime/types.js +2 -0
  51. package/dist/storage/auth-repo.js +160 -0
  52. package/dist/storage/backup-repo.js +210 -0
  53. package/dist/storage/codex-paths.js +71 -0
  54. package/dist/storage/config-repo.js +266 -0
  55. package/dist/storage/fs-utils.js +97 -0
  56. package/dist/storage/lock-repo.js +99 -0
  57. package/dist/storage/providers-repo.js +98 -0
  58. package/docs/Design/codex-switch-v0.0.5-design.md +32 -22
  59. package/docs/Design/codex-switch-v0.0.6-design.md +708 -0
  60. package/docs/Design/codex-switch-v0.0.7-design.md +862 -0
  61. package/docs/PRD/codex-switch-prd-v0.0.5-to-v0.1.0.md +227 -89
  62. package/docs/PRD/codex-switch-prd-v0.1.0.md +200 -226
  63. package/docs/PRD/codex-switch-prd.md +1 -1
  64. package/docs/Reference/codex-config-reference.md +604 -0
  65. package/docs/Reference/codex-config-reference.zh-CN.md +633 -0
  66. package/docs/cli-usage.md +78 -29
  67. package/docs/codex-switch-technical-architecture.md +73 -4
  68. package/docs/test-report-0.0.5.md +163 -0
  69. package/docs/test-report-0.0.7.md +118 -0
  70. package/docs/testing.md +151 -0
  71. package/package.json +1 -1
@@ -41,6 +41,7 @@ exports.parseStructuredConfig = parseStructuredConfig;
41
41
  exports.buildManagedProfileViews = buildManagedProfileViews;
42
42
  exports.collectConfigConsistencyIssues = collectConfigConsistencyIssues;
43
43
  exports.validateManagedProfileCreation = validateManagedProfileCreation;
44
+ exports.buildManagedProfileEnvKey = buildManagedProfileEnvKey;
44
45
  exports.planProfileLifecycleOutcome = planProfileLifecycleOutcome;
45
46
  exports.planConfigMutation = planConfigMutation;
46
47
  exports.applyPatchOperations = applyPatchOperations;
@@ -74,7 +75,9 @@ function parseStructuredConfig(configContent) {
74
75
  let activeProfile = null;
75
76
  let activeProfileRange = null;
76
77
  const profiles = [];
78
+ const modelProviders = [];
77
79
  let currentProfile = null;
80
+ let currentModelProvider = null;
78
81
  let inRoot = true;
79
82
  for (const line of lines) {
80
83
  const trimmed = line.content.trim();
@@ -83,6 +86,10 @@ function parseStructuredConfig(configContent) {
83
86
  if (currentProfile) {
84
87
  currentProfile.sectionEnd = line.start;
85
88
  }
89
+ if (currentModelProvider) {
90
+ currentModelProvider.sectionEnd = line.start;
91
+ currentModelProvider = null;
92
+ }
86
93
  currentProfile = {
87
94
  name: headerMatch[1],
88
95
  headerStart: line.start,
@@ -90,19 +97,45 @@ function parseStructuredConfig(configContent) {
90
97
  sectionEnd: configContent.length,
91
98
  managedFieldInsertIndex: configContent.length,
92
99
  modelValueRange: null,
93
- baseUrlValueRange: null,
100
+ modelProviderValueRange: null,
94
101
  model: null,
95
- baseUrl: null,
102
+ modelProvider: null,
96
103
  };
97
104
  profiles.push(currentProfile);
98
105
  inRoot = false;
99
106
  continue;
100
107
  }
108
+ const modelProviderHeaderMatch = trimmed.match(/^\[model_providers\.([^\]]+)\]$/);
109
+ if (modelProviderHeaderMatch) {
110
+ if (currentProfile) {
111
+ currentProfile.sectionEnd = line.start;
112
+ currentProfile = null;
113
+ }
114
+ if (currentModelProvider) {
115
+ currentModelProvider.sectionEnd = line.start;
116
+ }
117
+ currentModelProvider = {
118
+ name: modelProviderHeaderMatch[1],
119
+ sectionStart: line.start,
120
+ sectionEnd: configContent.length,
121
+ baseUrlValueRange: null,
122
+ baseUrl: null,
123
+ envKeyValueRange: null,
124
+ envKey: null,
125
+ };
126
+ modelProviders.push(currentModelProvider);
127
+ inRoot = false;
128
+ continue;
129
+ }
101
130
  if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
102
131
  if (currentProfile) {
103
132
  currentProfile.sectionEnd = line.start;
104
133
  currentProfile = null;
105
134
  }
135
+ if (currentModelProvider) {
136
+ currentModelProvider.sectionEnd = line.start;
137
+ currentModelProvider = null;
138
+ }
106
139
  inRoot = false;
107
140
  continue;
108
141
  }
@@ -125,14 +158,32 @@ function parseStructuredConfig(configContent) {
125
158
  end: line.start + modelMatch.valueEnd,
126
159
  };
127
160
  }
161
+ const modelProviderMatch = matchKeyValueLine(line.content, "model_provider");
162
+ if (modelProviderMatch) {
163
+ currentProfile.modelProvider = modelProviderMatch.value;
164
+ currentProfile.modelProviderValueRange = {
165
+ start: line.start + modelProviderMatch.valueStart,
166
+ end: line.start + modelProviderMatch.valueEnd,
167
+ };
168
+ }
169
+ }
170
+ if (currentModelProvider) {
128
171
  const baseUrlMatch = matchKeyValueLine(line.content, "base_url");
129
172
  if (baseUrlMatch) {
130
- currentProfile.baseUrl = baseUrlMatch.value;
131
- currentProfile.baseUrlValueRange = {
173
+ currentModelProvider.baseUrl = baseUrlMatch.value;
174
+ currentModelProvider.baseUrlValueRange = {
132
175
  start: line.start + baseUrlMatch.valueStart,
133
176
  end: line.start + baseUrlMatch.valueEnd,
134
177
  };
135
178
  }
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
+ }
136
187
  }
137
188
  }
138
189
  return {
@@ -144,6 +195,7 @@ function parseStructuredConfig(configContent) {
144
195
  ...profile,
145
196
  managedFieldInsertIndex: findManagedFieldInsertIndex(configContent, profile.sectionStart, profile.sectionEnd),
146
197
  })),
198
+ modelProviders,
147
199
  };
148
200
  }
149
201
  /**
@@ -151,10 +203,12 @@ function parseStructuredConfig(configContent) {
151
203
  */
152
204
  function buildManagedProfileViews(document, providers) {
153
205
  const linkMap = buildProfileLinkMap(providers);
206
+ const modelProviderMap = new Map(document.modelProviders.map((provider) => [provider.name, provider]));
154
207
  const views = [];
155
208
  const seen = new Set();
156
209
  for (const section of document.profiles) {
157
210
  const linkInfo = linkMap.get(section.name) ?? { linkedProviders: [], managed: false };
211
+ const modelProviderSection = section.modelProvider ? modelProviderMap.get(section.modelProvider) ?? null : null;
158
212
  seen.add(section.name);
159
213
  views.push({
160
214
  name: section.name,
@@ -162,8 +216,10 @@ function buildManagedProfileViews(document, providers) {
162
216
  isActive: document.activeProfile === section.name,
163
217
  linkedProviders: [...linkInfo.linkedProviders].sort(),
164
218
  model: section.model,
165
- baseUrl: section.baseUrl,
166
- managedFields: collectManagedFields(section.model, section.baseUrl),
219
+ modelProvider: section.modelProvider,
220
+ baseUrl: modelProviderSection?.baseUrl ?? null,
221
+ envKey: modelProviderSection?.envKey ?? null,
222
+ managedFields: collectManagedFields(section.model, section.modelProvider),
167
223
  source: linkInfo.managed ? "managed" : "unmanaged",
168
224
  });
169
225
  }
@@ -177,7 +233,9 @@ function buildManagedProfileViews(document, providers) {
177
233
  isActive: document.activeProfile === profile,
178
234
  linkedProviders: [...linkInfo.linkedProviders].sort(),
179
235
  model: null,
236
+ modelProvider: null,
180
237
  baseUrl: null,
238
+ envKey: null,
181
239
  managedFields: [],
182
240
  source: "orphaned-reference",
183
241
  });
@@ -210,6 +268,60 @@ function collectConfigConsistencyIssues(document, providers) {
210
268
  providers: [...view.linkedProviders],
211
269
  });
212
270
  }
271
+ if (view.source !== "orphaned-reference") {
272
+ if (!view.modelProvider) {
273
+ issues.push({
274
+ code: "MODEL_PROVIDER_MISSING",
275
+ profile: view.name,
276
+ });
277
+ }
278
+ else {
279
+ if (view.modelProvider !== view.name) {
280
+ issues.push({
281
+ code: "MODEL_PROVIDER_NAME_MISMATCH",
282
+ profile: view.name,
283
+ modelProvider: view.modelProvider,
284
+ });
285
+ }
286
+ const modelProviderSection = document.modelProviders.find((entry) => entry.name === view.modelProvider);
287
+ if (!modelProviderSection) {
288
+ issues.push({
289
+ code: "MODEL_PROVIDER_SECTION_MISSING",
290
+ profile: view.name,
291
+ modelProvider: view.modelProvider,
292
+ });
293
+ }
294
+ else if (!modelProviderSection.baseUrl) {
295
+ issues.push({
296
+ code: "MODEL_PROVIDER_BASE_URL_MISSING",
297
+ profile: view.name,
298
+ modelProvider: view.modelProvider,
299
+ });
300
+ }
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
+ }
324
+ }
213
325
  }
214
326
  if (document.activeProfile) {
215
327
  const activeLinkInfo = buildProfileLinkMap(providers).get(document.activeProfile);
@@ -219,6 +331,13 @@ function collectConfigConsistencyIssues(document, providers) {
219
331
  profile: document.activeProfile,
220
332
  });
221
333
  }
334
+ else if (activeLinkInfo.linkedProviders.length > 1) {
335
+ issues.push({
336
+ code: "ACTIVE_PROVIDER_UNRESOLVED",
337
+ profile: document.activeProfile,
338
+ providers: [...activeLinkInfo.linkedProviders],
339
+ });
340
+ }
222
341
  }
223
342
  return issues.sort((left, right) => {
224
343
  if (left.profile === right.profile) {
@@ -232,21 +351,32 @@ function collectConfigConsistencyIssues(document, providers) {
232
351
  */
233
352
  function validateManagedProfileCreation(profile, fields) {
234
353
  const model = fields.model?.trim() ?? "";
235
- const baseUrl = fields.baseUrl?.trim() ?? "";
236
- if (!model || !baseUrl) {
237
- throw (0, errors_1.cliError)("MANAGED_PROFILE_FIELDS_MISSING", `Managed profile "${profile}" requires both model and base_url.`, {
354
+ const modelProvider = fields.modelProvider?.trim() ?? "";
355
+ if (!model || !modelProvider) {
356
+ throw (0, errors_1.cliError)("MANAGED_PROFILE_FIELDS_MISSING", `Managed profile "${profile}" requires both model and model_provider.`, {
238
357
  profile,
239
358
  missingFields: [
240
359
  !model ? "model" : null,
241
- !baseUrl ? "base_url" : null,
360
+ !modelProvider ? "model_provider" : null,
242
361
  ].filter((value) => Boolean(value)),
243
362
  });
244
363
  }
245
364
  return {
246
365
  model,
247
- baseUrl,
366
+ modelProvider,
248
367
  };
249
368
  }
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
+ }
250
380
  /**
251
381
  * Computes keep/delete/switch outcomes when a provider leaves or changes profiles.
252
382
  */
@@ -297,9 +427,12 @@ function planProfileLifecycleOutcome(args) {
297
427
  function planConfigMutation(document, args) {
298
428
  const operations = [];
299
429
  const createdProfileSections = [];
430
+ const createdModelProviderSections = [];
300
431
  const deletedProfileSections = [];
301
432
  const updatedProfiles = [];
433
+ const updatedModelProviders = [];
302
434
  const sectionMap = new Map(document.profiles.map((profile) => [profile.name, profile]));
435
+ const modelProviderSectionMap = new Map(document.modelProviders.map((entry) => [entry.name, entry]));
303
436
  if (args.setActiveProfile && args.setActiveProfile !== document.activeProfile) {
304
437
  const quoted = `"${args.setActiveProfile}"`;
305
438
  if (document.activeProfileRange) {
@@ -344,7 +477,7 @@ function planConfigMutation(document, args) {
344
477
  index: document.rawText.length,
345
478
  text: `${prefix}[profiles.${profileName}]${document.lineEnding}` +
346
479
  `model = ${JSON.stringify(requiredFields.model)}${document.lineEnding}` +
347
- `base_url = ${JSON.stringify(requiredFields.baseUrl)}${document.lineEnding}`,
480
+ `model_provider = ${JSON.stringify(requiredFields.modelProvider)}${document.lineEnding}`,
348
481
  });
349
482
  createdProfileSections.push(profileName);
350
483
  continue;
@@ -354,11 +487,46 @@ function planConfigMutation(document, args) {
354
487
  updatedProfiles.push(profileName);
355
488
  }
356
489
  }
490
+ for (const [profileName, fields] of Object.entries(args.upsertModelProviders ?? {}).sort(([left], [right]) => left.localeCompare(right))) {
491
+ const section = modelProviderSectionMap.get(profileName);
492
+ if (!section) {
493
+ 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.`, {
497
+ profile: profileName,
498
+ modelProvider: profileName,
499
+ missingFields: [
500
+ !baseUrl ? "base_url" : null,
501
+ !envKey ? "env_key" : null,
502
+ ].filter((value) => Boolean(value)),
503
+ });
504
+ }
505
+ const prefix = document.rawText.length > 0 && !document.rawText.endsWith(document.lineEnding)
506
+ ? document.lineEnding
507
+ : "";
508
+ operations.push({
509
+ kind: "insert-at",
510
+ index: document.rawText.length,
511
+ text: `${prefix}[model_providers.${profileName}]${document.lineEnding}` +
512
+ `base_url = ${JSON.stringify(baseUrl)}${document.lineEnding}` +
513
+ `env_key = ${JSON.stringify(envKey)}${document.lineEnding}`,
514
+ });
515
+ createdModelProviderSections.push(profileName);
516
+ continue;
517
+ }
518
+ const sectionUpdated = planModelProviderFieldMutation(section, fields, operations);
519
+ if (sectionUpdated) {
520
+ updatedModelProviders.push(profileName);
521
+ }
522
+ }
357
523
  return {
358
524
  operations,
359
525
  createdProfileSections,
526
+ createdModelProviderSections,
360
527
  deletedProfileSections,
361
528
  updatedProfiles,
529
+ updatedModelProviders,
362
530
  switchedActiveProfile: Boolean(args.setActiveProfile && args.setActiveProfile !== document.activeProfile),
363
531
  };
364
532
  }
@@ -384,7 +552,7 @@ function applyPatchOperations(rawText, operations) {
384
552
  function planSectionFieldMutation(document, section, fields, operations) {
385
553
  let updated = false;
386
554
  const modelText = fields.model !== undefined ? JSON.stringify(fields.model) : null;
387
- const baseUrlText = fields.baseUrl !== undefined ? JSON.stringify(fields.baseUrl) : null;
555
+ const modelProviderText = fields.modelProvider !== undefined ? JSON.stringify(fields.modelProvider) : null;
388
556
  const inserts = [];
389
557
  if (modelText !== null && section.modelValueRange) {
390
558
  if (section.model !== fields.model) {
@@ -401,6 +569,38 @@ function planSectionFieldMutation(document, section, fields, operations) {
401
569
  inserts.push(`model = ${modelText}${document.lineEnding}`);
402
570
  updated = true;
403
571
  }
572
+ if (modelProviderText !== null && section.modelProviderValueRange) {
573
+ if (section.modelProvider !== fields.modelProvider) {
574
+ operations.push({
575
+ kind: "replace-range",
576
+ start: section.modelProviderValueRange.start,
577
+ end: section.modelProviderValueRange.end,
578
+ text: modelProviderText,
579
+ });
580
+ updated = true;
581
+ }
582
+ }
583
+ else if (modelProviderText !== null && !section.modelProviderValueRange) {
584
+ inserts.push(`model_provider = ${modelProviderText}${document.lineEnding}`);
585
+ updated = true;
586
+ }
587
+ if (inserts.length > 0) {
588
+ operations.push({
589
+ kind: "insert-at",
590
+ index: section.managedFieldInsertIndex,
591
+ text: inserts.join(""),
592
+ });
593
+ }
594
+ return updated;
595
+ }
596
+ /**
597
+ * Plans base_url/env_key updates for one model_providers section.
598
+ */
599
+ function planModelProviderFieldMutation(section, fields, operations) {
600
+ let updated = false;
601
+ const baseUrlText = fields.baseUrl !== undefined ? JSON.stringify(fields.baseUrl) : null;
602
+ const envKeyText = fields.envKey !== undefined ? JSON.stringify(fields.envKey) : null;
603
+ const inserts = [];
404
604
  if (baseUrlText !== null && section.baseUrlValueRange) {
405
605
  if (section.baseUrl !== fields.baseUrl) {
406
606
  operations.push({
@@ -412,16 +612,30 @@ function planSectionFieldMutation(document, section, fields, operations) {
412
612
  updated = true;
413
613
  }
414
614
  }
415
- else if (baseUrlText !== null && !section.baseUrlValueRange) {
416
- inserts.push(`base_url = ${baseUrlText}${document.lineEnding}`);
417
- updated = true;
615
+ else if (baseUrlText !== null) {
616
+ inserts.push(`base_url = ${baseUrlText}`);
617
+ }
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}`);
418
631
  }
419
632
  if (inserts.length > 0) {
420
633
  operations.push({
421
634
  kind: "insert-at",
422
- index: section.managedFieldInsertIndex,
423
- text: inserts.join(""),
635
+ index: section.sectionEnd,
636
+ text: `${inserts.join("\n")}\n`,
424
637
  });
638
+ updated = true;
425
639
  }
426
640
  return updated;
427
641
  }
@@ -483,13 +697,13 @@ function findManagedFieldInsertIndex(rawText, sectionStart, sectionEnd) {
483
697
  }
484
698
  return sectionStart + lines[lastMeaningfulIndex].end;
485
699
  }
486
- function collectManagedFields(model, baseUrl) {
700
+ function collectManagedFields(model, modelProvider) {
487
701
  const fields = [];
488
702
  if (model !== null) {
489
703
  fields.push("model");
490
704
  }
491
- if (baseUrl !== null) {
492
- fields.push("base_url");
705
+ if (modelProvider !== null) {
706
+ fields.push("model_provider");
493
707
  }
494
708
  return fields;
495
709
  }
@@ -4,6 +4,7 @@ exports.validateProvidersShape = validateProvidersShape;
4
4
  exports.cleanProviderRecord = cleanProviderRecord;
5
5
  exports.sortProviders = sortProviders;
6
6
  exports.findProviderByProfile = findProviderByProfile;
7
+ exports.findProvidersByProfile = findProvidersByProfile;
7
8
  exports.maskSecret = maskSecret;
8
9
  /**
9
10
  * Validates and normalizes unknown JSON into the providers.json domain model.
@@ -28,6 +29,9 @@ function validateProvidersShape(input) {
28
29
  if (typeof provider.apiKey !== "string" || provider.apiKey.trim() === "") {
29
30
  throw new Error(`Provider "${name}" is missing a valid apiKey.`);
30
31
  }
32
+ if (typeof provider.envKey !== "string" || provider.envKey.trim() === "") {
33
+ throw new Error(`Provider "${name}" is missing a valid envKey.`);
34
+ }
31
35
  if (provider.baseUrl !== undefined && typeof provider.baseUrl !== "string") {
32
36
  throw new Error(`Provider "${name}" has an invalid baseUrl.`);
33
37
  }
@@ -42,6 +46,7 @@ function validateProvidersShape(input) {
42
46
  providers[name] = cleanProviderRecord({
43
47
  profile: provider.profile,
44
48
  apiKey: provider.apiKey,
49
+ envKey: provider.envKey,
45
50
  baseUrl: provider.baseUrl,
46
51
  note: provider.note,
47
52
  tags: provider.tags,
@@ -56,6 +61,7 @@ function cleanProviderRecord(record) {
56
61
  const next = {
57
62
  profile: record.profile.trim(),
58
63
  apiKey: record.apiKey.trim(),
64
+ envKey: record.envKey.trim(),
59
65
  };
60
66
  if (record.baseUrl && record.baseUrl.trim() !== "") {
61
67
  next.baseUrl = record.baseUrl.trim();
@@ -84,12 +90,20 @@ function sortProviders(providers) {
84
90
  * Finds the provider name associated with a given Codex profile.
85
91
  */
86
92
  function findProviderByProfile(providers, profile) {
93
+ const matches = findProvidersByProfile(providers, profile);
94
+ return matches.length > 0 ? matches[0] : null;
95
+ }
96
+ /**
97
+ * Returns all provider names associated with a given Codex profile.
98
+ */
99
+ function findProvidersByProfile(providers, profile) {
100
+ const matches = [];
87
101
  for (const [name, provider] of Object.entries(providers.providers)) {
88
102
  if (provider.profile === profile) {
89
- return name;
103
+ matches.push(name);
90
104
  }
91
105
  }
92
- return null;
106
+ return matches.sort();
93
107
  }
94
108
  /**
95
109
  * Masks a secret for human-readable output while preserving a short fingerprint.
@@ -15,6 +15,7 @@ function buildSetupDrafts(profiles, detailsByProfile) {
15
15
  record: (0, providers_1.cleanProviderRecord)({
16
16
  profile,
17
17
  apiKey: detail.apiKey ?? "",
18
+ envKey: detail.envKey ?? "",
18
19
  baseUrl: detail.baseUrl,
19
20
  note: detail.note,
20
21
  tags: detail.tags,