@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.
- package/README.AI.md +5 -2
- package/README.md +12 -6
- package/dist/app/add-provider.js +21 -3
- package/dist/app/edit-provider.js +39 -11
- package/dist/app/get-status.js +8 -1
- package/dist/app/init-codex.js +68 -0
- package/dist/app/list-providers.js +1 -0
- package/dist/app/run-doctor.js +60 -0
- package/dist/app/setup-codex.js +17 -8
- package/dist/app/show-config.js +9 -1
- package/dist/app/switch-provider.js +14 -7
- package/dist/cli/add-interactive.js +4 -2
- package/dist/cli/args.js +3 -0
- package/dist/cli/help.js +3 -0
- package/dist/cli/interactive.js +3 -0
- package/dist/cli/output.js +20 -5
- package/dist/cli/prompt.js +3 -0
- package/dist/cli.js +1 -1
- package/dist/commands/handlers.js +80 -11
- package/dist/commands/help.js +2 -1
- package/dist/commands/registry.js +73 -13
- package/dist/domain/config.js +137 -0
- package/dist/domain/providers.js +16 -2
- package/dist/domain/setup.js +1 -0
- package/dist/infra/backup-repo.js +3 -0
- package/dist/infra/codex-cli.js +3 -0
- package/dist/infra/codex-paths.js +3 -0
- package/dist/infra/fs-utils.js +3 -0
- package/dist/infra/lock-repo.js +3 -0
- package/dist/infra/providers-repo.js +3 -0
- package/dist/interaction/add-interactive.js +9 -18
- package/dist/interaction/interactive.js +84 -11
- package/dist/runtime/codex-probe.js +7 -0
- package/dist/storage/auth-repo.js +160 -0
- package/dist/storage/config-repo.js +58 -0
- package/docs/Design/codex-switch-v0.0.7-design.md +862 -0
- package/docs/PRD/codex-switch-prd-v0.0.5-to-v0.1.0.md +131 -25
- package/docs/Reference/codex-config-reference.md +604 -0
- package/docs/Reference/codex-config-reference.zh-CN.md +633 -0
- package/docs/cli-usage.md +77 -29
- package/docs/test-report-0.0.7.md +118 -0
- package/docs/testing.md +67 -47
- package/package.json +1 -1
package/dist/domain/config.js
CHANGED
|
@@ -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 [];
|
package/dist/domain/providers.js
CHANGED
|
@@ -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
|
-
|
|
103
|
+
matches.push(name);
|
|
90
104
|
}
|
|
91
105
|
}
|
|
92
|
-
return
|
|
106
|
+
return matches.sort();
|
|
93
107
|
}
|
|
94
108
|
/**
|
|
95
109
|
* Masks a secret for human-readable output while preserving a short fingerprint.
|
package/dist/domain/setup.js
CHANGED
|
@@ -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; } });
|
package/dist/infra/codex-cli.js
CHANGED
|
@@ -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; } });
|
package/dist/infra/fs-utils.js
CHANGED
|
@@ -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; } });
|
package/dist/infra/lock-repo.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
197
|
-
|
|
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 {
|