@minniexcode/codex-switch 0.0.6 → 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 (43) hide show
  1. package/README.AI.md +5 -2
  2. package/README.md +12 -6
  3. package/dist/app/add-provider.js +21 -3
  4. package/dist/app/edit-provider.js +39 -11
  5. package/dist/app/get-status.js +8 -1
  6. package/dist/app/init-codex.js +68 -0
  7. package/dist/app/list-providers.js +1 -0
  8. package/dist/app/run-doctor.js +60 -0
  9. package/dist/app/setup-codex.js +17 -8
  10. package/dist/app/show-config.js +9 -1
  11. package/dist/app/switch-provider.js +14 -7
  12. package/dist/cli/add-interactive.js +4 -2
  13. package/dist/cli/args.js +3 -0
  14. package/dist/cli/help.js +3 -0
  15. package/dist/cli/interactive.js +3 -0
  16. package/dist/cli/output.js +20 -5
  17. package/dist/cli/prompt.js +3 -0
  18. package/dist/cli.js +1 -1
  19. package/dist/commands/handlers.js +80 -11
  20. package/dist/commands/help.js +2 -1
  21. package/dist/commands/registry.js +73 -13
  22. package/dist/domain/config.js +137 -0
  23. package/dist/domain/providers.js +16 -2
  24. package/dist/domain/setup.js +1 -0
  25. package/dist/infra/backup-repo.js +3 -0
  26. package/dist/infra/codex-cli.js +3 -0
  27. package/dist/infra/codex-paths.js +3 -0
  28. package/dist/infra/fs-utils.js +3 -0
  29. package/dist/infra/lock-repo.js +3 -0
  30. package/dist/infra/providers-repo.js +3 -0
  31. package/dist/interaction/add-interactive.js +9 -18
  32. package/dist/interaction/interactive.js +84 -11
  33. package/dist/runtime/codex-probe.js +7 -0
  34. package/dist/storage/auth-repo.js +160 -0
  35. package/dist/storage/config-repo.js +58 -0
  36. package/docs/Design/codex-switch-v0.0.7-design.md +862 -0
  37. package/docs/PRD/codex-switch-prd-v0.0.5-to-v0.1.0.md +131 -25
  38. package/docs/Reference/codex-config-reference.md +604 -0
  39. package/docs/Reference/codex-config-reference.zh-CN.md +633 -0
  40. package/docs/cli-usage.md +77 -29
  41. package/docs/test-report-0.0.7.md +118 -0
  42. package/docs/testing.md +67 -47
  43. 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;
@@ -119,6 +120,8 @@ function parseStructuredConfig(configContent) {
119
120
  sectionEnd: configContent.length,
120
121
  baseUrlValueRange: null,
121
122
  baseUrl: null,
123
+ envKeyValueRange: null,
124
+ envKey: null,
122
125
  };
123
126
  modelProviders.push(currentModelProvider);
124
127
  inRoot = false;
@@ -173,6 +176,14 @@ function parseStructuredConfig(configContent) {
173
176
  end: line.start + baseUrlMatch.valueEnd,
174
177
  };
175
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
+ }
176
187
  }
177
188
  }
178
189
  return {
@@ -207,6 +218,7 @@ function buildManagedProfileViews(document, providers) {
207
218
  model: section.model,
208
219
  modelProvider: section.modelProvider,
209
220
  baseUrl: modelProviderSection?.baseUrl ?? null,
221
+ envKey: modelProviderSection?.envKey ?? null,
210
222
  managedFields: collectManagedFields(section.model, section.modelProvider),
211
223
  source: linkInfo.managed ? "managed" : "unmanaged",
212
224
  });
@@ -223,6 +235,7 @@ function buildManagedProfileViews(document, providers) {
223
235
  model: null,
224
236
  modelProvider: null,
225
237
  baseUrl: null,
238
+ envKey: null,
226
239
  managedFields: [],
227
240
  source: "orphaned-reference",
228
241
  });
@@ -285,6 +298,28 @@ function collectConfigConsistencyIssues(document, providers) {
285
298
  modelProvider: view.modelProvider,
286
299
  });
287
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
+ }
288
323
  }
289
324
  }
290
325
  }
@@ -296,6 +331,13 @@ function collectConfigConsistencyIssues(document, providers) {
296
331
  profile: document.activeProfile,
297
332
  });
298
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
+ }
299
341
  }
300
342
  return issues.sort((left, right) => {
301
343
  if (left.profile === right.profile) {
@@ -324,6 +366,17 @@ function validateManagedProfileCreation(profile, fields) {
324
366
  modelProvider,
325
367
  };
326
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
+ }
327
380
  /**
328
381
  * Computes keep/delete/switch outcomes when a provider leaves or changes profiles.
329
382
  */
@@ -374,9 +427,12 @@ function planProfileLifecycleOutcome(args) {
374
427
  function planConfigMutation(document, args) {
375
428
  const operations = [];
376
429
  const createdProfileSections = [];
430
+ const createdModelProviderSections = [];
377
431
  const deletedProfileSections = [];
378
432
  const updatedProfiles = [];
433
+ const updatedModelProviders = [];
379
434
  const sectionMap = new Map(document.profiles.map((profile) => [profile.name, profile]));
435
+ const modelProviderSectionMap = new Map(document.modelProviders.map((entry) => [entry.name, entry]));
380
436
  if (args.setActiveProfile && args.setActiveProfile !== document.activeProfile) {
381
437
  const quoted = `"${args.setActiveProfile}"`;
382
438
  if (document.activeProfileRange) {
@@ -431,11 +487,46 @@ function planConfigMutation(document, args) {
431
487
  updatedProfiles.push(profileName);
432
488
  }
433
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
+ }
434
523
  return {
435
524
  operations,
436
525
  createdProfileSections,
526
+ createdModelProviderSections,
437
527
  deletedProfileSections,
438
528
  updatedProfiles,
529
+ updatedModelProviders,
439
530
  switchedActiveProfile: Boolean(args.setActiveProfile && args.setActiveProfile !== document.activeProfile),
440
531
  };
441
532
  }
@@ -502,6 +593,52 @@ function planSectionFieldMutation(document, section, fields, operations) {
502
593
  }
503
594
  return updated;
504
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 = [];
604
+ if (baseUrlText !== null && section.baseUrlValueRange) {
605
+ if (section.baseUrl !== fields.baseUrl) {
606
+ operations.push({
607
+ kind: "replace-range",
608
+ start: section.baseUrlValueRange.start,
609
+ end: section.baseUrlValueRange.end,
610
+ text: baseUrlText,
611
+ });
612
+ updated = true;
613
+ }
614
+ }
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}`);
631
+ }
632
+ if (inserts.length > 0) {
633
+ operations.push({
634
+ kind: "insert-at",
635
+ index: section.sectionEnd,
636
+ text: `${inserts.join("\n")}\n`,
637
+ });
638
+ updated = true;
639
+ }
640
+ return updated;
641
+ }
505
642
  function splitWithOffsets(value) {
506
643
  if (value.length === 0) {
507
644
  return [];
@@ -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,
@@ -1,6 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.saveLatestManifest = exports.restoreManifest = exports.loadManifestById = exports.loadLatestManifest = exports.listBackups = exports.createBackup = void 0;
4
+ /**
5
+ * Compatibility facade that re-exports backup repository helpers from storage.
6
+ */
4
7
  var backup_repo_1 = require("../storage/backup-repo");
5
8
  Object.defineProperty(exports, "createBackup", { enumerable: true, get: function () { return backup_repo_1.createBackup; } });
6
9
  Object.defineProperty(exports, "listBackups", { enumerable: true, get: function () { return backup_repo_1.listBackups; } });
@@ -1,6 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.setCodexSpawnImplementation = exports.runCodexLogin = exports.resetCodexSpawnImplementation = exports.readCodexVersion = exports.checkCodexVersion = exports.checkCodexAvailable = void 0;
4
+ /**
5
+ * Compatibility facade that re-exports codex CLI runtime helpers.
6
+ */
4
7
  var codex_cli_1 = require("../runtime/codex-cli");
5
8
  Object.defineProperty(exports, "checkCodexAvailable", { enumerable: true, get: function () { return codex_cli_1.checkCodexAvailable; } });
6
9
  Object.defineProperty(exports, "checkCodexVersion", { enumerable: true, get: function () { return codex_cli_1.checkCodexVersion; } });
@@ -1,6 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.resolveCodexDir = exports.createCodexPaths = exports.CODEX_DIR_ENV_NAME = void 0;
4
+ /**
5
+ * Compatibility facade that re-exports Codex path utilities from storage.
6
+ */
4
7
  var codex_paths_1 = require("../storage/codex-paths");
5
8
  Object.defineProperty(exports, "CODEX_DIR_ENV_NAME", { enumerable: true, get: function () { return codex_paths_1.CODEX_DIR_ENV_NAME; } });
6
9
  Object.defineProperty(exports, "createCodexPaths", { enumerable: true, get: function () { return codex_paths_1.createCodexPaths; } });
@@ -1,6 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.writeTextFileAtomic = exports.readRequiredFile = exports.printErrorDetails = exports.formatDetail = exports.ensureDir = void 0;
4
+ /**
5
+ * Compatibility facade that re-exports shared filesystem helpers from storage.
6
+ */
4
7
  var fs_utils_1 = require("../storage/fs-utils");
5
8
  Object.defineProperty(exports, "ensureDir", { enumerable: true, get: function () { return fs_utils_1.ensureDir; } });
6
9
  Object.defineProperty(exports, "formatDetail", { enumerable: true, get: function () { return fs_utils_1.formatDetail; } });
@@ -1,5 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.withCodexLock = void 0;
4
+ /**
5
+ * Compatibility facade that re-exports Codex lock helpers from storage.
6
+ */
4
7
  var lock_repo_1 = require("../storage/lock-repo");
5
8
  Object.defineProperty(exports, "withCodexLock", { enumerable: true, get: function () { return lock_repo_1.withCodexLock; } });
@@ -1,6 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.writeProvidersFile = exports.readProvidersFileIfExists = exports.readProvidersFile = exports.readProviderRecord = exports.mergeProviders = void 0;
4
+ /**
5
+ * Compatibility facade that re-exports provider repository helpers from storage.
6
+ */
4
7
  var providers_repo_1 = require("../storage/providers-repo");
5
8
  Object.defineProperty(exports, "mergeProviders", { enumerable: true, get: function () { return providers_repo_1.mergeProviders; } });
6
9
  Object.defineProperty(exports, "readProviderRecord", { enumerable: true, get: function () { return providers_repo_1.readProviderRecord; } });
@@ -4,13 +4,12 @@ exports.COMMON_TAG_CHOICES = void 0;
4
4
  exports.collectAddInput = collectAddInput;
5
5
  exports.createNonInteractiveAddError = createNonInteractiveAddError;
6
6
  exports.promptTags = promptTags;
7
- exports.parseTags = parseTags;
8
7
  const errors_1 = require("../domain/errors");
9
8
  exports.COMMON_TAG_CHOICES = ["free", "paid", "daily", "backup"];
10
9
  /**
11
10
  * Collects add command inputs interactively when required values are missing.
12
11
  */
13
- async function collectAddInput(runtime, defaults, providerExists) {
12
+ async function collectAddInput(runtime, defaults, providerExists, profileExists) {
14
13
  runtime.writeLine("Interactive add mode");
15
14
  runtime.writeLine("Provide the missing required fields. Press Enter to skip optional fields.");
16
15
  const providerName = defaults.providerName
@@ -20,13 +19,19 @@ async function collectAddInput(runtime, defaults, providerExists) {
20
19
  const apiKey = defaults.apiKey
21
20
  ? normalizeRequiredValue(defaults.apiKey)
22
21
  : await promptConfirmedSecret(runtime, "API key", "Confirm API key");
23
- const baseUrl = defaults.baseUrl ?? normalizeOptionalValue(await runtime.inputText("Base URL (optional)"));
22
+ const createProfile = !profileExists(profile);
23
+ const model = createProfile ? await promptRequiredValue(runtime, `Model for new profile "${profile}"`) : null;
24
+ const baseUrl = createProfile
25
+ ? (defaults.baseUrl ? normalizeRequiredValue(defaults.baseUrl) : await promptRequiredValue(runtime, `Base URL for new profile "${profile}"`))
26
+ : defaults.baseUrl ?? normalizeOptionalValue(await runtime.inputText("Base URL (optional)"));
24
27
  const note = defaults.note ?? normalizeOptionalValue(await runtime.inputText("Note (optional)"));
25
28
  const tags = defaults.tags.length > 0 ? defaults.tags : await promptTags(runtime);
26
29
  return {
27
30
  providerName,
28
31
  profile,
29
32
  apiKey,
33
+ createProfile,
34
+ model,
30
35
  baseUrl,
31
36
  note,
32
37
  tags,
@@ -87,22 +92,8 @@ function normalizeOptionalValue(value) {
87
92
  }
88
93
  async function promptTags(runtime, defaults = []) {
89
94
  const defaultPresetTags = defaults.filter(isCommonTag);
90
- const defaultCustomTags = defaults.filter((tag) => !isCommonTag(tag));
91
- const presetTags = await runtime.selectMany("Select tags (optional)", exports.COMMON_TAG_CHOICES.map((tag) => ({ value: tag, label: tag })), { defaultValues: defaultPresetTags });
92
- const customTags = parseTags(await runtime.inputText("Custom tags (optional, comma-separated)", {
93
- defaultValue: defaultCustomTags.join(", "),
94
- }));
95
- return dedupeTags([...presetTags, ...customTags]);
96
- }
97
- function parseTags(value) {
98
- return dedupeTags(value
99
- .split(",")
100
- .map((tag) => tag.trim())
101
- .filter((tag) => tag.length > 0));
95
+ return runtime.selectMany("Select tags (optional)", exports.COMMON_TAG_CHOICES.map((tag) => ({ value: tag, label: tag })), { defaultValues: defaultPresetTags });
102
96
  }
103
97
  function isCommonTag(tag) {
104
98
  return exports.COMMON_TAG_CHOICES.includes(tag);
105
99
  }
106
- function dedupeTags(tags) {
107
- return Array.from(new Set(tags));
108
- }
@@ -44,6 +44,7 @@ exports.getRollbackSummaryById = getRollbackSummaryById;
44
44
  exports.confirmRollback = confirmRollback;
45
45
  exports.chooseSetupStrategy = chooseSetupStrategy;
46
46
  exports.chooseCodexDir = chooseCodexDir;
47
+ exports.confirmCreateCodexDir = confirmCreateCodexDir;
47
48
  exports.chooseSetupProfiles = chooseSetupProfiles;
48
49
  exports.collectSetupProviderDetails = collectSetupProviderDetails;
49
50
  exports.collectEditInput = collectEditInput;
@@ -61,6 +62,9 @@ const add_interactive_1 = require("./add-interactive");
61
62
  function canPrompt(runtime, jsonMode) {
62
63
  return !jsonMode && runtime.isInteractive();
63
64
  }
65
+ /**
66
+ * Prompts the user to choose one configured provider when a command omitted its target.
67
+ */
64
68
  async function promptForProviderSelection(runtime, providersPath, message) {
65
69
  const providers = (0, providers_repo_1.readProvidersFile)(providersPath);
66
70
  const choices = Object.entries(providers.providers)
@@ -75,6 +79,9 @@ async function promptForProviderSelection(runtime, providersPath, message) {
75
79
  }
76
80
  return runtime.selectOne(message, choices);
77
81
  }
82
+ /**
83
+ * Confirms destructive provider removal and turns a declined prompt into a typed cancellation.
84
+ */
78
85
  async function confirmProviderRemoval(runtime, providerName) {
79
86
  const confirmed = await runtime.confirmAction(`Remove provider "${providerName}"?`, {
80
87
  defaultValue: false,
@@ -83,6 +90,9 @@ async function confirmProviderRemoval(runtime, providerName) {
83
90
  throw (0, errors_1.cliError)("PROMPT_CANCELLED", `Removal cancelled for provider "${providerName}".`);
84
91
  }
85
92
  }
93
+ /**
94
+ * Confirms provider import semantics, including whether the file will merge or replace the registry.
95
+ */
86
96
  async function confirmImport(runtime, sourceFile, merge = false) {
87
97
  const confirmed = await runtime.confirmAction(merge
88
98
  ? `Import providers from ${path.resolve(sourceFile)} and merge into the current registry?`
@@ -91,22 +101,37 @@ async function confirmImport(runtime, sourceFile, merge = false) {
91
101
  throw (0, errors_1.cliError)("PROMPT_CANCELLED", "Import cancelled.");
92
102
  }
93
103
  }
104
+ /**
105
+ * Confirms whether an existing export target may be overwritten.
106
+ */
94
107
  async function confirmExportOverwrite(runtime, targetFile) {
95
108
  return runtime.confirmAction(`Overwrite existing export target ${path.resolve(targetFile)}?`, {
96
109
  defaultValue: false,
97
110
  });
98
111
  }
112
+ /**
113
+ * Resolves whether the export target already exists after normalizing to an absolute path.
114
+ */
99
115
  function exportTargetExists(targetFile) {
100
116
  return fs.existsSync(path.resolve(targetFile));
101
117
  }
118
+ /**
119
+ * Builds a rollback preview for the latest managed backup.
120
+ */
102
121
  function getRollbackSummary(latestBackupPath) {
103
122
  const manifest = (0, backup_repo_1.loadLatestManifest)(latestBackupPath);
104
123
  return buildRollbackSummary(manifest);
105
124
  }
125
+ /**
126
+ * Builds a rollback preview for one explicit backup id.
127
+ */
106
128
  function getRollbackSummaryById(backupsDir, backupId) {
107
129
  const manifest = (0, backup_repo_1.loadManifestById)(backupsDir, backupId);
108
130
  return buildRollbackSummary(manifest);
109
131
  }
132
+ /**
133
+ * Converts a backup manifest into the human preview shown before rollback confirmation.
134
+ */
110
135
  function buildRollbackSummary(manifest) {
111
136
  const previewLines = [
112
137
  "Rollback preview",
@@ -119,6 +144,9 @@ function buildRollbackSummary(manifest) {
119
144
  ];
120
145
  return { manifest, previewLines };
121
146
  }
147
+ /**
148
+ * Prints the rollback preview and requires explicit confirmation before restore proceeds.
149
+ */
122
150
  async function confirmRollback(runtime, latestBackupPath, backupsDir, backupId) {
123
151
  const { previewLines } = backupId && backupsDir
124
152
  ? getRollbackSummaryById(backupsDir, backupId)
@@ -133,13 +161,19 @@ async function confirmRollback(runtime, latestBackupPath, backupsDir, backupId)
133
161
  throw (0, errors_1.cliError)("PROMPT_CANCELLED", "Rollback cancelled.");
134
162
  }
135
163
  }
164
+ /**
165
+ * Prompts for setup merge strategy when providers.json already exists.
166
+ */
136
167
  async function chooseSetupStrategy(runtime) {
137
- return runtime.selectOne("providers.json already exists. Choose a setup strategy.", [
168
+ return runtime.selectOne("providers.json already exists. Choose a migrate strategy.", [
138
169
  { value: "merge", label: "merge", hint: "keep existing providers and override by imported names" },
139
170
  { value: "overwrite", label: "overwrite", hint: "replace the existing registry" },
140
- { value: "cancel", label: "cancel", hint: "abort setup without writing" },
171
+ { value: "cancel", label: "cancel", hint: "abort migrate without writing" },
141
172
  ]);
142
173
  }
174
+ /**
175
+ * Resolves the Codex directory from discovered candidates or a manually entered path.
176
+ */
143
177
  async function chooseCodexDir(runtime, candidates) {
144
178
  if (candidates.length === 0) {
145
179
  const manual = (await runtime.inputText("Codex directory path")).trim();
@@ -170,6 +204,17 @@ async function chooseCodexDir(runtime, candidates) {
170
204
  }
171
205
  return (0, codex_paths_1.resolveCodexDir)(manual);
172
206
  }
207
+ /**
208
+ * Confirms whether a missing Codex directory should be created during init.
209
+ */
210
+ async function confirmCreateCodexDir(runtime, codexDir) {
211
+ return runtime.confirmAction(`Create missing Codex directory ${codexDir}?`, {
212
+ defaultValue: false,
213
+ });
214
+ }
215
+ /**
216
+ * Lets setup adopt a subset of unmanaged config profiles into providers.json.
217
+ */
173
218
  async function chooseSetupProfiles(runtime, profiles) {
174
219
  if (profiles.length === 0) {
175
220
  return [];
@@ -177,29 +222,57 @@ async function chooseSetupProfiles(runtime, profiles) {
177
222
  return runtime.selectMany("Choose unmanaged config profiles to adopt into providers.json.", profiles.map((profile) => ({
178
223
  value: profile.name,
179
224
  label: profile.name,
180
- hint: `${profile.model} | ${profile.baseUrl}`,
225
+ hint: `${profile.model} | ${profile.baseUrl} | ${profile.envKey}`,
181
226
  })));
182
227
  }
183
- async function collectSetupProviderDetails(runtime, profiles) {
228
+ /**
229
+ * Collects provider metadata for each adopted config profile during setup.
230
+ */
231
+ async function collectSetupProviderDetails(runtime, profiles, defaultsByProfile = {}) {
184
232
  const result = {};
185
233
  for (const profile of profiles) {
234
+ const defaults = defaultsByProfile[profile] ?? {};
186
235
  const providerName = (await runtime.inputText(`Provider name for profile "${profile}"`, {
187
- defaultValue: profile,
236
+ defaultValue: defaults.providerName ?? profile,
237
+ })).trim();
238
+ if (defaults.envKey) {
239
+ runtime.writeLine(`Runtime env key for "${profile}": ${defaults.envKey}`);
240
+ }
241
+ const apiKey = await promptRequiredSecret(runtime, `API key for profile "${profile}"`, defaults.apiKey?.trim() || undefined);
242
+ const baseUrl = (await runtime.inputText(`Base URL note for profile "${profile}" (optional)`, {
243
+ defaultValue: defaults.baseUrl ?? "",
244
+ })).trim();
245
+ const note = (await runtime.inputText(`Note for profile "${profile}" (optional)`, {
246
+ defaultValue: defaults.note ?? "",
188
247
  })).trim();
189
- const apiKey = (await runtime.inputSecret(`API key for profile "${profile}"`)).trim();
190
- const baseUrl = (await runtime.inputText(`Base URL for profile "${profile}" (optional)`)).trim();
191
- const note = (await runtime.inputText(`Note for profile "${profile}" (optional)`)).trim();
192
248
  const tags = await (0, add_interactive_1.promptTags)(runtime);
193
249
  result[profile] = {
194
- providerName: providerName || profile,
250
+ providerName: providerName || defaults.providerName || profile,
195
251
  apiKey,
196
- baseUrl: baseUrl || undefined,
197
- note: note || undefined,
252
+ envKey: defaults.envKey,
253
+ baseUrl: baseUrl || defaults.baseUrl || undefined,
254
+ note: note || defaults.note || undefined,
255
+ // Empty selections are omitted so downstream setup validation can distinguish unset from explicit data.
198
256
  tags: tags.length > 0 ? tags : undefined,
199
257
  };
200
258
  }
201
259
  return result;
202
260
  }
261
+ /**
262
+ * Re-prompts until a required secret value is provided, optionally falling back to a non-empty default.
263
+ */
264
+ async function promptRequiredSecret(runtime, label, defaultValue) {
265
+ while (true) {
266
+ const value = (await runtime.inputSecret(label)).trim() || defaultValue || "";
267
+ if (value.length > 0) {
268
+ return value;
269
+ }
270
+ runtime.writeLine(`${label} is required.`);
271
+ }
272
+ }
273
+ /**
274
+ * Collects editable provider fields, preserving current values when prompts are left blank.
275
+ */
203
276
  async function collectEditInput(runtime, current) {
204
277
  const profile = (await runtime.inputText("Profile", { defaultValue: current.profile })).trim();
205
278
  const apiKey = (await runtime.inputSecret("API key")).trim() || current.apiKey;
@@ -3,6 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.codexRuntimeProbe = void 0;
4
4
  exports.probeCodexRuntime = probeCodexRuntime;
5
5
  const codex_cli_1 = require("./codex-cli");
6
+ /**
7
+ * Default dependency probe implementation for the local codex CLI runtime.
8
+ */
6
9
  exports.codexRuntimeProbe = {
7
10
  probe(options) {
8
11
  if (options?.minVersion) {
@@ -11,6 +14,9 @@ exports.codexRuntimeProbe = {
11
14
  return probeCodexRuntime();
12
15
  },
13
16
  };
17
+ /**
18
+ * Checks whether the codex CLI is installed and, optionally, satisfies a minimum version.
19
+ */
14
20
  function probeCodexRuntime(minVersion) {
15
21
  const availability = (0, codex_cli_1.checkCodexAvailable)();
16
22
  if (!availability.ok) {
@@ -31,6 +37,7 @@ function probeCodexRuntime(minVersion) {
31
37
  };
32
38
  }
33
39
  if (minVersion) {
40
+ // Reuse the dedicated semver check so doctor and setup report the same unsupported-version behavior.
34
41
  const versionCheck = (0, codex_cli_1.checkCodexVersion)(minVersion);
35
42
  if (!versionCheck.ok) {
36
43
  return {