@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.
@@ -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
- lines.push(`${provider.name} -> ${provider.profile}${tags}${note}`);
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(`codexDir: ${String(data?.codexDir ?? "")}`);
119
- lines.push(`configExists: ${String(data?.configExists ?? false)}`);
120
- lines.push(`providersExists: ${String(data?.providersExists ?? false)}`);
121
- lines.push(`currentProfile: ${String(data?.currentProfile ?? "")}`);
122
- lines.push(`mappedProvider: ${String(data?.provider ?? "")}`);
123
- lines.push(`activeProviderResolvable: ${String(data?.activeProviderResolvable ?? false)}`);
124
- const auth = data?.auth ?? {};
125
- lines.push(`authExists: ${String(auth.exists ?? false)}`);
126
- lines.push(`authValid: ${String(auth.valid ?? false)}`);
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(`Initialized Codex directory ${String(data?.codexDir ?? "")}.`);
157
- lines.push(`Created codexDir: ${String(data?.createdCodexDir ?? false)}`);
158
- lines.push(`Created providers.json: ${String(data?.createdProvidersFile ?? false)}`);
159
- lines.push(`providersAlreadyExisted: ${String(data?.providersAlreadyExisted ?? false)}`);
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(`${issue.code}: ${issue.message}`);
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.");
@@ -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 and registry files.",
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 unmanaged Codex config profiles into providers.json.",
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. Use init or migrate instead.",
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 AI-friendly idempotent providers.json initialization.",
142
- "Use migrate for interactive adoption from existing config.toml profiles.",
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 configured providers from providers.json.",
153
+ summary: "List managed providers with profile, type, and current-state hints.",
152
154
  usage: ["codexs list [--json] [--codex-dir <path>]"],
153
- details: ["Reads providers.json and prints provider-to-profile mappings.", "Use --json for machine-readable automation output."],
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 a quick status summary for the local Codex directory.",
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 file presence, current profile, and whether the live profile is mapped.",
189
- "When the active provider uses a local runtime bridge, status also reports bridge and SDK state.",
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 with explicit flags or progressive TTY prompts.",
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 config profile to a provider.",
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 configuration and environment diagnostics.",
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.",
@@ -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 = buildProfileLinkMap(providers).get(document.activeProfile);
351
+ const activeLinkInfo = profileLinkMap.get(document.activeProfile);
327
352
  if (!activeLinkInfo) {
328
353
  issues.push({
329
354
  code: "UNMANAGED_ACTIVE_PROFILE",
@@ -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
- return {
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
  };