@minniexcode/codex-switch 0.0.11 → 0.0.12

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.md CHANGED
@@ -1,89 +1,78 @@
1
- ## @minniexcode/codex-switch
1
+ # @minniexcode/codex-switch
2
2
 
3
- `codex-switch` is a local-first CLI for managing and switching Codex provider/profile configuration safely.
3
+ `@minniexcode/codex-switch` is a local-first CLI for managing and switching Codex provider and profile configuration safely.
4
4
 
5
- It is designed for users who work with multiple Codex providers, API keys, or profiles and want a repeatable, backup-first workflow instead of manually editing files under `~/.codex/`.
5
+ It keeps `codex-switch` tool state separate from the target Codex runtime, so provider management, backup flow, and runtime projection stay explicit instead of relying on manual file edits.
6
6
 
7
- 中文版: [README.CN.md](./README.CN.md)
7
+ Chinese version: [README.CN.md](./README.CN.md)
8
8
 
9
- ## Overview
9
+ ## Version
10
10
 
11
- What it does in `0.0.11`:
11
+ Current package version: `0.0.12`
12
12
 
13
- - Initializes a dedicated `codex-switch` tool home
14
- - Adopts unmanaged runtime profiles from an existing Codex directory
15
- - Lists, shows, adds, edits, and removes provider records
16
- - Switches the active provider/profile safely
17
- - Supports explicit GitHub Copilot upstream onboarding
18
- - Manages the local Copilot bridge runtime explicitly
19
- - Imports and exports provider definitions
20
- - Runs diagnostics and detects local drift
21
- - Lists backups and rolls back to a previous managed state
22
- - Inspects `config.toml` profiles through structured read commands
23
-
24
- Current version: `0.0.11`
13
+ This repository is still on a development-version line. The current release focuses on making the primary workflows, help text, and operational boundaries consistent with the implementation.
25
14
 
26
15
  ## Install
27
16
 
28
- Install globally:
29
-
30
17
  ```bash
31
18
  npm install -g @minniexcode/codex-switch
32
19
  ```
33
20
 
34
- Or run directly:
21
+ Run without a global install:
35
22
 
36
23
  ```bash
37
24
  npx @minniexcode/codex-switch --help
38
25
  ```
39
26
 
40
- CLI entry:
27
+ Built CLI entrypoint:
41
28
 
42
29
  ```bash
43
30
  codexs --help
44
31
  ```
45
32
 
46
- ## Quick Start
33
+ ## Primary Workflows
47
34
 
48
- Initialize tool state and adopt an existing Codex runtime:
35
+ Direct provider workflow:
49
36
 
50
37
  ```bash
51
38
  codexs init
52
- codexs migrate
53
- ```
54
-
55
- Inspect managed providers and config:
56
-
57
- ```bash
58
- codexs list
59
- codexs show my-provider
60
- codexs config show
61
- ```
62
-
63
- Add and switch a direct provider:
64
-
65
- ```bash
66
39
  codexs add my-provider --profile my-provider --api-key sk-xxx
67
40
  codexs switch my-provider
41
+ codexs status
42
+ codexs doctor
68
43
  ```
69
44
 
70
- Prepare GitHub Copilot and manage its bridge:
45
+ GitHub Copilot workflow:
71
46
 
72
47
  ```bash
48
+ codexs init
73
49
  codexs login copilot
74
50
  codexs add copilot-main --copilot --profile copilot-main
75
- codexs bridge start copilot-main
51
+ codexs switch copilot-main
52
+ codexs status
53
+ codexs doctor
76
54
  ```
77
55
 
78
- Check runtime state:
56
+ Notes:
57
+
58
+ - `init` prepares the `codex-switch` tool home and managed state.
59
+ - `login copilot` handles upstream Copilot onboarding and auth readiness.
60
+ - `add --copilot` does not perform login for you; it assumes Copilot login is already ready.
61
+ - `status` is the main read command after switching.
62
+ - `doctor` is the main repair-oriented diagnostic command.
63
+
64
+ ## Advanced Adopt Workflow
65
+
66
+ Use `migrate` only when you already have Codex runtime state that should be adopted into managed `providers.json` state:
79
67
 
80
68
  ```bash
81
- codexs current
82
- codexs status
83
- codexs doctor
69
+ codexs init
70
+ codexs migrate
84
71
  ```
85
72
 
86
- ## Common Commands
73
+ `migrate` is an advanced adopt helper. It is not the default first step for a fresh install.
74
+
75
+ ## Command Surface
87
76
 
88
77
  ```bash
89
78
  codexs init
@@ -99,31 +88,22 @@ codexs add <provider> --profile <name> --api-key <key>
99
88
  codexs add <provider> --copilot --profile <name>
100
89
  codexs edit <provider>
101
90
  codexs switch <provider>
91
+ codexs remove <provider> [--force] [--switch-to <profile>]
92
+ codexs import <file>
93
+ codexs export <file> [--force]
102
94
  codexs bridge start [provider]
103
95
  codexs bridge status [provider]
104
96
  codexs bridge stop [provider]
105
- codexs remove <provider> [--force] [--switch-to <profile>]
106
- codexs import <file> [--merge]
107
- codexs export <file> [--force]
108
97
  codexs backups list
109
98
  codexs rollback [backup-id]
110
99
  codexs doctor
111
100
  ```
112
101
 
113
- Command help:
102
+ `setup` still exists only as a deprecated compatibility entry that points callers to `init` or `migrate`.
114
103
 
115
- ```bash
116
- codexs help init
117
- codexs help login
118
- codexs help add
119
- codexs help bridge
120
- codexs help config
121
- codexs help migrate
122
- ```
123
-
124
- ## How It Works
104
+ ## Runtime Model
125
105
 
126
- Starting in `0.0.11`, `codex-switch` uses a dual-path model.
106
+ `codex-switch` uses a dual-path model.
127
107
 
128
108
  Tool home:
129
109
 
@@ -144,32 +124,26 @@ Target Codex runtime:
144
124
  auth.json
145
125
  ```
146
126
 
147
- Notes:
148
-
149
- - `providers.json` is the managed provider registry and now lives under the tool home
150
- - `codex-switch.json` stores tool-level metadata such as `defaultCodexDir`
151
- - `config.toml` is still the active runtime-routing file in the target Codex directory
152
- - `auth.json` is the active auth projection file
153
- - direct-provider switches rewrite `OPENAI_API_KEY`
154
- - Copilot bridge providers keep upstream login in the official Copilot runtime while `codex-switch` manages the local bridge secret, bridge state, and routing
155
- - mutating commands back up before writing and rollback stays available after failed or undesired changes
127
+ Key points:
156
128
 
157
- Path overrides and resolution:
129
+ - `providers.json` is the managed provider registry and lives under the tool home.
130
+ - `codex-switch.json` stores tool-level metadata such as `defaultCodexDir`.
131
+ - `config.toml` remains the active runtime routing file in the target Codex directory.
132
+ - `auth.json` remains the active auth projection file in the target Codex directory.
133
+ - Direct providers rewrite `OPENAI_API_KEY` into the active runtime projection.
134
+ - Copilot providers keep upstream GitHub authentication in the official Copilot runtime while `codex-switch` manages local bridge state and routing.
158
135
 
159
- - `--codex-dir <path>` explicitly targets a Codex runtime directory
160
- - `CODEXS_CODEX_DIR` sets the default target when `--codex-dir` is not passed
161
- - `CODEXS_HOME` overrides the tool home location
136
+ Path controls:
162
137
 
163
- ## Automation
138
+ - `--codex-dir <path>` targets a specific Codex runtime directory.
139
+ - `CODEXS_CODEX_DIR` provides the default target runtime when `--codex-dir` is not passed.
140
+ - `CODEXS_HOME` overrides the tool home location.
164
141
 
165
- This CLI supports both human TTY use and non-interactive automation.
142
+ ## Automation Notes
166
143
 
167
- Current exceptions:
144
+ This CLI supports both human TTY usage and non-interactive automation.
168
145
 
169
- - `login copilot` requires a real TTY and does not support `--json`
170
- - `migrate` remains intentionally interactive for adopt profile selection and provider detail collection
171
-
172
- Recommended global flags:
146
+ Global flags:
173
147
 
174
148
  ```bash
175
149
  --json
@@ -178,29 +152,22 @@ Recommended global flags:
178
152
  --version
179
153
  ```
180
154
 
181
- Recommendations:
182
-
183
- - use `--json` for stable machine-readable output
184
- - pass all required arguments explicitly in scripts or CI
185
- - use `--codex-dir <path>` for sandbox or test environments
186
- - use `CODEXS_HOME` when you want tool state isolated from your default workstation setup
155
+ Operational limits:
187
156
 
188
- ## Testing
157
+ - `login copilot` requires a real TTY and does not support `--json`.
158
+ - `migrate` still depends on interactive profile selection and provider-detail collection in this release.
159
+ - Automation should pass explicit arguments and prefer `--json` for stable parsing.
189
160
 
190
- Build and test locally:
161
+ ## Local Development
191
162
 
192
163
  ```bash
193
164
  npm run build
194
165
  npm test
195
166
  npx tsc --noEmit
167
+ node dist/cli.js --help
168
+ npm pack --dry-run
196
169
  ```
197
170
 
198
- The repository includes a development fixture under `dev-codex/local-sandbox` plus dedicated test docs:
199
-
200
- - [Testing Guide](./docs/Tests/testing.md)
201
- - [Bridge Testing Notes](./docs/Tests/testing-bridge-v0.0.9.md)
202
- - [Test Report for 0.0.7](./docs/Tests/test-report-0.0.7.md)
203
-
204
171
  ## Documentation
205
172
 
206
173
  - [Chinese README](./README.CN.md)
@@ -208,9 +175,8 @@ The repository includes a development fixture under `dev-codex/local-sandbox` pl
208
175
  - [Detailed CLI Usage](./docs/cli-usage.md)
209
176
  - [Testing Guide](./docs/Tests/testing.md)
210
177
  - [Product Overview](./docs/codex-switch-product-overview.md)
211
- - [Technical Architecture](./docs/codex-switch-technical-architecture.md)
212
- - [PRD 0.0.11](./docs/PRD/codex-switch-prd-v0.0.11.md)
213
- - [Design Doc 0.0.11](./docs/Design/codex-switch-v0.0.11-design.md)
178
+ - [PRD 0.0.12](./docs/PRD/codex-switch-prd-v0.0.12.md)
179
+ - [Release Gate PRD 0.1.0](./docs/PRD/codex-switch-prd-v0.1.0.md)
214
180
 
215
181
  ## License
216
182
 
@@ -67,8 +67,10 @@ async function getStatus(codexDir, configPath, providersPath, authPath, options)
67
67
  }
68
68
  }
69
69
  const liveState = (0, runtime_state_1.inspectLiveStateDrift)(currentProfile, providers);
70
- const activeProviderCandidates = currentProfile && providers ? (0, providers_1.findProvidersByProfile)(providers, currentProfile) : [];
71
- const activeProvider = activeProviderCandidates.length === 1 && providers ? providers.providers[activeProviderCandidates[0]] : null;
70
+ const activeProviderCandidates = liveState.mappedProviders;
71
+ const activeProvider = liveState.providerResolvable && providers && liveState.mappedProvider
72
+ ? providers.providers[liveState.mappedProvider]
73
+ : null;
72
74
  const copilotInstall = (0, copilot_installer_1.probeCopilotSdkInstall)(options?.runtimesDir);
73
75
  const runtimeStateInspection = (0, runtime_state_repo_1.inspectCopilotBridgeState)(options?.runtimeDir);
74
76
  const runtimeState = runtimeStateInspection.state;
@@ -108,6 +110,9 @@ async function getStatus(codexDir, configPath, providersPath, authPath, options)
108
110
  // Surface unmanaged live state without mutating anything during a read-only status call.
109
111
  warnings.push("Current config profile is not mapped in providers.json. Backfill would be required before treating live state as managed.");
110
112
  }
113
+ if (liveState.reason === "shared-profile") {
114
+ warnings.push(`Current config profile "${currentProfile}" is shared by multiple providers in providers.json, so the active provider cannot be resolved uniquely.`);
115
+ }
111
116
  if (runtimeStateInspection.exists && !runtimeStateInspection.valid) {
112
117
  warnings.push(`Copilot bridge runtime state is unreadable: ${runtimeStateInspection.parseError ?? "unknown parse failure"}`);
113
118
  }
@@ -128,7 +133,7 @@ async function getStatus(codexDir, configPath, providersPath, authPath, options)
128
133
  currentProfile,
129
134
  currentProfileMapped: liveState.profileMapped,
130
135
  provider: liveState.mappedProvider,
131
- activeProviderResolvable: activeProviderCandidates.length === 1,
136
+ activeProviderResolvable: liveState.providerResolvable,
132
137
  activeProviderCandidates,
133
138
  runtimeProvider: activeProvider && (0, providers_1.isCopilotBridgeProvider)(activeProvider) ? activeProvider.runtime?.kind ?? null : null,
134
139
  copilotSdk: {
@@ -1,16 +1,59 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.listProviders = listProviders;
37
+ const fs = __importStar(require("node:fs"));
38
+ const providers_1 = require("../domain/providers");
39
+ const runtime_state_1 = require("../domain/runtime-state");
40
+ const config_repo_1 = require("../storage/config-repo");
4
41
  const providers_repo_1 = require("../storage/providers-repo");
5
42
  /**
6
43
  * Returns the sorted list of configured providers for display.
7
44
  */
8
- function listProviders(providersPath) {
45
+ function listProviders(providersPath, configPath) {
9
46
  const providers = (0, providers_repo_1.readProvidersFile)(providersPath);
10
47
  const names = Object.keys(providers.providers).sort();
48
+ const currentProfile = configPath && fs.existsSync(configPath)
49
+ ? (0, config_repo_1.readStructuredConfig)(configPath).activeProfile
50
+ : null;
51
+ const liveState = (0, runtime_state_1.inspectLiveStateDrift)(currentProfile, providers);
11
52
  const items = names.map((name) => ({
12
53
  name,
13
54
  profile: providers.providers[name].profile,
55
+ providerType: (0, providers_1.isCopilotBridgeProvider)(providers.providers[name]) ? "copilot" : "direct",
56
+ isActive: liveState.providerResolvable && liveState.mappedProvider === name,
14
57
  note: providers.providers[name].note ?? null,
15
58
  tags: providers.providers[name].tags ?? [],
16
59
  }));
@@ -18,6 +61,10 @@ function listProviders(providersPath) {
18
61
  data: {
19
62
  providers: items,
20
63
  count: items.length,
64
+ currentProfile,
65
+ activeProvider: liveState.mappedProvider,
66
+ activeProviderResolvable: liveState.providerResolvable,
67
+ activeProviderCandidates: liveState.mappedProviders,
21
68
  },
22
69
  };
23
70
  }
@@ -85,12 +85,22 @@ 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
+ }
88
97
  for (const provider of providers) {
89
98
  const tags = Array.isArray(provider.tags) && provider.tags.length > 0
90
99
  ? ` tags=${provider.tags.join(",")}`
91
100
  : "";
92
101
  const note = provider.note ? ` note=${provider.note}` : "";
93
- lines.push(`${provider.name} -> ${provider.profile}${tags}${note}`);
102
+ const current = provider.isActive ? " current" : "";
103
+ lines.push(`${provider.name} [${String(provider.providerType ?? "direct")}]${current} -> ${provider.profile}${tags}${note}`);
94
104
  }
95
105
  }
96
106
  break;
@@ -115,17 +125,15 @@ function renderHumanSuccess(command, data, warnings) {
115
125
  lines.push(`Current profile: ${String(data?.profile ?? "")}`);
116
126
  break;
117
127
  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}`);
128
+ lines.push("Status summary:");
129
+ lines.push(` target runtime: ${String(data?.codexDir ?? "")}`);
130
+ lines.push(` tool home: ${String(data?.storage?.toolHome?.root ?? "")}`);
131
+ lines.push(` current profile: ${String(data?.currentProfile ?? "(none)")}`);
132
+ lines.push(` mapped provider: ${renderStatusMappedProvider(data)}`);
133
+ lines.push(` provider path: ${renderStatusProviderPath(data)}`);
134
+ lines.push(` runtime health: ${renderStatusHealth(data)}`);
135
+ lines.push(` warnings: ${warnings.length}`);
136
+ lines.push(` next step: ${renderStatusNextStep(data, warnings)}`);
129
137
  break;
130
138
  case "config-show": {
131
139
  lines.push(`activeProfile: ${String(data?.activeProfile ?? "")}`);
@@ -153,10 +161,26 @@ function renderHumanSuccess(command, data, warnings) {
153
161
  lines.push(`Exported providers to ${String(data?.exportedTo ?? "")}.`);
154
162
  break;
155
163
  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)}`);
164
+ lines.push("Initialized codex-switch tool home.");
165
+ lines.push(`tool home: ${String(data?.toolHomeDir ?? "")}`);
166
+ lines.push(`tool config: ${String(data?.toolConfigPath ?? "")}`);
167
+ lines.push(`providers registry: ${String(data?.providersPath ?? "")}`);
168
+ lines.push(`tool home created: ${String(data?.createdToolHomeDir ?? false)}`);
169
+ lines.push(`tool config created: ${String(data?.createdToolConfigFile ?? false)}`);
170
+ lines.push(`providers registry created: ${String(data?.createdProvidersFile ?? false)}`);
171
+ lines.push("next step: run `codexs add ...` for a direct provider, or `codexs login copilot` before `add --copilot`.");
172
+ break;
173
+ case "login":
174
+ lines.push(`Copilot login ready: ${String(data?.authReady ?? false)}`);
175
+ lines.push(`upstream: ${String(data?.upstream ?? "")}`);
176
+ lines.push(`sdk installed: ${String(data?.sdkInstalled ?? false)}${data?.sdkInstalledNow ? " (installed now)" : ""}`);
177
+ lines.push(`copilot cli source: ${String(data?.cliSource ?? "not-needed")}`);
178
+ if (data?.cliCommand) {
179
+ lines.push(`copilot cli command: ${String(data?.cliCommand)}`);
180
+ }
181
+ lines.push(`login launched: ${String(data?.loginLaunched ?? false)}`);
182
+ lines.push(`auth ready: ${String(data?.authReady ?? false)}`);
183
+ lines.push("next step: run `codexs add <provider> --copilot --profile <name>` and then `codexs switch <provider>`.");
160
184
  break;
161
185
  case "migrate":
162
186
  lines.push(`Migrated providers in ${String(data?.codexDir ?? "")} using ${String(data?.strategy ?? "")}.`);
@@ -185,10 +209,12 @@ function renderHumanSuccess(command, data, warnings) {
185
209
  break;
186
210
  case "doctor": {
187
211
  const healthy = Boolean(data?.healthy);
188
- lines.push(healthy ? "No issues found." : "Issues found:");
189
212
  const issues = data?.issues ?? [];
213
+ lines.push(healthy ? "Doctor summary: healthy. No action required." : `Doctor summary: ${issues.length} issue(s) need attention.`);
214
+ lines.push(`target runtime: ${String(data?.codexDir ?? "")}`);
190
215
  for (const issue of issues) {
191
- lines.push(`${issue.code}: ${issue.message}`);
216
+ lines.push(`- ${String(issue.code)}: ${String(issue.message)}`);
217
+ lines.push(` next step: ${renderDoctorIssueNextStep(issue)}`);
192
218
  }
193
219
  break;
194
220
  }
@@ -212,6 +238,107 @@ function renderHumanSuccess(command, data, warnings) {
212
238
  }
213
239
  return lines;
214
240
  }
241
+ /**
242
+ * Summarizes runtime health for the human-readable status output.
243
+ */
244
+ function renderStatusHealth(data) {
245
+ const configExists = Boolean(data?.configExists);
246
+ const providersExists = Boolean(data?.providersExists);
247
+ const auth = data?.auth ?? {};
248
+ const bridge = data?.copilotBridge ?? null;
249
+ const issues = Array.isArray(data?.issues) ? data?.issues : [];
250
+ const activeProviderResolvable = data?.activeProviderResolvable !== false;
251
+ const liveState = data?.liveState ?? {};
252
+ const copilotSdk = data?.copilotSdk ?? {};
253
+ const copilotAuth = data?.copilotAuth ?? null;
254
+ const runtimeProvider = typeof data?.runtimeProvider === "string" ? data.runtimeProvider : null;
255
+ const activePathUsesCopilot = runtimeProvider === "copilot-sdk-bridge";
256
+ if (!configExists || !providersExists) {
257
+ return "incomplete local state";
258
+ }
259
+ if (!activeProviderResolvable || liveState.reason === "shared-profile") {
260
+ return "active provider ambiguous";
261
+ }
262
+ if (issues.some((issue) => issue.code === "UNMANAGED_ACTIVE_PROFILE")) {
263
+ return "active profile unmanaged";
264
+ }
265
+ if (issues.some((issue) => issue.code === "ACTIVE_PROVIDER_UNRESOLVED")) {
266
+ return "active provider ambiguous";
267
+ }
268
+ if (activePathUsesCopilot && copilotSdk.installed === false) {
269
+ return "copilot sdk missing";
270
+ }
271
+ if (activePathUsesCopilot && copilotAuth && copilotAuth.ready === false) {
272
+ return "copilot auth required";
273
+ }
274
+ if (activePathUsesCopilot && bridge && bridge.ok === false) {
275
+ return "copilot runtime needs repair";
276
+ }
277
+ if (auth.exists === false) {
278
+ return "auth projection missing";
279
+ }
280
+ if (auth.valid === false) {
281
+ return "auth projection invalid";
282
+ }
283
+ return "ok";
284
+ }
285
+ /**
286
+ * Renders the mapped provider line without claiming a unique winner for shared profiles.
287
+ */
288
+ function renderStatusMappedProvider(data) {
289
+ if (typeof data?.provider === "string" && data.provider.length > 0) {
290
+ return data.provider;
291
+ }
292
+ const candidates = Array.isArray(data?.activeProviderCandidates) ? data?.activeProviderCandidates : [];
293
+ if (candidates.length > 1) {
294
+ return `(ambiguous: ${candidates.join(", ")})`;
295
+ }
296
+ return "(unmanaged or unresolved)";
297
+ }
298
+ /**
299
+ * Renders the active workflow path in status output.
300
+ */
301
+ function renderStatusProviderPath(data) {
302
+ return typeof data?.runtimeProvider === "string" && data.runtimeProvider === "copilot-sdk-bridge" ? "copilot" : "direct";
303
+ }
304
+ /**
305
+ * Suggests the next operator action for the human-readable status output.
306
+ */
307
+ function renderStatusNextStep(data, warnings) {
308
+ if (warnings.length > 0) {
309
+ return "run `codexs doctor` to inspect warnings before the next write command";
310
+ }
311
+ if (!data?.provider) {
312
+ return "run `codexs switch <provider>` after adding or adopting a managed provider";
313
+ }
314
+ return "run `codexs doctor` if you need a deeper diagnostic pass";
315
+ }
316
+ /**
317
+ * Turns structured doctor issue codes into repair-oriented next steps.
318
+ */
319
+ function renderDoctorIssueNextStep(issue) {
320
+ switch (issue.code) {
321
+ case "CONFIG_NOT_FOUND":
322
+ return "restore or create config.toml before switching providers";
323
+ case "PROVIDERS_NOT_FOUND":
324
+ return "run `codexs init` and then add or migrate providers";
325
+ case "COPILOT_SDK_MISSING":
326
+ return "run `codexs login copilot` to install the optional Copilot runtime";
327
+ case "COPILOT_AUTH_REQUIRED":
328
+ return "run `codexs login copilot` to complete upstream authentication";
329
+ case "BRIDGE_STATE_STALE":
330
+ case "BRIDGE_STATE_MISSING":
331
+ case "BRIDGE_HEALTHCHECK_FAILED":
332
+ return "reselect the provider with `codexs switch <provider>` or inspect bridge state";
333
+ case "UNMANAGED_ACTIVE_PROFILE":
334
+ return "switch to a managed provider or adopt the active profile with `codexs migrate`";
335
+ case "ACTIVE_PROVIDER_UNRESOLVED":
336
+ case "SHARED_PROFILE_REFERENCE":
337
+ return "make provider-to-profile mappings unique before relying on current-provider detection";
338
+ default:
339
+ return "inspect the issue details and rerun `codexs doctor` after fixing the state";
340
+ }
341
+ }
215
342
  /**
216
343
  * Writes one rendered line to either stdout or stderr.
217
344
  */
@@ -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.");