@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.AI.md +56 -98
- package/README.CN.md +90 -126
- package/README.md +63 -97
- package/dist/app/get-status.js +8 -3
- package/dist/app/list-providers.js +48 -1
- package/dist/cli/output.js +145 -18
- package/dist/commands/handlers.js +10 -6
- package/dist/commands/help.js +9 -5
- package/dist/commands/registry.js +15 -12
- 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/PRD/codex-switch-prd-v0.0.12.md +279 -0
- package/docs/PRD/codex-switch-prd-v0.1.0.md +125 -237
- package/docs/Tests/testing.md +39 -112
- package/docs/cli-usage.md +138 -439
- package/docs/codex-switch-command-design.md +3 -0
- package/docs/codex-switch-product-overview.md +52 -207
- package/docs/codex-switch-technical-architecture.md +3 -0
- package/package.json +1 -1
- package/dist/app/rollback-latest.js +0 -26
package/README.md
CHANGED
|
@@ -1,89 +1,78 @@
|
|
|
1
|
-
|
|
1
|
+
# @minniexcode/codex-switch
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`@minniexcode/codex-switch` is a local-first CLI for managing and switching Codex provider and profile configuration safely.
|
|
4
4
|
|
|
5
|
-
It
|
|
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
|
-
|
|
7
|
+
Chinese version: [README.CN.md](./README.CN.md)
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Version
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Current package version: `0.0.12`
|
|
12
12
|
|
|
13
|
-
|
|
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
|
-
|
|
21
|
+
Run without a global install:
|
|
35
22
|
|
|
36
23
|
```bash
|
|
37
24
|
npx @minniexcode/codex-switch --help
|
|
38
25
|
```
|
|
39
26
|
|
|
40
|
-
CLI
|
|
27
|
+
Built CLI entrypoint:
|
|
41
28
|
|
|
42
29
|
```bash
|
|
43
30
|
codexs --help
|
|
44
31
|
```
|
|
45
32
|
|
|
46
|
-
##
|
|
33
|
+
## Primary Workflows
|
|
47
34
|
|
|
48
|
-
|
|
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
|
-
|
|
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
|
|
51
|
+
codexs switch copilot-main
|
|
52
|
+
codexs status
|
|
53
|
+
codexs doctor
|
|
76
54
|
```
|
|
77
55
|
|
|
78
|
-
|
|
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
|
|
82
|
-
codexs
|
|
83
|
-
codexs doctor
|
|
69
|
+
codexs init
|
|
70
|
+
codexs migrate
|
|
84
71
|
```
|
|
85
72
|
|
|
86
|
-
|
|
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
|
-
|
|
102
|
+
`setup` still exists only as a deprecated compatibility entry that points callers to `init` or `migrate`.
|
|
114
103
|
|
|
115
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
142
|
+
## Automation Notes
|
|
166
143
|
|
|
167
|
-
|
|
144
|
+
This CLI supports both human TTY usage and non-interactive automation.
|
|
168
145
|
|
|
169
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
- [
|
|
212
|
-
- [PRD 0.0
|
|
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
|
|
package/dist/app/get-status.js
CHANGED
|
@@ -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 =
|
|
71
|
-
const activeProvider =
|
|
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:
|
|
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
|
}
|
package/dist/cli/output.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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}`);
|
|
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(
|
|
157
|
-
lines.push(`
|
|
158
|
-
lines.push(`
|
|
159
|
-
lines.push(`
|
|
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(
|
|
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.");
|