@minniexcode/codex-switch 0.0.6 → 0.0.8
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 +90 -5
- package/dist/app/edit-provider.js +39 -11
- package/dist/app/get-status.js +31 -1
- package/dist/app/init-codex.js +68 -0
- package/dist/app/list-providers.js +1 -0
- package/dist/app/run-doctor.js +96 -1
- package/dist/app/setup-codex.js +18 -9
- package/dist/app/show-config.js +9 -1
- package/dist/app/switch-provider.js +61 -8
- 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 +107 -13
- package/dist/commands/help.js +2 -1
- package/dist/commands/registry.js +87 -15
- package/dist/domain/config.js +137 -0
- package/dist/domain/providers.js +90 -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/runtime/copilot-adapter.js +173 -0
- package/dist/runtime/copilot-bridge-worker.js +25 -0
- package/dist/runtime/copilot-bridge.js +433 -0
- package/dist/runtime/copilot-installer.js +125 -0
- package/dist/runtime/copilot-sdk-loader.js +59 -0
- package/dist/storage/auth-repo.js +160 -0
- package/dist/storage/config-repo.js +58 -0
- package/dist/storage/fs-utils.js +3 -0
- package/dist/storage/runtime-state-repo.js +80 -0
- package/docs/Design/codex-switch-v0.0.7-design.md +862 -0
- package/docs/Design/codex-switch-v0.0.8-design.md +132 -0
- package/docs/Design/codex-switch-v0.0.9-to-v0.0.12-roadmap.md +413 -0
- package/docs/PRD/codex-switch-prd-v0.0.5-to-v0.1.0.md +131 -25
- package/docs/PRD/codex-switch-prd-v0.0.8.md +62 -0
- 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
|
@@ -11,6 +11,9 @@ exports.isKnownHelpTopic = isKnownHelpTopic;
|
|
|
11
11
|
exports.getPublicCommandNames = getPublicCommandNames;
|
|
12
12
|
exports.getNestedCommandTokens = getNestedCommandTokens;
|
|
13
13
|
const handlers_1 = require("./handlers");
|
|
14
|
+
/**
|
|
15
|
+
* Canonical command registry used by parsing, help rendering, and dispatch.
|
|
16
|
+
*/
|
|
14
17
|
exports.COMMANDS = [
|
|
15
18
|
{
|
|
16
19
|
id: "config-show",
|
|
@@ -39,19 +42,48 @@ exports.COMMANDS = [
|
|
|
39
42
|
examples: ["codexs config list-profiles", "codexs config list-profiles --json"],
|
|
40
43
|
},
|
|
41
44
|
{
|
|
42
|
-
id: "
|
|
43
|
-
tokens: ["
|
|
45
|
+
id: "init",
|
|
46
|
+
tokens: ["init"],
|
|
47
|
+
handler: handlers_1.handleRegisteredCommand,
|
|
48
|
+
group: "write",
|
|
49
|
+
summary: "Initialize a Codex directory with an empty managed providers registry.",
|
|
50
|
+
usage: ["codexs init [--json] [--codex-dir <path>]"],
|
|
51
|
+
details: [
|
|
52
|
+
"Creates providers.json with an empty providers object when it does not exist yet.",
|
|
53
|
+
"Does not require codex CLI, config.toml, or auth.json, and does not run doctor.",
|
|
54
|
+
"TTY mode can resolve ambiguous Codex directories and confirm creating a missing directory.",
|
|
55
|
+
"Non-TTY and --json runs never prompt and fail when the target directory is missing.",
|
|
56
|
+
],
|
|
57
|
+
examples: ["codexs init", "codexs init --json --codex-dir ~/.codex"],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "migrate",
|
|
61
|
+
tokens: ["migrate"],
|
|
44
62
|
handler: handlers_1.handleRegisteredCommand,
|
|
45
63
|
group: "write",
|
|
46
|
-
summary: "
|
|
47
|
-
usage: ["codexs
|
|
64
|
+
summary: "Adopt unmanaged Codex config profiles into providers.json.",
|
|
65
|
+
usage: ["codexs migrate [--json] [--codex-dir <path>] [--merge|--overwrite]"],
|
|
48
66
|
details: [
|
|
49
67
|
"Reads config.toml profiles, collects complete provider records, then writes providers.json under managed backup flow.",
|
|
50
68
|
"TTY mode can collect missing provider details and choose merge or overwrite when providers.json already exists.",
|
|
51
|
-
"
|
|
52
|
-
"Non-TTY and --json runs fail fast
|
|
69
|
+
"Migrate adopts only runtime profiles that already expose model, model_provider, matching base_url, and env_key.",
|
|
70
|
+
"Non-TTY and --json runs still fail fast because migrate profile selection and provider details remain interactive in this release.",
|
|
53
71
|
],
|
|
54
|
-
examples: ["codexs
|
|
72
|
+
examples: ["codexs migrate", "codexs migrate --overwrite --json --codex-dir ~/.codex"],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "setup",
|
|
76
|
+
tokens: ["setup"],
|
|
77
|
+
handler: handlers_1.handleRegisteredCommand,
|
|
78
|
+
group: "write",
|
|
79
|
+
summary: "Deprecated. Use init or migrate instead.",
|
|
80
|
+
usage: ["codexs setup"],
|
|
81
|
+
details: [
|
|
82
|
+
"setup no longer performs initialization or migration work.",
|
|
83
|
+
"Use init for AI-friendly idempotent providers.json initialization.",
|
|
84
|
+
"Use migrate for interactive adoption from existing config.toml profiles.",
|
|
85
|
+
],
|
|
86
|
+
examples: ["codexs help init", "codexs help migrate"],
|
|
55
87
|
},
|
|
56
88
|
{
|
|
57
89
|
id: "list",
|
|
@@ -96,6 +128,7 @@ exports.COMMANDS = [
|
|
|
96
128
|
usage: ["codexs status [--json] [--codex-dir <path>]"],
|
|
97
129
|
details: [
|
|
98
130
|
"Reports file presence, current profile, and whether the live profile is mapped.",
|
|
131
|
+
"When the active provider uses a local runtime bridge, status also reports bridge and SDK state.",
|
|
99
132
|
"Surfaces config consistency signals without mutating any files.",
|
|
100
133
|
"Use doctor for deeper diagnostics.",
|
|
101
134
|
],
|
|
@@ -114,7 +147,7 @@ exports.COMMANDS = [
|
|
|
114
147
|
details: [
|
|
115
148
|
"Passed flags replace only the selected fields and keep the rest unchanged.",
|
|
116
149
|
"TTY mode can first select a provider, then prompt for fields when no editable options were provided.",
|
|
117
|
-
"Interactive tags use preset multi-select
|
|
150
|
+
"Interactive tags use preset multi-select only.",
|
|
118
151
|
"When rebinding to a missing profile, --create-profile requires both --model and --base-url.",
|
|
119
152
|
"Backs up providers.json and config.toml before writing.",
|
|
120
153
|
],
|
|
@@ -128,6 +161,7 @@ exports.COMMANDS = [
|
|
|
128
161
|
summary: "Add a provider with explicit flags or progressive TTY prompts.",
|
|
129
162
|
usage: [
|
|
130
163
|
"codexs add <provider> --profile <name> --api-key <key> [--base-url <url>] [--note <text>] [--tag <tag> ...]",
|
|
164
|
+
"codexs add <provider> --copilot --profile <name> [--bridge-host <host>] [--bridge-port <port>] [--bridge-api-key <secret>] [--install-copilot-sdk]",
|
|
131
165
|
"codexs add <provider> --profile <name> --api-key <key> --create-profile --model <name> --base-url <url>",
|
|
132
166
|
"codexs add [--profile <name>] [--api-key <key>] [--base-url <url>] [--note <text>] [--tag <tag> ...]",
|
|
133
167
|
],
|
|
@@ -135,26 +169,32 @@ exports.COMMANDS = [
|
|
|
135
169
|
"Prompts only for missing required values when stdin/stdout are TTYs and --json is not set.",
|
|
136
170
|
"Interactive add collects provider name, profile, and apiKey progressively as plain text inputs.",
|
|
137
171
|
"Confirm API key when prompted interactively because the hidden prompt asks twice before writing.",
|
|
138
|
-
"Interactive tags use preset multi-select
|
|
172
|
+
"Interactive tags use preset multi-select only.",
|
|
139
173
|
"Automation and non-TTY environments must pass all required values explicitly.",
|
|
140
174
|
"Creating a missing profile section requires --create-profile together with --model and --base-url.",
|
|
175
|
+
"Use --copilot to create a GitHub Copilot bridge provider backed by the official SDK.",
|
|
176
|
+
],
|
|
177
|
+
examples: [
|
|
178
|
+
"codexs add packycode --profile packycode --api-key sk-xxx",
|
|
179
|
+
"codexs add copilot-main --copilot --profile copilot-main --install-copilot-sdk",
|
|
180
|
+
"codexs add",
|
|
141
181
|
],
|
|
142
|
-
examples: ["codexs add packycode --profile packycode --api-key sk-xxx", "codexs add packycode --profile packycode", "codexs add"],
|
|
143
182
|
},
|
|
144
183
|
{
|
|
145
184
|
id: "switch",
|
|
146
185
|
tokens: ["switch"],
|
|
147
186
|
handler: handlers_1.handleRegisteredCommand,
|
|
148
187
|
group: "write",
|
|
149
|
-
summary: "Switch to a provider and
|
|
150
|
-
usage: ["codexs switch <provider> [--
|
|
188
|
+
summary: "Switch to a provider and rewrite the managed auth mirror.",
|
|
189
|
+
usage: ["codexs switch <provider> [--json] [--codex-dir <path>]"],
|
|
151
190
|
details: [
|
|
152
191
|
"When <provider> is omitted in a TTY, an interactive provider selector is shown.",
|
|
153
192
|
"When <provider> is passed explicitly, switch proceeds directly without extra confirmation.",
|
|
154
|
-
"
|
|
193
|
+
"Switch updates the active config profile and rewrites auth.json from the provider envKey/apiKey pair.",
|
|
194
|
+
"Copilot bridge providers probe the optional official SDK before switching and fail fast if it is missing.",
|
|
155
195
|
"Backs up config.toml and auth.json, then rolls back on failure.",
|
|
156
196
|
],
|
|
157
|
-
examples: ["codexs switch freemodel", "codexs switch
|
|
197
|
+
examples: ["codexs switch freemodel", "codexs switch packycode --json"],
|
|
158
198
|
},
|
|
159
199
|
{
|
|
160
200
|
id: "remove",
|
|
@@ -220,7 +260,11 @@ exports.COMMANDS = [
|
|
|
220
260
|
group: "recovery",
|
|
221
261
|
summary: "Run configuration and environment diagnostics.",
|
|
222
262
|
usage: ["codexs doctor [--json] [--codex-dir <path>]"],
|
|
223
|
-
details: [
|
|
263
|
+
details: [
|
|
264
|
+
"Checks the expected config files, provider/profile consistency, and Codex CLI availability.",
|
|
265
|
+
"Copilot bridge providers add runtime dependency, auth, and bridge health diagnostics.",
|
|
266
|
+
"Returns structured issues so users and AI agents can act on them.",
|
|
267
|
+
],
|
|
224
268
|
examples: ["codexs doctor", "codexs doctor --json"],
|
|
225
269
|
},
|
|
226
270
|
{
|
|
@@ -243,22 +287,38 @@ const HELP_TOPIC_SET = new Set([
|
|
|
243
287
|
...exports.COMMANDS.map((command) => command.tokens.join(" ")),
|
|
244
288
|
...new Set(exports.COMMANDS.filter((command) => command.tokens.length > 1).map((command) => command.tokens[0])),
|
|
245
289
|
]);
|
|
290
|
+
/**
|
|
291
|
+
* Returns a defensive copy of the public command registry.
|
|
292
|
+
*/
|
|
246
293
|
function getCommandDefinitions() {
|
|
247
294
|
return exports.COMMANDS.slice();
|
|
248
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* Returns stable internal command ids in registry order.
|
|
298
|
+
*/
|
|
249
299
|
function getKnownCommandIds() {
|
|
250
300
|
return exports.COMMANDS.map((command) => command.id);
|
|
251
301
|
}
|
|
302
|
+
/**
|
|
303
|
+
* Resolves one command definition by its canonical internal id.
|
|
304
|
+
*/
|
|
252
305
|
function findCommandDefinition(commandId) {
|
|
253
306
|
if (commandId === "help" || commandId === "version") {
|
|
254
307
|
return null;
|
|
255
308
|
}
|
|
256
309
|
return exports.COMMANDS.find((command) => command.id === commandId) ?? null;
|
|
257
310
|
}
|
|
311
|
+
/**
|
|
312
|
+
* Resolves a command definition from its tokenized CLI spelling.
|
|
313
|
+
*/
|
|
258
314
|
function findCommandDefinitionByTokens(tokens) {
|
|
259
315
|
return exports.COMMANDS.find((command) => command.tokens.join(" ") === tokens.join(" ")) ?? null;
|
|
260
316
|
}
|
|
317
|
+
/**
|
|
318
|
+
* Matches argv against the longest registered token sequence first.
|
|
319
|
+
*/
|
|
261
320
|
function resolveCommandFromArgv(argv) {
|
|
321
|
+
// Nested commands such as "config show" must win over their shorter root tokens.
|
|
262
322
|
for (const command of exports.COMMANDS
|
|
263
323
|
.slice()
|
|
264
324
|
.sort((left, right) => right.tokens.length - left.tokens.length)) {
|
|
@@ -275,15 +335,27 @@ function resolveCommandFromArgv(argv) {
|
|
|
275
335
|
consumedTokens: 0,
|
|
276
336
|
};
|
|
277
337
|
}
|
|
338
|
+
/**
|
|
339
|
+
* Reports whether a name is reserved by either a command id or its public token form.
|
|
340
|
+
*/
|
|
278
341
|
function isKnownCommandName(commandName) {
|
|
279
342
|
return COMMAND_NAME_SET.has(commandName);
|
|
280
343
|
}
|
|
344
|
+
/**
|
|
345
|
+
* Reports whether a help topic is recognized by the help renderer.
|
|
346
|
+
*/
|
|
281
347
|
function isKnownHelpTopic(topic) {
|
|
282
348
|
return HELP_TOPIC_SET.has(topic);
|
|
283
349
|
}
|
|
350
|
+
/**
|
|
351
|
+
* Returns public command names exactly as they appear in help and examples.
|
|
352
|
+
*/
|
|
284
353
|
function getPublicCommandNames() {
|
|
285
354
|
return exports.COMMANDS.map((command) => command.tokens.join(" "));
|
|
286
355
|
}
|
|
356
|
+
/**
|
|
357
|
+
* Returns nested command spellings for one root token such as "config" or "backups".
|
|
358
|
+
*/
|
|
287
359
|
function getNestedCommandTokens(rootToken) {
|
|
288
360
|
return exports.COMMANDS
|
|
289
361
|
.filter((command) => command.tokens.length > 1 && command.tokens[0] === rootToken)
|
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,7 +4,11 @@ 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;
|
|
9
|
+
exports.isRuntimeBackedProvider = isRuntimeBackedProvider;
|
|
10
|
+
exports.isCopilotBridgeProvider = isCopilotBridgeProvider;
|
|
11
|
+
exports.buildCopilotBridgeBaseUrl = buildCopilotBridgeBaseUrl;
|
|
8
12
|
/**
|
|
9
13
|
* Validates and normalizes unknown JSON into the providers.json domain model.
|
|
10
14
|
*/
|
|
@@ -28,6 +32,9 @@ function validateProvidersShape(input) {
|
|
|
28
32
|
if (typeof provider.apiKey !== "string" || provider.apiKey.trim() === "") {
|
|
29
33
|
throw new Error(`Provider "${name}" is missing a valid apiKey.`);
|
|
30
34
|
}
|
|
35
|
+
if (typeof provider.envKey !== "string" || provider.envKey.trim() === "") {
|
|
36
|
+
throw new Error(`Provider "${name}" is missing a valid envKey.`);
|
|
37
|
+
}
|
|
31
38
|
if (provider.baseUrl !== undefined && typeof provider.baseUrl !== "string") {
|
|
32
39
|
throw new Error(`Provider "${name}" has an invalid baseUrl.`);
|
|
33
40
|
}
|
|
@@ -38,13 +45,22 @@ function validateProvidersShape(input) {
|
|
|
38
45
|
(!Array.isArray(provider.tags) || provider.tags.some((tag) => typeof tag !== "string"))) {
|
|
39
46
|
throw new Error(`Provider "${name}" has invalid tags.`);
|
|
40
47
|
}
|
|
48
|
+
if (provider.runtime !== undefined) {
|
|
49
|
+
validateProviderRuntime(name, provider.runtime);
|
|
50
|
+
const expectedBaseUrl = buildCopilotBridgeBaseUrl(provider.runtime);
|
|
51
|
+
if (typeof provider.baseUrl !== "string" || provider.baseUrl.trim() !== expectedBaseUrl) {
|
|
52
|
+
throw new Error(`Provider "${name}" baseUrl must match runtime bridge base URL "${expectedBaseUrl}".`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
41
55
|
// Normalize provider fields during validation so the persisted format stays clean.
|
|
42
56
|
providers[name] = cleanProviderRecord({
|
|
43
57
|
profile: provider.profile,
|
|
44
58
|
apiKey: provider.apiKey,
|
|
59
|
+
envKey: provider.envKey,
|
|
45
60
|
baseUrl: provider.baseUrl,
|
|
46
61
|
note: provider.note,
|
|
47
62
|
tags: provider.tags,
|
|
63
|
+
runtime: provider.runtime,
|
|
48
64
|
});
|
|
49
65
|
}
|
|
50
66
|
return { providers };
|
|
@@ -56,6 +72,7 @@ function cleanProviderRecord(record) {
|
|
|
56
72
|
const next = {
|
|
57
73
|
profile: record.profile.trim(),
|
|
58
74
|
apiKey: record.apiKey.trim(),
|
|
75
|
+
envKey: record.envKey.trim(),
|
|
59
76
|
};
|
|
60
77
|
if (record.baseUrl && record.baseUrl.trim() !== "") {
|
|
61
78
|
next.baseUrl = record.baseUrl.trim();
|
|
@@ -66,6 +83,18 @@ function cleanProviderRecord(record) {
|
|
|
66
83
|
if (record.tags && record.tags.length > 0) {
|
|
67
84
|
next.tags = record.tags.map((tag) => tag.trim()).filter((tag) => tag.length > 0);
|
|
68
85
|
}
|
|
86
|
+
if (record.runtime) {
|
|
87
|
+
next.runtime = {
|
|
88
|
+
kind: record.runtime.kind,
|
|
89
|
+
upstream: record.runtime.upstream,
|
|
90
|
+
bridgeHost: record.runtime.bridgeHost.trim(),
|
|
91
|
+
bridgePort: record.runtime.bridgePort,
|
|
92
|
+
bridgePath: record.runtime.bridgePath,
|
|
93
|
+
premiumRequests: record.runtime.premiumRequests,
|
|
94
|
+
authSource: record.runtime.authSource,
|
|
95
|
+
sdkInstallMode: record.runtime.sdkInstallMode,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
69
98
|
return next;
|
|
70
99
|
}
|
|
71
100
|
/**
|
|
@@ -84,12 +113,20 @@ function sortProviders(providers) {
|
|
|
84
113
|
* Finds the provider name associated with a given Codex profile.
|
|
85
114
|
*/
|
|
86
115
|
function findProviderByProfile(providers, profile) {
|
|
116
|
+
const matches = findProvidersByProfile(providers, profile);
|
|
117
|
+
return matches.length > 0 ? matches[0] : null;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Returns all provider names associated with a given Codex profile.
|
|
121
|
+
*/
|
|
122
|
+
function findProvidersByProfile(providers, profile) {
|
|
123
|
+
const matches = [];
|
|
87
124
|
for (const [name, provider] of Object.entries(providers.providers)) {
|
|
88
125
|
if (provider.profile === profile) {
|
|
89
|
-
|
|
126
|
+
matches.push(name);
|
|
90
127
|
}
|
|
91
128
|
}
|
|
92
|
-
return
|
|
129
|
+
return matches.sort();
|
|
93
130
|
}
|
|
94
131
|
/**
|
|
95
132
|
* Masks a secret for human-readable output while preserving a short fingerprint.
|
|
@@ -100,3 +137,54 @@ function maskSecret(value) {
|
|
|
100
137
|
}
|
|
101
138
|
return `${value.slice(0, 3)}***${value.slice(-2)}`;
|
|
102
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Returns whether one provider record relies on an auxiliary runtime component.
|
|
142
|
+
*/
|
|
143
|
+
function isRuntimeBackedProvider(provider) {
|
|
144
|
+
return Boolean(provider.runtime);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Returns whether one provider uses the GitHub Copilot SDK bridge runtime.
|
|
148
|
+
*/
|
|
149
|
+
function isCopilotBridgeProvider(provider) {
|
|
150
|
+
return provider.runtime?.kind === "copilot-sdk-bridge";
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Builds the canonical local bridge URL for one Copilot runtime provider.
|
|
154
|
+
*/
|
|
155
|
+
function buildCopilotBridgeBaseUrl(runtime) {
|
|
156
|
+
return `http://${runtime.bridgeHost}:${runtime.bridgePort}${runtime.bridgePath}`;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Validates one runtime-backed provider block.
|
|
160
|
+
*/
|
|
161
|
+
function validateProviderRuntime(name, runtime) {
|
|
162
|
+
if (!runtime || typeof runtime !== "object" || Array.isArray(runtime)) {
|
|
163
|
+
throw new Error(`Provider "${name}" has an invalid runtime block.`);
|
|
164
|
+
}
|
|
165
|
+
const record = runtime;
|
|
166
|
+
if (record.kind !== "copilot-sdk-bridge") {
|
|
167
|
+
throw new Error(`Provider "${name}" has an unsupported runtime kind.`);
|
|
168
|
+
}
|
|
169
|
+
if (record.upstream !== "github-copilot") {
|
|
170
|
+
throw new Error(`Provider "${name}" has an invalid runtime upstream.`);
|
|
171
|
+
}
|
|
172
|
+
if (typeof record.bridgeHost !== "string" || record.bridgeHost.trim() === "") {
|
|
173
|
+
throw new Error(`Provider "${name}" has an invalid runtime bridgeHost.`);
|
|
174
|
+
}
|
|
175
|
+
if (typeof record.bridgePort !== "number" || !Number.isInteger(record.bridgePort) || record.bridgePort <= 0) {
|
|
176
|
+
throw new Error(`Provider "${name}" has an invalid runtime bridgePort.`);
|
|
177
|
+
}
|
|
178
|
+
if (record.bridgePath !== "/v1") {
|
|
179
|
+
throw new Error(`Provider "${name}" has an invalid runtime bridgePath.`);
|
|
180
|
+
}
|
|
181
|
+
if (record.premiumRequests !== true) {
|
|
182
|
+
throw new Error(`Provider "${name}" must enable runtime premiumRequests.`);
|
|
183
|
+
}
|
|
184
|
+
if (record.authSource !== "official-sdk") {
|
|
185
|
+
throw new Error(`Provider "${name}" has an invalid runtime authSource.`);
|
|
186
|
+
}
|
|
187
|
+
if (record.sdkInstallMode !== "lazy") {
|
|
188
|
+
throw new Error(`Provider "${name}" has an invalid runtime sdkInstallMode.`);
|
|
189
|
+
}
|
|
190
|
+
}
|
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; } });
|