@minniexcode/codex-switch 0.0.11 → 0.1.0
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 +110 -152
- package/README.CN.md +179 -215
- package/README.md +183 -217
- package/dist/app/add-provider.js +9 -3
- package/dist/app/edit-provider.js +17 -3
- package/dist/app/get-status.js +8 -3
- package/dist/app/list-providers.js +48 -1
- package/dist/app/run-doctor.js +4 -0
- package/dist/cli/output.js +153 -18
- package/dist/commands/handlers.js +10 -6
- package/dist/commands/help.js +9 -5
- package/dist/commands/registry.js +22 -14
- package/dist/domain/config.js +26 -1
- package/dist/domain/providers.js +16 -0
- package/dist/domain/runtime-state.js +30 -8
- package/dist/infra/config-repo.js +16 -206
- package/dist/interaction/interactive.js +16 -6
- package/dist/runtime/copilot-bridge.js +2 -1
- package/dist/storage/config-repo.js +0 -23
- package/docs/Design/codex-switch-v0.0.12-design.md +343 -0
- package/docs/Design/codex-switch-v0.1.0-design.md +152 -0
- package/docs/PRD/codex-switch-prd-v0.0.12.md +279 -0
- package/docs/PRD/codex-switch-prd-v0.1.0.md +217 -317
- package/docs/Tests/testing.md +31 -151
- package/docs/cli-usage.md +223 -524
- package/docs/codex-switch-command-design.md +649 -646
- package/docs/codex-switch-product-overview.md +86 -241
- package/docs/codex-switch-technical-architecture.md +1115 -1112
- package/package.json +51 -51
- package/dist/app/rollback-latest.js +0 -26
package/dist/cli/output.js
CHANGED
|
@@ -85,12 +85,25 @@ function renderHumanSuccess(command, data, warnings) {
|
|
|
85
85
|
lines.push("No providers configured.");
|
|
86
86
|
}
|
|
87
87
|
else {
|
|
88
|
+
const currentProfile = typeof data?.currentProfile === "string" ? data.currentProfile : null;
|
|
89
|
+
const activeProviderResolvable = data?.activeProviderResolvable !== false;
|
|
90
|
+
const activeCandidates = Array.isArray(data?.activeProviderCandidates) ? data?.activeProviderCandidates : [];
|
|
91
|
+
if (currentProfile) {
|
|
92
|
+
lines.push(`Current profile: ${currentProfile}`);
|
|
93
|
+
if (!activeProviderResolvable && activeCandidates.length > 1) {
|
|
94
|
+
lines.push(`Current provider: ambiguous (${activeCandidates.join(", ")})`);
|
|
95
|
+
}
|
|
96
|
+
else if (!activeProviderResolvable) {
|
|
97
|
+
lines.push("Current provider: unmanaged or unresolved");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
88
100
|
for (const provider of providers) {
|
|
89
101
|
const tags = Array.isArray(provider.tags) && provider.tags.length > 0
|
|
90
102
|
? ` tags=${provider.tags.join(",")}`
|
|
91
103
|
: "";
|
|
92
104
|
const note = provider.note ? ` note=${provider.note}` : "";
|
|
93
|
-
|
|
105
|
+
const current = provider.isActive ? " current" : "";
|
|
106
|
+
lines.push(`${provider.name} [${String(provider.providerType ?? "direct")}]${current} -> ${provider.profile}${tags}${note}`);
|
|
94
107
|
}
|
|
95
108
|
}
|
|
96
109
|
break;
|
|
@@ -115,17 +128,15 @@ function renderHumanSuccess(command, data, warnings) {
|
|
|
115
128
|
lines.push(`Current profile: ${String(data?.profile ?? "")}`);
|
|
116
129
|
break;
|
|
117
130
|
case "status":
|
|
118
|
-
lines.push(
|
|
119
|
-
lines.push(`
|
|
120
|
-
lines.push(`
|
|
121
|
-
lines.push(`
|
|
122
|
-
lines.push(`
|
|
123
|
-
lines.push(`
|
|
124
|
-
|
|
125
|
-
lines.push(`
|
|
126
|
-
lines.push(`
|
|
127
|
-
lines.push(`authMode: ${String(auth.authMode ?? "")}`);
|
|
128
|
-
lines.push(`issues: ${Array.isArray(data?.issues) ? (data?.issues).length : 0}`);
|
|
131
|
+
lines.push("Status summary:");
|
|
132
|
+
lines.push(` target runtime: ${String(data?.codexDir ?? "")}`);
|
|
133
|
+
lines.push(` tool home: ${String(data?.storage?.toolHome?.root ?? "")}`);
|
|
134
|
+
lines.push(` current profile: ${String(data?.currentProfile ?? "(none)")}`);
|
|
135
|
+
lines.push(` mapped provider: ${renderStatusMappedProvider(data)}`);
|
|
136
|
+
lines.push(` provider path: ${renderStatusProviderPath(data)}`);
|
|
137
|
+
lines.push(` runtime health: ${renderStatusHealth(data)}`);
|
|
138
|
+
lines.push(` warnings: ${warnings.length}`);
|
|
139
|
+
lines.push(` next step: ${renderStatusNextStep(data, warnings)}`);
|
|
129
140
|
break;
|
|
130
141
|
case "config-show": {
|
|
131
142
|
lines.push(`activeProfile: ${String(data?.activeProfile ?? "")}`);
|
|
@@ -153,10 +164,26 @@ function renderHumanSuccess(command, data, warnings) {
|
|
|
153
164
|
lines.push(`Exported providers to ${String(data?.exportedTo ?? "")}.`);
|
|
154
165
|
break;
|
|
155
166
|
case "init":
|
|
156
|
-
lines.push(
|
|
157
|
-
lines.push(`
|
|
158
|
-
lines.push(`
|
|
159
|
-
lines.push(`
|
|
167
|
+
lines.push("Initialized codex-switch tool home.");
|
|
168
|
+
lines.push(`tool home: ${String(data?.toolHomeDir ?? "")}`);
|
|
169
|
+
lines.push(`tool config: ${String(data?.toolConfigPath ?? "")}`);
|
|
170
|
+
lines.push(`providers registry: ${String(data?.providersPath ?? "")}`);
|
|
171
|
+
lines.push(`tool home created: ${String(data?.createdToolHomeDir ?? false)}`);
|
|
172
|
+
lines.push(`tool config created: ${String(data?.createdToolConfigFile ?? false)}`);
|
|
173
|
+
lines.push(`providers registry created: ${String(data?.createdProvidersFile ?? false)}`);
|
|
174
|
+
lines.push("next step: run `codexs add ...` for a direct provider, or `codexs login copilot` before `add --copilot`.");
|
|
175
|
+
break;
|
|
176
|
+
case "login":
|
|
177
|
+
lines.push(`Copilot login ready: ${String(data?.authReady ?? false)}`);
|
|
178
|
+
lines.push(`upstream: ${String(data?.upstream ?? "")}`);
|
|
179
|
+
lines.push(`sdk installed: ${String(data?.sdkInstalled ?? false)}${data?.sdkInstalledNow ? " (installed now)" : ""}`);
|
|
180
|
+
lines.push(`copilot cli source: ${String(data?.cliSource ?? "not-needed")}`);
|
|
181
|
+
if (data?.cliCommand) {
|
|
182
|
+
lines.push(`copilot cli command: ${String(data?.cliCommand)}`);
|
|
183
|
+
}
|
|
184
|
+
lines.push(`login launched: ${String(data?.loginLaunched ?? false)}`);
|
|
185
|
+
lines.push(`auth ready: ${String(data?.authReady ?? false)}`);
|
|
186
|
+
lines.push("next step: run `codexs add <provider> --copilot --profile <name>` and then `codexs switch <provider>`.");
|
|
160
187
|
break;
|
|
161
188
|
case "migrate":
|
|
162
189
|
lines.push(`Migrated providers in ${String(data?.codexDir ?? "")} using ${String(data?.strategy ?? "")}.`);
|
|
@@ -185,10 +212,12 @@ function renderHumanSuccess(command, data, warnings) {
|
|
|
185
212
|
break;
|
|
186
213
|
case "doctor": {
|
|
187
214
|
const healthy = Boolean(data?.healthy);
|
|
188
|
-
lines.push(healthy ? "No issues found." : "Issues found:");
|
|
189
215
|
const issues = data?.issues ?? [];
|
|
216
|
+
lines.push(healthy ? "Doctor summary: healthy. No action required." : `Doctor summary: ${issues.length} issue(s) need attention.`);
|
|
217
|
+
lines.push(`target runtime: ${String(data?.codexDir ?? "")}`);
|
|
190
218
|
for (const issue of issues) {
|
|
191
|
-
lines.push(
|
|
219
|
+
lines.push(`- ${String(issue.code)}: ${String(issue.message)}`);
|
|
220
|
+
lines.push(` next step: ${renderDoctorIssueNextStep(issue)}`);
|
|
192
221
|
}
|
|
193
222
|
break;
|
|
194
223
|
}
|
|
@@ -212,6 +241,112 @@ function renderHumanSuccess(command, data, warnings) {
|
|
|
212
241
|
}
|
|
213
242
|
return lines;
|
|
214
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* Summarizes runtime health for the human-readable status output.
|
|
246
|
+
*/
|
|
247
|
+
function renderStatusHealth(data) {
|
|
248
|
+
const configExists = Boolean(data?.configExists);
|
|
249
|
+
const providersExists = Boolean(data?.providersExists);
|
|
250
|
+
const auth = data?.auth ?? {};
|
|
251
|
+
const bridge = data?.copilotBridge ?? null;
|
|
252
|
+
const issues = Array.isArray(data?.issues) ? data?.issues : [];
|
|
253
|
+
const activeProviderResolvable = data?.activeProviderResolvable !== false;
|
|
254
|
+
const liveState = data?.liveState ?? {};
|
|
255
|
+
const copilotSdk = data?.copilotSdk ?? {};
|
|
256
|
+
const copilotAuth = data?.copilotAuth ?? null;
|
|
257
|
+
const runtimeProvider = typeof data?.runtimeProvider === "string" ? data.runtimeProvider : null;
|
|
258
|
+
const activePathUsesCopilot = runtimeProvider === "copilot-sdk-bridge";
|
|
259
|
+
if (!configExists || !providersExists) {
|
|
260
|
+
return "incomplete local state";
|
|
261
|
+
}
|
|
262
|
+
if (!activeProviderResolvable || liveState.reason === "shared-profile") {
|
|
263
|
+
return "active provider ambiguous";
|
|
264
|
+
}
|
|
265
|
+
if (issues.some((issue) => issue.code === "UNMANAGED_ACTIVE_PROFILE")) {
|
|
266
|
+
return "active profile unmanaged";
|
|
267
|
+
}
|
|
268
|
+
if (issues.some((issue) => issue.code === "ACTIVE_PROVIDER_UNRESOLVED")) {
|
|
269
|
+
return "active provider ambiguous";
|
|
270
|
+
}
|
|
271
|
+
if (issues.some((issue) => issue.code === "PROVIDER_BASE_URL_MISMATCH")) {
|
|
272
|
+
return "provider projection drift";
|
|
273
|
+
}
|
|
274
|
+
if (activePathUsesCopilot && copilotSdk.installed === false) {
|
|
275
|
+
return "copilot sdk missing";
|
|
276
|
+
}
|
|
277
|
+
if (activePathUsesCopilot && copilotAuth && copilotAuth.ready === false) {
|
|
278
|
+
return "copilot auth required";
|
|
279
|
+
}
|
|
280
|
+
if (activePathUsesCopilot && bridge && bridge.ok === false) {
|
|
281
|
+
return "copilot runtime needs repair";
|
|
282
|
+
}
|
|
283
|
+
if (auth.exists === false) {
|
|
284
|
+
return "auth projection missing";
|
|
285
|
+
}
|
|
286
|
+
if (auth.valid === false) {
|
|
287
|
+
return "auth projection invalid";
|
|
288
|
+
}
|
|
289
|
+
return "ok";
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Renders the mapped provider line without claiming a unique winner for shared profiles.
|
|
293
|
+
*/
|
|
294
|
+
function renderStatusMappedProvider(data) {
|
|
295
|
+
if (typeof data?.provider === "string" && data.provider.length > 0) {
|
|
296
|
+
return data.provider;
|
|
297
|
+
}
|
|
298
|
+
const candidates = Array.isArray(data?.activeProviderCandidates) ? data?.activeProviderCandidates : [];
|
|
299
|
+
if (candidates.length > 1) {
|
|
300
|
+
return `(ambiguous: ${candidates.join(", ")})`;
|
|
301
|
+
}
|
|
302
|
+
return "(unmanaged or unresolved)";
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Renders the active workflow path in status output.
|
|
306
|
+
*/
|
|
307
|
+
function renderStatusProviderPath(data) {
|
|
308
|
+
return typeof data?.runtimeProvider === "string" && data.runtimeProvider === "copilot-sdk-bridge" ? "copilot" : "direct";
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Suggests the next operator action for the human-readable status output.
|
|
312
|
+
*/
|
|
313
|
+
function renderStatusNextStep(data, warnings) {
|
|
314
|
+
if (warnings.length > 0) {
|
|
315
|
+
return "run `codexs doctor` to inspect warnings before the next write command";
|
|
316
|
+
}
|
|
317
|
+
if (!data?.provider) {
|
|
318
|
+
return "run `codexs switch <provider>` after adding or adopting a managed provider";
|
|
319
|
+
}
|
|
320
|
+
return "run `codexs doctor` if you need a deeper diagnostic pass";
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Turns structured doctor issue codes into repair-oriented next steps.
|
|
324
|
+
*/
|
|
325
|
+
function renderDoctorIssueNextStep(issue) {
|
|
326
|
+
switch (issue.code) {
|
|
327
|
+
case "CONFIG_NOT_FOUND":
|
|
328
|
+
return "restore or create config.toml before switching providers";
|
|
329
|
+
case "PROVIDERS_NOT_FOUND":
|
|
330
|
+
return "run `codexs init` and then add or migrate providers";
|
|
331
|
+
case "COPILOT_SDK_MISSING":
|
|
332
|
+
return "run `codexs login copilot` to install the optional Copilot runtime";
|
|
333
|
+
case "COPILOT_AUTH_REQUIRED":
|
|
334
|
+
return "run `codexs login copilot` to complete upstream authentication";
|
|
335
|
+
case "BRIDGE_STATE_STALE":
|
|
336
|
+
case "BRIDGE_STATE_MISSING":
|
|
337
|
+
case "BRIDGE_HEALTHCHECK_FAILED":
|
|
338
|
+
return "reselect the provider with `codexs switch <provider>` or inspect bridge state";
|
|
339
|
+
case "UNMANAGED_ACTIVE_PROFILE":
|
|
340
|
+
return "switch to a managed provider or adopt the active profile with `codexs migrate`";
|
|
341
|
+
case "ACTIVE_PROVIDER_UNRESOLVED":
|
|
342
|
+
case "SHARED_PROFILE_REFERENCE":
|
|
343
|
+
return "make provider-to-profile mappings unique before relying on current-provider detection";
|
|
344
|
+
case "PROVIDER_BASE_URL_MISMATCH":
|
|
345
|
+
return "rerun `codexs edit <provider> --base-url <url>` or `codexs switch <provider>` to repair the runtime projection";
|
|
346
|
+
default:
|
|
347
|
+
return "inspect the issue details and rerun `codexs doctor` after fixing the state";
|
|
348
|
+
}
|
|
349
|
+
}
|
|
215
350
|
/**
|
|
216
351
|
* Writes one rendered line to either stdout or stderr.
|
|
217
352
|
*/
|
|
@@ -79,11 +79,11 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
79
79
|
const paths = setupPaths;
|
|
80
80
|
switch (ctx.command) {
|
|
81
81
|
case "list":
|
|
82
|
-
return (0, list_providers_1.listProviders)(paths.providersPath);
|
|
82
|
+
return (0, list_providers_1.listProviders)(paths.providersPath, paths.configPath);
|
|
83
83
|
case "show": {
|
|
84
84
|
let providerName = parsed.positionals[0] ?? null;
|
|
85
85
|
if (!providerName && (0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
|
|
86
|
-
providerName = await (0, interactive_1.promptForProviderSelection)(runtime, paths.providersPath, "Choose a provider to show");
|
|
86
|
+
providerName = await (0, interactive_1.promptForProviderSelection)(runtime, paths.providersPath, paths.configPath, "Choose a provider to show");
|
|
87
87
|
}
|
|
88
88
|
if (!providerName) {
|
|
89
89
|
throw (0, errors_1.cliError)("INVALID_ARGUMENT", "Missing provider name for show command.");
|
|
@@ -172,6 +172,7 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
172
172
|
(0, copilot_installer_1.installCopilotSdk)(paths.runtimesDir);
|
|
173
173
|
installedNow = true;
|
|
174
174
|
}
|
|
175
|
+
const availability = (0, copilot_cli_1.checkCopilotCliAvailable)(paths.runtimesDir);
|
|
175
176
|
try {
|
|
176
177
|
await (0, copilot_adapter_1.readCopilotAuthState)(paths.runtimesDir);
|
|
177
178
|
return {
|
|
@@ -181,6 +182,8 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
181
182
|
sdkInstalledNow: installedNow,
|
|
182
183
|
authReady: true,
|
|
183
184
|
loginLaunched: false,
|
|
185
|
+
cliSource: availability.ok ? availability.source ?? null : null,
|
|
186
|
+
cliCommand: availability.command ?? null,
|
|
184
187
|
},
|
|
185
188
|
};
|
|
186
189
|
}
|
|
@@ -190,7 +193,6 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
190
193
|
throw error;
|
|
191
194
|
}
|
|
192
195
|
}
|
|
193
|
-
const availability = (0, copilot_cli_1.checkCopilotCliAvailable)(paths.runtimesDir);
|
|
194
196
|
if (!availability.ok) {
|
|
195
197
|
throw (0, errors_1.cliError)("COPILOT_CLI_MISSING", "The official Copilot CLI could not be resolved from the installed runtime or PATH.", {
|
|
196
198
|
cause: availability.cause,
|
|
@@ -225,6 +227,8 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
225
227
|
sdkInstalledNow: installedNow,
|
|
226
228
|
authReady: true,
|
|
227
229
|
loginLaunched: true,
|
|
230
|
+
cliSource: availability.source ?? null,
|
|
231
|
+
cliCommand: availability.command ?? null,
|
|
228
232
|
},
|
|
229
233
|
};
|
|
230
234
|
}
|
|
@@ -242,7 +246,7 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
242
246
|
case "switch": {
|
|
243
247
|
let providerName = parsed.positionals[0] ?? null;
|
|
244
248
|
if (!providerName && (0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
|
|
245
|
-
providerName = await (0, interactive_1.promptForProviderSelection)(runtime, paths.providersPath, "Choose a provider to switch to");
|
|
249
|
+
providerName = await (0, interactive_1.promptForProviderSelection)(runtime, paths.providersPath, paths.configPath, "Choose a provider to switch to");
|
|
246
250
|
}
|
|
247
251
|
if (!providerName) {
|
|
248
252
|
throw (0, errors_1.cliError)("PROVIDER_NOT_FOUND", "Missing provider name for switch command.");
|
|
@@ -413,7 +417,7 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
413
417
|
case "edit": {
|
|
414
418
|
let providerName = parsed.positionals[0] ?? null;
|
|
415
419
|
if (!providerName && (0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
|
|
416
|
-
providerName = await (0, interactive_1.promptForProviderSelection)(runtime, paths.providersPath, "Choose a provider to edit");
|
|
420
|
+
providerName = await (0, interactive_1.promptForProviderSelection)(runtime, paths.providersPath, paths.configPath, "Choose a provider to edit");
|
|
417
421
|
}
|
|
418
422
|
if (!providerName) {
|
|
419
423
|
throw (0, errors_1.cliError)("INVALID_ARGUMENT", "Missing provider name for edit command.");
|
|
@@ -472,7 +476,7 @@ async function handleRegisteredCommand(ctx, parsed, runtime = (0, prompt_1.creat
|
|
|
472
476
|
const force = (0, args_1.hasFlag)(parsed.commandOptions, "--force");
|
|
473
477
|
const switchToProfile = (0, args_1.getSingleOption)(parsed.commandOptions, "--switch-to", false) ?? undefined;
|
|
474
478
|
if (!providerName && (0, interactive_1.canPrompt)(runtime, ctx.options.json)) {
|
|
475
|
-
providerName = await (0, interactive_1.promptForProviderSelection)(runtime, paths.providersPath, "Choose a provider to remove");
|
|
479
|
+
providerName = await (0, interactive_1.promptForProviderSelection)(runtime, paths.providersPath, paths.configPath, "Choose a provider to remove");
|
|
476
480
|
}
|
|
477
481
|
if (!providerName) {
|
|
478
482
|
throw (0, errors_1.cliError)("PROVIDER_NOT_FOUND", "Missing provider name for remove command.");
|
package/dist/commands/help.js
CHANGED
|
@@ -30,6 +30,10 @@ function buildHelpText(commandName) {
|
|
|
30
30
|
"codex-switch",
|
|
31
31
|
"",
|
|
32
32
|
"Manage and switch local Codex provider/profile configuration safely.",
|
|
33
|
+
"Primary workflows: direct providers use init -> add -> switch -> status -> doctor.",
|
|
34
|
+
"Primary workflows: Copilot providers use init -> login copilot -> add --copilot -> switch -> status -> doctor.",
|
|
35
|
+
"Advanced adopt flows use migrate only when you already have Codex runtime state to import.",
|
|
36
|
+
"Deprecated entry: setup still exists only to point callers to init or migrate.",
|
|
33
37
|
"",
|
|
34
38
|
"Usage:",
|
|
35
39
|
" codexs <command> [options]",
|
|
@@ -61,14 +65,14 @@ function buildHelpText(commandName) {
|
|
|
61
65
|
"",
|
|
62
66
|
"Examples:",
|
|
63
67
|
" codexs init",
|
|
68
|
+
" codexs add packycode --profile packycode --api-key sk-xxx",
|
|
69
|
+
" codexs switch packycode",
|
|
70
|
+
" codexs status",
|
|
71
|
+
" codexs doctor",
|
|
64
72
|
" codexs login copilot",
|
|
73
|
+
" codexs add copilot-main --copilot --profile copilot-main",
|
|
65
74
|
" codexs migrate",
|
|
66
|
-
" codexs list",
|
|
67
|
-
" codexs switch",
|
|
68
|
-
" codexs bridge start",
|
|
69
|
-
" codexs add packycode --profile packycode --api-key sk-xxx",
|
|
70
75
|
" codexs config show",
|
|
71
|
-
" codexs remove freemodel",
|
|
72
76
|
" codexs backups list",
|
|
73
77
|
" codexs rollback",
|
|
74
78
|
" codexs help add",
|
|
@@ -88,13 +88,14 @@ exports.COMMANDS = [
|
|
|
88
88
|
tokens: ["init"],
|
|
89
89
|
handler: handlers_1.handleRegisteredCommand,
|
|
90
90
|
group: "write",
|
|
91
|
-
summary: "Initialize the codex-switch tool home
|
|
91
|
+
summary: "Initialize the codex-switch tool home for the primary workflow.",
|
|
92
92
|
usage: ["codexs init [--json] [--codex-dir <path>]"],
|
|
93
93
|
details: [
|
|
94
94
|
"Creates codex-switch.json and providers.json under the tool home when they do not exist yet.",
|
|
95
95
|
"Does not create or validate config.toml, auth.json, or the target Codex directory.",
|
|
96
96
|
"When --codex-dir is passed explicitly and codex-switch.json does not exist yet, init persists it as defaultCodexDir.",
|
|
97
97
|
"Otherwise init stays scoped to tool-home state and does not persist fallback Codex directory resolution.",
|
|
98
|
+
"Use init first for fresh direct-provider or Copilot setups.",
|
|
98
99
|
],
|
|
99
100
|
examples: ["codexs init", "codexs init --json --codex-dir ~/.codex"],
|
|
100
101
|
},
|
|
@@ -119,13 +120,14 @@ exports.COMMANDS = [
|
|
|
119
120
|
tokens: ["migrate"],
|
|
120
121
|
handler: handlers_1.handleRegisteredCommand,
|
|
121
122
|
group: "write",
|
|
122
|
-
summary: "Adopt
|
|
123
|
+
summary: "Adopt existing Codex runtime profiles into managed providers.json state.",
|
|
123
124
|
usage: ["codexs migrate [--json] [--codex-dir <path>] [--merge|--overwrite]"],
|
|
124
125
|
details: [
|
|
125
126
|
"Reads config.toml profiles, collects complete provider records, then writes providers.json under managed backup flow.",
|
|
126
127
|
"TTY mode can collect missing provider details and choose merge or overwrite when providers.json already exists.",
|
|
127
128
|
"Migrate adopts only runtime profiles that already expose model, model_provider, and matching base_url.",
|
|
128
129
|
"Non-TTY and --json runs still fail fast because migrate profile selection and provider details remain interactive in this release.",
|
|
130
|
+
"Treat migrate as an advanced adopt helper for existing runtime state, not the default first step for fresh installs.",
|
|
129
131
|
],
|
|
130
132
|
examples: ["codexs migrate", "codexs migrate --overwrite --json --codex-dir ~/.codex"],
|
|
131
133
|
},
|
|
@@ -134,12 +136,12 @@ exports.COMMANDS = [
|
|
|
134
136
|
tokens: ["setup"],
|
|
135
137
|
handler: handlers_1.handleRegisteredCommand,
|
|
136
138
|
group: "write",
|
|
137
|
-
summary: "Deprecated.
|
|
139
|
+
summary: "Deprecated. Kept only to point callers to init or migrate.",
|
|
138
140
|
usage: ["codexs setup"],
|
|
139
141
|
details: [
|
|
140
142
|
"setup no longer performs initialization or migration work.",
|
|
141
|
-
"Use init for
|
|
142
|
-
"Use migrate
|
|
143
|
+
"Use init for the primary fresh-install workflow.",
|
|
144
|
+
"Use migrate only when adopting from existing config.toml profiles.",
|
|
143
145
|
],
|
|
144
146
|
examples: ["codexs help init", "codexs help migrate"],
|
|
145
147
|
},
|
|
@@ -148,9 +150,13 @@ exports.COMMANDS = [
|
|
|
148
150
|
tokens: ["list"],
|
|
149
151
|
handler: handlers_1.handleRegisteredCommand,
|
|
150
152
|
group: "read",
|
|
151
|
-
summary: "List
|
|
153
|
+
summary: "List managed providers with profile, type, and current-state hints.",
|
|
152
154
|
usage: ["codexs list [--json] [--codex-dir <path>]"],
|
|
153
|
-
details: [
|
|
155
|
+
details: [
|
|
156
|
+
"Reads providers.json and prints provider-to-profile mappings together with provider type.",
|
|
157
|
+
"When the active profile is shared by multiple providers, list surfaces the ambiguity instead of inventing one current provider.",
|
|
158
|
+
"Use --json for machine-readable automation output.",
|
|
159
|
+
],
|
|
154
160
|
examples: ["codexs list", "codexs list --json"],
|
|
155
161
|
},
|
|
156
162
|
{
|
|
@@ -182,12 +188,13 @@ exports.COMMANDS = [
|
|
|
182
188
|
tokens: ["status"],
|
|
183
189
|
handler: handlers_1.handleRegisteredCommand,
|
|
184
190
|
group: "read",
|
|
185
|
-
summary: "Show
|
|
191
|
+
summary: "Show tool-home, target-runtime, provider-path, and runtime-health status.",
|
|
186
192
|
usage: ["codexs status [--json] [--codex-dir <path>]"],
|
|
187
193
|
details: [
|
|
188
|
-
"Reports
|
|
189
|
-
"When the active provider uses a local runtime bridge, status also reports bridge
|
|
190
|
-
"Surfaces config consistency signals without mutating any files.",
|
|
194
|
+
"Reports the target Codex runtime, tool-home storage roles, current profile, and whether the live profile is mapped.",
|
|
195
|
+
"When the active provider uses a local runtime bridge, status also reports bridge, Copilot SDK, and upstream auth state.",
|
|
196
|
+
"Surfaces dual-path config consistency signals without mutating any files.",
|
|
197
|
+
"Organizes the human-readable view around current state, health impact, and next step.",
|
|
191
198
|
"Use doctor for deeper diagnostics.",
|
|
192
199
|
],
|
|
193
200
|
examples: ["codexs status", "codexs status --json --codex-dir ./.tmp-codex"],
|
|
@@ -216,7 +223,7 @@ exports.COMMANDS = [
|
|
|
216
223
|
tokens: ["add"],
|
|
217
224
|
handler: handlers_1.handleRegisteredCommand,
|
|
218
225
|
group: "write",
|
|
219
|
-
summary: "Add a provider
|
|
226
|
+
summary: "Add a managed provider for the primary direct or Copilot workflows.",
|
|
220
227
|
usage: [
|
|
221
228
|
"codexs add <provider> --profile <name> --api-key <key> [--base-url <url>] [--note <text>] [--tag <tag> ...]",
|
|
222
229
|
"codexs add <provider> --copilot --profile <name> [--bridge-host <host>] [--bridge-port <port>] [--bridge-api-key <secret>] [--install-copilot-sdk]",
|
|
@@ -247,7 +254,7 @@ exports.COMMANDS = [
|
|
|
247
254
|
tokens: ["switch"],
|
|
248
255
|
handler: handlers_1.handleRegisteredCommand,
|
|
249
256
|
group: "write",
|
|
250
|
-
summary: "Switch the active
|
|
257
|
+
summary: "Switch the active runtime to a managed provider.",
|
|
251
258
|
usage: ["codexs switch <provider> [--json] [--codex-dir <path>]"],
|
|
252
259
|
details: [
|
|
253
260
|
"When <provider> is omitted in a TTY, an interactive provider selector is shown.",
|
|
@@ -255,6 +262,7 @@ exports.COMMANDS = [
|
|
|
255
262
|
"Direct providers update the active config profile and rewrite auth.json with auth_mode=apikey plus OPENAI_API_KEY.",
|
|
256
263
|
"Copilot bridge providers also rewrite OPENAI_API_KEY to the local bridge secret while managing runtime routing and bridge state.",
|
|
257
264
|
"Copilot bridge providers probe the optional official SDK before switching and fail fast if it is missing.",
|
|
265
|
+
"Switch succeeds only after the managed profile projection is written to the target runtime.",
|
|
258
266
|
"Backs up config.toml and auth.json and rolls back on failure.",
|
|
259
267
|
],
|
|
260
268
|
examples: ["codexs switch freemodel", "codexs switch packycode --json"],
|
|
@@ -321,7 +329,7 @@ exports.COMMANDS = [
|
|
|
321
329
|
tokens: ["doctor"],
|
|
322
330
|
handler: handlers_1.handleRegisteredCommand,
|
|
323
331
|
group: "recovery",
|
|
324
|
-
summary: "Run
|
|
332
|
+
summary: "Run issue-first diagnostics across tool-home and target-runtime state.",
|
|
325
333
|
usage: ["codexs doctor [--json] [--codex-dir <path>]"],
|
|
326
334
|
details: [
|
|
327
335
|
"Checks the expected config files, provider/profile consistency, and Codex CLI availability.",
|
package/dist/domain/config.js
CHANGED
|
@@ -268,6 +268,8 @@ function buildManagedProfileViews(document, providers) {
|
|
|
268
268
|
*/
|
|
269
269
|
function collectConfigConsistencyIssues(document, providers) {
|
|
270
270
|
const issues = [];
|
|
271
|
+
const providerMap = providers?.providers ?? null;
|
|
272
|
+
const profileLinkMap = buildProfileLinkMap(providers);
|
|
271
273
|
for (const view of buildManagedProfileViews(document, providers)) {
|
|
272
274
|
if (view.source === "orphaned-reference") {
|
|
273
275
|
issues.push({
|
|
@@ -319,11 +321,34 @@ function collectConfigConsistencyIssues(document, providers) {
|
|
|
319
321
|
modelProvider: view.modelProvider,
|
|
320
322
|
});
|
|
321
323
|
}
|
|
324
|
+
else {
|
|
325
|
+
const profileLinkInfo = profileLinkMap.get(view.name);
|
|
326
|
+
if (profileLinkInfo &&
|
|
327
|
+
profileLinkInfo.linkedProviders.length === 1 &&
|
|
328
|
+
providerMap) {
|
|
329
|
+
const providerName = profileLinkInfo.linkedProviders[0];
|
|
330
|
+
const provider = providerMap[providerName];
|
|
331
|
+
if (provider &&
|
|
332
|
+
!provider.runtime &&
|
|
333
|
+
typeof provider.baseUrl === "string" &&
|
|
334
|
+
provider.baseUrl.trim() !== "" &&
|
|
335
|
+
provider.baseUrl !== modelProviderSection.baseUrl) {
|
|
336
|
+
issues.push({
|
|
337
|
+
code: "PROVIDER_BASE_URL_MISMATCH",
|
|
338
|
+
profile: view.name,
|
|
339
|
+
provider: providerName,
|
|
340
|
+
providerBaseUrl: provider.baseUrl,
|
|
341
|
+
configBaseUrl: modelProviderSection.baseUrl,
|
|
342
|
+
providerType: "direct",
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
322
347
|
}
|
|
323
348
|
}
|
|
324
349
|
}
|
|
325
350
|
if (document.activeProfile) {
|
|
326
|
-
const activeLinkInfo =
|
|
351
|
+
const activeLinkInfo = profileLinkMap.get(document.activeProfile);
|
|
327
352
|
if (!activeLinkInfo) {
|
|
328
353
|
issues.push({
|
|
329
354
|
code: "UNMANAGED_ACTIVE_PROFILE",
|
package/dist/domain/providers.js
CHANGED
|
@@ -10,6 +10,7 @@ exports.isRuntimeBackedProvider = isRuntimeBackedProvider;
|
|
|
10
10
|
exports.isCopilotBridgeProvider = isCopilotBridgeProvider;
|
|
11
11
|
exports.buildCopilotBridgeBaseUrl = buildCopilotBridgeBaseUrl;
|
|
12
12
|
exports.buildCopilotModelProviderProjection = buildCopilotModelProviderProjection;
|
|
13
|
+
exports.buildDirectModelProviderProjection = buildDirectModelProviderProjection;
|
|
13
14
|
/**
|
|
14
15
|
* Validates and normalizes unknown JSON into the providers.json domain model.
|
|
15
16
|
*/
|
|
@@ -162,6 +163,21 @@ function buildCopilotModelProviderProjection(runtime) {
|
|
|
162
163
|
wireApi: "responses",
|
|
163
164
|
};
|
|
164
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Builds the Codex-facing custom model_provider projection for a direct provider.
|
|
168
|
+
*/
|
|
169
|
+
function buildDirectModelProviderProjection(profile, baseUrl) {
|
|
170
|
+
const normalizedBaseUrl = baseUrl.trim();
|
|
171
|
+
if (!normalizedBaseUrl) {
|
|
172
|
+
throw new Error(`Direct model provider "${profile}" requires a non-empty base_url.`);
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
baseUrl: normalizedBaseUrl,
|
|
176
|
+
name: profile.trim(),
|
|
177
|
+
requiresOpenAiAuth: true,
|
|
178
|
+
wireApi: "responses",
|
|
179
|
+
};
|
|
180
|
+
}
|
|
165
181
|
/**
|
|
166
182
|
* Validates one runtime-backed provider block.
|
|
167
183
|
*/
|
|
@@ -97,7 +97,9 @@ function inspectLiveStateDrift(currentProfile, providers) {
|
|
|
97
97
|
return {
|
|
98
98
|
currentProfile,
|
|
99
99
|
mappedProvider: null,
|
|
100
|
+
mappedProviders: [],
|
|
100
101
|
profileMapped: false,
|
|
102
|
+
providerResolvable: false,
|
|
101
103
|
canBackfillActiveProvider: false,
|
|
102
104
|
reason: providers ? "profile-missing" : "config-missing",
|
|
103
105
|
};
|
|
@@ -106,27 +108,47 @@ function inspectLiveStateDrift(currentProfile, providers) {
|
|
|
106
108
|
return {
|
|
107
109
|
currentProfile,
|
|
108
110
|
mappedProvider: null,
|
|
111
|
+
mappedProviders: [],
|
|
109
112
|
profileMapped: false,
|
|
113
|
+
providerResolvable: false,
|
|
110
114
|
canBackfillActiveProvider: false,
|
|
111
115
|
reason: "providers-missing",
|
|
112
116
|
};
|
|
113
117
|
}
|
|
118
|
+
const mappedProviders = [];
|
|
114
119
|
for (const [name, provider] of Object.entries(providers.providers)) {
|
|
115
|
-
// A direct profile match means the runtime state is still managed.
|
|
116
120
|
if (provider.profile === currentProfile) {
|
|
117
|
-
|
|
118
|
-
currentProfile,
|
|
119
|
-
mappedProvider: name,
|
|
120
|
-
profileMapped: true,
|
|
121
|
-
canBackfillActiveProvider: false,
|
|
122
|
-
reason: "ok",
|
|
123
|
-
};
|
|
121
|
+
mappedProviders.push(name);
|
|
124
122
|
}
|
|
125
123
|
}
|
|
124
|
+
if (mappedProviders.length === 1) {
|
|
125
|
+
return {
|
|
126
|
+
currentProfile,
|
|
127
|
+
mappedProvider: mappedProviders[0],
|
|
128
|
+
mappedProviders,
|
|
129
|
+
profileMapped: true,
|
|
130
|
+
providerResolvable: true,
|
|
131
|
+
canBackfillActiveProvider: false,
|
|
132
|
+
reason: "ok",
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (mappedProviders.length > 1) {
|
|
136
|
+
return {
|
|
137
|
+
currentProfile,
|
|
138
|
+
mappedProvider: null,
|
|
139
|
+
mappedProviders,
|
|
140
|
+
profileMapped: true,
|
|
141
|
+
providerResolvable: false,
|
|
142
|
+
canBackfillActiveProvider: false,
|
|
143
|
+
reason: "shared-profile",
|
|
144
|
+
};
|
|
145
|
+
}
|
|
126
146
|
return {
|
|
127
147
|
currentProfile,
|
|
128
148
|
mappedProvider: null,
|
|
149
|
+
mappedProviders: [],
|
|
129
150
|
profileMapped: false,
|
|
151
|
+
providerResolvable: false,
|
|
130
152
|
canBackfillActiveProvider: true,
|
|
131
153
|
reason: "provider-unmapped",
|
|
132
154
|
};
|