@minniexcode/codex-switch 0.1.2 → 0.1.4
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 +3 -2
- package/README.CN.md +4 -2
- package/README.md +6 -2
- package/dist/app/bridge.js +11 -0
- package/dist/app/get-status.js +9 -1
- package/dist/app/run-doctor.js +2 -0
- package/dist/app/switch-provider.js +10 -3
- package/dist/cli/output.js +53 -1
- package/dist/interaction/interactive.js +9 -2
- package/dist/runtime/copilot-adapter.js +27 -7
- package/dist/runtime/copilot-bridge-worker.js +16 -0
- package/dist/runtime/copilot-bridge.js +284 -80
- package/dist/runtime/copilot-cli.js +15 -0
- package/dist/storage/runtime-state-repo.js +8 -0
- package/docs/Design/codex-switch-v0.1.3-design.md +10 -0
- package/docs/Design/codex-switch-v0.1.4-design.md +18 -0
- package/docs/PRD/codex-switch-prd-v0.1.3.md +22 -0
- package/docs/PRD/codex-switch-prd-v0.1.4.md +37 -0
- package/docs/Tests/testing.md +4 -1
- package/docs/cli-usage.md +7 -3
- package/docs/codex-switch-product-overview.md +8 -4
- package/docs/codex-switch-technical-architecture.md +6 -2
- package/package.json +1 -1
package/README.AI.md
CHANGED
|
@@ -6,8 +6,8 @@ This file summarizes the current operational contract for AI agents, automation
|
|
|
6
6
|
|
|
7
7
|
- Package: `@minniexcode/codex-switch`
|
|
8
8
|
- CLI name: `codexs`
|
|
9
|
-
- Current repository version: `0.1.
|
|
10
|
-
- Version status:
|
|
9
|
+
- Current repository version: `0.1.4`
|
|
10
|
+
- Version status: development line
|
|
11
11
|
- Runtime contract target: Codex `0.134.0+`
|
|
12
12
|
|
|
13
13
|
## Product Role
|
|
@@ -133,6 +133,7 @@ Important behavioral constraints:
|
|
|
133
133
|
- `migrate` remains interactive when provider adoption requires human input.
|
|
134
134
|
- `status` is the main dual-path summary command.
|
|
135
135
|
- `doctor` is the deeper repair-oriented diagnostic command.
|
|
136
|
+
- The current `0.1.4` line focuses on bridge stability, surfaced runtime log metadata, and stricter release-hygiene verification rather than command-surface expansion.
|
|
136
137
|
|
|
137
138
|
## Safety Notes
|
|
138
139
|
|
package/README.CN.md
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
## 版本定位
|
|
8
8
|
|
|
9
|
-
当前包版本:`0.1.
|
|
9
|
+
当前包版本:`0.1.4`
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
这是当前仓库开发线。`0.1.4` 聚焦于 bridge 稳定性与可观测性,包括 bridge 复用探测加固、运行态日志元数据外显,以及更严格的 release hygiene 门禁,同时不扩展 provider 命令面。
|
|
12
12
|
|
|
13
13
|
## 安装
|
|
14
14
|
|
|
@@ -209,6 +209,8 @@ npm pack --dry-run
|
|
|
209
209
|
- [Design 0.1.1](./docs/Design/codex-switch-v0.1.1-design.md)
|
|
210
210
|
- [PRD 0.1.2](./docs/PRD/codex-switch-prd-v0.1.2.md)
|
|
211
211
|
- [Design 0.1.2](./docs/Design/codex-switch-v0.1.2-design.md)
|
|
212
|
+
- [PRD 0.1.4](./docs/PRD/codex-switch-prd-v0.1.4.md)
|
|
213
|
+
- [Design 0.1.4](./docs/Design/codex-switch-v0.1.4-design.md)
|
|
212
214
|
|
|
213
215
|
## License
|
|
214
216
|
|
package/README.md
CHANGED
|
@@ -8,9 +8,9 @@ Chinese version: [README.CN.md](./README.CN.md)
|
|
|
8
8
|
|
|
9
9
|
## Version
|
|
10
10
|
|
|
11
|
-
Current package version: `0.1.
|
|
11
|
+
Current package version: `0.1.4`
|
|
12
12
|
|
|
13
|
-
This is the current
|
|
13
|
+
This is the current repository development line. `0.1.4` is the bridge stability and observability line, focused on bridge reuse hardening, surfaced runtime log metadata, and a stricter release-hygiene gate while keeping the provider surface unchanged.
|
|
14
14
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
@@ -217,7 +217,11 @@ npm pack --dry-run
|
|
|
217
217
|
- [PRD 0.1.0](./docs/PRD/codex-switch-prd-v0.1.0.md)
|
|
218
218
|
- [PRD 0.1.1](./docs/PRD/codex-switch-prd-v0.1.1.md)
|
|
219
219
|
- [PRD 0.1.2](./docs/PRD/codex-switch-prd-v0.1.2.md)
|
|
220
|
+
- [PRD 0.1.3](./docs/PRD/codex-switch-prd-v0.1.3.md)
|
|
221
|
+
- [PRD 0.1.4](./docs/PRD/codex-switch-prd-v0.1.4.md)
|
|
220
222
|
- [Design 0.1.2](./docs/Design/codex-switch-v0.1.2-design.md)
|
|
223
|
+
- [Design 0.1.3](./docs/Design/codex-switch-v0.1.3-design.md)
|
|
224
|
+
- [Design 0.1.4](./docs/Design/codex-switch-v0.1.4-design.md)
|
|
221
225
|
|
|
222
226
|
## License
|
|
223
227
|
|
package/dist/app/bridge.js
CHANGED
|
@@ -58,6 +58,9 @@ async function startBridge(args) {
|
|
|
58
58
|
port: bridge.port,
|
|
59
59
|
reused: bridge.reused,
|
|
60
60
|
portChanged: bridge.portChanged,
|
|
61
|
+
replaced: bridge.replaced,
|
|
62
|
+
restartReason: bridge.restartReason ?? null,
|
|
63
|
+
logPath: bridge.logPath,
|
|
61
64
|
defaultPort: DEFAULT_BRIDGE_PORT,
|
|
62
65
|
},
|
|
63
66
|
};
|
|
@@ -74,6 +77,8 @@ async function stopBridge(args) {
|
|
|
74
77
|
provider: null,
|
|
75
78
|
stopped: true,
|
|
76
79
|
hadRuntimeState: false,
|
|
80
|
+
logPath: null,
|
|
81
|
+
lastRestartReason: null,
|
|
77
82
|
},
|
|
78
83
|
};
|
|
79
84
|
}
|
|
@@ -84,6 +89,8 @@ async function stopBridge(args) {
|
|
|
84
89
|
provider: args.providerName,
|
|
85
90
|
stopped: true,
|
|
86
91
|
hadRuntimeState: false,
|
|
92
|
+
logPath: null,
|
|
93
|
+
lastRestartReason: null,
|
|
87
94
|
},
|
|
88
95
|
};
|
|
89
96
|
}
|
|
@@ -109,6 +116,8 @@ async function stopBridge(args) {
|
|
|
109
116
|
provider: target.providerName,
|
|
110
117
|
stopped: true,
|
|
111
118
|
hadRuntimeState: Boolean(state),
|
|
119
|
+
logPath: state?.logPath ?? null,
|
|
120
|
+
lastRestartReason: state?.lastRestartReason ?? null,
|
|
112
121
|
},
|
|
113
122
|
};
|
|
114
123
|
}
|
|
@@ -146,6 +155,8 @@ async function statusBridge(args) {
|
|
|
146
155
|
matches: Boolean(state && state.provider === target.providerName && state.baseUrl === expectedBaseUrl),
|
|
147
156
|
active: runtimeStatus.ok,
|
|
148
157
|
health: runtimeStatus,
|
|
158
|
+
logPath: state?.logPath ?? null,
|
|
159
|
+
lastRestartReason: state?.lastRestartReason ?? null,
|
|
149
160
|
},
|
|
150
161
|
};
|
|
151
162
|
}
|
package/dist/app/get-status.js
CHANGED
|
@@ -88,6 +88,9 @@ async function getStatus(codexDir, configPath, providersPath, authPath, options)
|
|
|
88
88
|
runtime: "copilot-bridge",
|
|
89
89
|
reason: "failed",
|
|
90
90
|
cause: runtimeStateInspection.parseError ?? "Failed to parse Copilot bridge runtime state.",
|
|
91
|
+
details: {
|
|
92
|
+
logPath: runtimeState?.logPath ?? null,
|
|
93
|
+
},
|
|
91
94
|
}
|
|
92
95
|
: bridgeProbeTarget
|
|
93
96
|
? await (0, copilot_bridge_1.probeCopilotBridgeRuntime)(bridgeProbeTarget, runtimeState, options?.runtimeDir)
|
|
@@ -97,7 +100,10 @@ async function getStatus(codexDir, configPath, providersPath, authPath, options)
|
|
|
97
100
|
runtime: "copilot-bridge",
|
|
98
101
|
reason: "failed",
|
|
99
102
|
cause: "Copilot bridge runtime state exists but no matching managed Copilot provider is active.",
|
|
100
|
-
details:
|
|
103
|
+
details: {
|
|
104
|
+
...runtimeState,
|
|
105
|
+
logPath: runtimeState.logPath ?? null,
|
|
106
|
+
},
|
|
101
107
|
}
|
|
102
108
|
: null;
|
|
103
109
|
const copilotAuth = activeProvider && (0, providers_1.isCopilotBridgeProvider)(activeProvider)
|
|
@@ -148,6 +154,8 @@ async function getStatus(codexDir, configPath, providersPath, authPath, options)
|
|
|
148
154
|
copilotAuth,
|
|
149
155
|
copilotBridge,
|
|
150
156
|
copilotRuntimeState: runtimeState,
|
|
157
|
+
copilotBridgeLogPath: runtimeState?.logPath ?? null,
|
|
158
|
+
copilotBridgeRestartReason: runtimeState?.lastRestartReason ?? null,
|
|
151
159
|
liveState,
|
|
152
160
|
auth: authState,
|
|
153
161
|
configProfiles: configViews,
|
package/dist/app/run-doctor.js
CHANGED
|
@@ -117,6 +117,7 @@ async function runDoctor(args) {
|
|
|
117
117
|
issues.push({
|
|
118
118
|
code: "BRIDGE_STATE_STALE",
|
|
119
119
|
message: `Copilot bridge runtime state is unreadable: ${runtimeStateInspection.parseError ?? "unknown parse failure"}`,
|
|
120
|
+
logPath: runtimeState?.logPath ?? null,
|
|
120
121
|
});
|
|
121
122
|
}
|
|
122
123
|
if (document?.currentModelProvider && providers) {
|
|
@@ -209,6 +210,7 @@ async function runDoctor(args) {
|
|
|
209
210
|
}),
|
|
210
211
|
liveState: drift,
|
|
211
212
|
auth: authState,
|
|
213
|
+
copilotRuntimeState: runtimeState,
|
|
212
214
|
},
|
|
213
215
|
warnings: issues.length === 0 ? [] : [`doctor found ${issues.length} issue(s)`],
|
|
214
216
|
};
|
|
@@ -22,7 +22,9 @@ async function switchProvider(args) {
|
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
24
|
const document = (0, config_repo_1.readStructuredConfig)(args.configPath);
|
|
25
|
-
const
|
|
25
|
+
const providerProfileSection = document.profiles.find((entry) => entry.name === provider.profile) ?? null;
|
|
26
|
+
const providerModelProviderSection = document.modelProviders.find((entry) => entry.name === provider.profile) ?? null;
|
|
27
|
+
const resolvedModel = provider.model ?? providerProfileSection?.model ?? document.currentModel;
|
|
26
28
|
if (!resolvedModel) {
|
|
27
29
|
throw (0, errors_1.cliError)("MANAGED_PROFILE_FIELDS_MISSING", `Provider "${args.providerName}" has no model to switch with.`, {
|
|
28
30
|
provider: args.providerName,
|
|
@@ -90,6 +92,10 @@ async function switchProvider(args) {
|
|
|
90
92
|
profile: nextProvider.profile,
|
|
91
93
|
portChanged: bridge.portChanged,
|
|
92
94
|
bridgePort: bridge.port,
|
|
95
|
+
bridgeReused: bridge.reused,
|
|
96
|
+
bridgeReplaced: bridge.replaced,
|
|
97
|
+
bridgeRestartReason: bridge.restartReason ?? null,
|
|
98
|
+
bridgeLogPath: bridge.logPath,
|
|
93
99
|
};
|
|
94
100
|
},
|
|
95
101
|
});
|
|
@@ -112,7 +118,8 @@ async function switchProvider(args) {
|
|
|
112
118
|
],
|
|
113
119
|
mutate: () => {
|
|
114
120
|
const directBaseUrl = provider.baseUrl?.trim() ?? "";
|
|
115
|
-
|
|
121
|
+
const resolvedBaseUrl = directBaseUrl || providerModelProviderSection?.baseUrl?.trim() || "";
|
|
122
|
+
if (!resolvedBaseUrl) {
|
|
116
123
|
throw (0, errors_1.cliError)("MANAGED_PROFILE_FIELDS_MISSING", `Provider "${args.providerName}" requires base_url before switching.`, {
|
|
117
124
|
provider: args.providerName,
|
|
118
125
|
modelProvider: provider.profile,
|
|
@@ -123,7 +130,7 @@ async function switchProvider(args) {
|
|
|
123
130
|
setCurrentModel: resolvedModel,
|
|
124
131
|
setCurrentModelProvider: provider.profile,
|
|
125
132
|
upsertModelProviders: {
|
|
126
|
-
[provider.profile]: (0, providers_1.buildDirectModelProviderProjection)(provider.profile,
|
|
133
|
+
[provider.profile]: (0, providers_1.buildDirectModelProviderProjection)(provider.profile, resolvedBaseUrl),
|
|
127
134
|
},
|
|
128
135
|
deleteLegacyProfile: true,
|
|
129
136
|
deleteLegacyProfilesByName: [provider.profile],
|
package/dist/cli/output.js
CHANGED
|
@@ -144,6 +144,12 @@ function renderHumanSuccess(command, data, warnings) {
|
|
|
144
144
|
lines.push(` mapped provider: ${renderStatusMappedProvider(data)}`);
|
|
145
145
|
lines.push(` provider path: ${renderStatusProviderPath(data)}`);
|
|
146
146
|
lines.push(` runtime health: ${renderStatusHealth(data)}`);
|
|
147
|
+
if (data?.copilotBridgeLogPath) {
|
|
148
|
+
lines.push(` bridge log: ${String(data.copilotBridgeLogPath)}`);
|
|
149
|
+
}
|
|
150
|
+
if (data?.copilotBridgeRestartReason) {
|
|
151
|
+
lines.push(` bridge restart reason: ${String(data.copilotBridgeRestartReason)}`);
|
|
152
|
+
}
|
|
147
153
|
lines.push(` warnings: ${warnings.length}`);
|
|
148
154
|
lines.push(` next step: ${renderStatusNextStep(data, warnings)}`);
|
|
149
155
|
break;
|
|
@@ -167,6 +173,15 @@ function renderHumanSuccess(command, data, warnings) {
|
|
|
167
173
|
case "switch":
|
|
168
174
|
lines.push(`Switched to provider ${String(data?.provider ?? "")} using model provider ${String(data?.modelProvider ?? data?.profile ?? "")}.`);
|
|
169
175
|
lines.push(`Model: ${String(data?.model ?? "")}`);
|
|
176
|
+
if (data?.bridgeReplaced) {
|
|
177
|
+
lines.push(`Bridge replaced: true`);
|
|
178
|
+
}
|
|
179
|
+
if (data?.bridgeRestartReason) {
|
|
180
|
+
lines.push(`Bridge restart reason: ${String(data?.bridgeRestartReason)}`);
|
|
181
|
+
}
|
|
182
|
+
if (data?.bridgeLogPath) {
|
|
183
|
+
lines.push(`Bridge log: ${String(data?.bridgeLogPath)}`);
|
|
184
|
+
}
|
|
170
185
|
lines.push(`Backup: ${String(data?.backupPath ?? "")}`);
|
|
171
186
|
break;
|
|
172
187
|
case "import":
|
|
@@ -227,12 +242,49 @@ function renderHumanSuccess(command, data, warnings) {
|
|
|
227
242
|
const issues = data?.issues ?? [];
|
|
228
243
|
lines.push(healthy ? "Doctor summary: healthy. No action required." : `Doctor summary: ${issues.length} issue(s) need attention.`);
|
|
229
244
|
lines.push(`target runtime: ${String(data?.codexDir ?? "")}`);
|
|
245
|
+
if (data?.copilotRuntimeState && data.copilotRuntimeState.logPath) {
|
|
246
|
+
lines.push(`bridge log: ${String(data.copilotRuntimeState.logPath)}`);
|
|
247
|
+
}
|
|
230
248
|
for (const issue of issues) {
|
|
231
249
|
lines.push(`- ${String(issue.code)}: ${String(issue.message)}`);
|
|
232
250
|
lines.push(` next step: ${renderDoctorIssueNextStep(issue)}`);
|
|
233
251
|
}
|
|
234
252
|
break;
|
|
235
253
|
}
|
|
254
|
+
case "bridge-start":
|
|
255
|
+
lines.push(`Bridge ready for provider ${String(data?.provider ?? "")}.`);
|
|
256
|
+
lines.push(`Endpoint: ${String(data?.baseUrl ?? "")}`);
|
|
257
|
+
lines.push(`Reused: ${String(data?.reused ?? false)}`);
|
|
258
|
+
lines.push(`Replaced existing: ${String(data?.replaced ?? false)}`);
|
|
259
|
+
if (data?.restartReason) {
|
|
260
|
+
lines.push(`Restart reason: ${String(data?.restartReason)}`);
|
|
261
|
+
}
|
|
262
|
+
if (data?.logPath) {
|
|
263
|
+
lines.push(`Bridge log: ${String(data?.logPath)}`);
|
|
264
|
+
}
|
|
265
|
+
break;
|
|
266
|
+
case "bridge-status":
|
|
267
|
+
lines.push(`Bridge provider: ${String(data?.provider ?? "")}`);
|
|
268
|
+
lines.push(`Active: ${String(data?.active ?? false)}`);
|
|
269
|
+
lines.push(`Expected base URL: ${String(data?.expectedBaseUrl ?? "")}`);
|
|
270
|
+
lines.push(`Matches runtime state: ${String(data?.matches ?? false)}`);
|
|
271
|
+
if (data?.lastRestartReason) {
|
|
272
|
+
lines.push(`Last restart reason: ${String(data?.lastRestartReason)}`);
|
|
273
|
+
}
|
|
274
|
+
if (data?.logPath) {
|
|
275
|
+
lines.push(`Bridge log: ${String(data?.logPath)}`);
|
|
276
|
+
}
|
|
277
|
+
break;
|
|
278
|
+
case "bridge-stop":
|
|
279
|
+
lines.push(`Bridge stopped for provider ${String(data?.provider ?? "(none)")}.`);
|
|
280
|
+
lines.push(`Had runtime state: ${String(data?.hadRuntimeState ?? false)}`);
|
|
281
|
+
if (data?.lastRestartReason) {
|
|
282
|
+
lines.push(`Last restart reason: ${String(data?.lastRestartReason)}`);
|
|
283
|
+
}
|
|
284
|
+
if (data?.logPath) {
|
|
285
|
+
lines.push(`Bridge log: ${String(data?.logPath)}`);
|
|
286
|
+
}
|
|
287
|
+
break;
|
|
236
288
|
case "backups-list": {
|
|
237
289
|
const backups = data?.backups ?? [];
|
|
238
290
|
for (const backup of backups) {
|
|
@@ -344,7 +396,7 @@ function renderDoctorIssueNextStep(issue) {
|
|
|
344
396
|
case "BRIDGE_STATE_STALE":
|
|
345
397
|
case "BRIDGE_STATE_MISSING":
|
|
346
398
|
case "BRIDGE_HEALTHCHECK_FAILED":
|
|
347
|
-
return "reselect the provider with `codexs switch <provider>` or inspect bridge state";
|
|
399
|
+
return "reselect the provider with `codexs switch <provider>` or inspect the bridge log/state";
|
|
348
400
|
case "UNMANAGED_ACTIVE_PROFILE":
|
|
349
401
|
return "switch to a managed provider or adopt the active route with `codexs migrate`";
|
|
350
402
|
case "LEGACY_PROFILE_SELECTOR":
|
|
@@ -70,18 +70,25 @@ function canPrompt(runtime, jsonMode) {
|
|
|
70
70
|
*/
|
|
71
71
|
async function promptForProviderSelection(runtime, providersPath, configPath, message) {
|
|
72
72
|
const providers = (0, providers_repo_1.readProvidersFile)(providersPath);
|
|
73
|
-
const
|
|
73
|
+
const document = fs.existsSync(configPath) ? (0, config_repo_1.readStructuredConfig)(configPath) : null;
|
|
74
|
+
const currentModelProvider = document?.currentModelProvider ?? null;
|
|
74
75
|
const liveState = (0, runtime_state_1.inspectLiveStateDrift)(currentModelProvider, providers);
|
|
76
|
+
const legacyCurrentProvider = !liveState.providerResolvable &&
|
|
77
|
+
document?.legacyProfile &&
|
|
78
|
+
providers.providers[document.legacyProfile]
|
|
79
|
+
? document.legacyProfile
|
|
80
|
+
: null;
|
|
75
81
|
const choices = Object.entries(providers.providers)
|
|
76
82
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
77
83
|
.map(([providerName, provider]) => {
|
|
78
84
|
const providerType = (0, providers_1.isCopilotBridgeProvider)(provider) ? "copilot" : "direct";
|
|
79
85
|
const currentMarker = liveState.providerResolvable && liveState.mappedProvider === providerName ? " | current" : "";
|
|
86
|
+
const legacyMarker = !currentMarker && legacyCurrentProvider === providerName ? " | current" : "";
|
|
80
87
|
const ambiguousMarker = !liveState.providerResolvable && liveState.mappedProviders.includes(providerName) ? " | current=ambiguous" : "";
|
|
81
88
|
return {
|
|
82
89
|
value: providerName,
|
|
83
90
|
label: providerName,
|
|
84
|
-
hint: `profile=${provider.profile} | type=${providerType}${currentMarker}${ambiguousMarker}`,
|
|
91
|
+
hint: `profile=${provider.profile} | type=${providerType}${currentMarker}${legacyMarker}${ambiguousMarker}`,
|
|
85
92
|
};
|
|
86
93
|
});
|
|
87
94
|
if (choices.length === 0) {
|
|
@@ -265,14 +265,23 @@ function createCopilotClient(sdk, runtimesDir) {
|
|
|
265
265
|
if (!ClientCtor) {
|
|
266
266
|
throw (0, errors_1.cliError)("COPILOT_SDK_API_UNSUPPORTED", "The installed Copilot SDK does not expose CopilotClient.", {});
|
|
267
267
|
}
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
268
|
+
const runtimeConnection = resolveRuntimeConnectionFactory(sdk);
|
|
269
|
+
if (!runtimeConnection?.forStdio) {
|
|
270
|
+
throw (0, errors_1.cliError)("COPILOT_SDK_API_UNSUPPORTED", "The installed Copilot SDK does not expose RuntimeConnection.forStdio().", {});
|
|
271
|
+
}
|
|
272
|
+
const runtimeInvocation = (0, copilot_cli_1.resolveCopilotSdkRuntimeInvocation)(runtimesDir);
|
|
273
|
+
if (!runtimeInvocation) {
|
|
274
|
+
throw (0, errors_1.cliError)("COPILOT_SDK_API_UNSUPPORTED", "The installed Copilot runtime is missing the @github/copilot npm loader required by the SDK.", {
|
|
275
|
+
expectedRuntimeFile: "node_modules/@github/copilot/npm-loader.js",
|
|
276
|
+
});
|
|
277
|
+
}
|
|
274
278
|
try {
|
|
275
|
-
return new ClientCtor(
|
|
279
|
+
return new ClientCtor({
|
|
280
|
+
connection: runtimeConnection.forStdio({
|
|
281
|
+
path: runtimeInvocation.path,
|
|
282
|
+
args: runtimeInvocation.args,
|
|
283
|
+
}),
|
|
284
|
+
});
|
|
276
285
|
}
|
|
277
286
|
catch (error) {
|
|
278
287
|
throw (0, errors_1.cliError)("COPILOT_SDK_API_UNSUPPORTED", "The installed Copilot SDK CopilotClient could not be constructed.", {
|
|
@@ -434,6 +443,17 @@ function resolveConstructor(target, name) {
|
|
|
434
443
|
}
|
|
435
444
|
return null;
|
|
436
445
|
}
|
|
446
|
+
function resolveRuntimeConnectionFactory(target) {
|
|
447
|
+
const direct = target.RuntimeConnection;
|
|
448
|
+
if (direct && typeof direct === "object") {
|
|
449
|
+
return direct;
|
|
450
|
+
}
|
|
451
|
+
const nestedDefault = target.default;
|
|
452
|
+
if (nestedDefault) {
|
|
453
|
+
return resolveRuntimeConnectionFactory(nestedDefault);
|
|
454
|
+
}
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
437
457
|
function resolveApproveAll(target) {
|
|
438
458
|
const direct = target.approveAll;
|
|
439
459
|
if (typeof direct === "function") {
|
|
@@ -3,6 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
const copilot_bridge_1 = require("./copilot-bridge");
|
|
4
4
|
const copilot_adapter_1 = require("./copilot-adapter");
|
|
5
5
|
let requestQueue = Promise.resolve();
|
|
6
|
+
/**
|
|
7
|
+
* Writes one worker lifecycle entry to stderr so the detached parent log capture persists it.
|
|
8
|
+
*/
|
|
9
|
+
function logWorkerEvent(message) {
|
|
10
|
+
process.stderr.write(`[${new Date().toISOString()}] ${message}\n`);
|
|
11
|
+
}
|
|
6
12
|
function enqueueRequest(task) {
|
|
7
13
|
const run = requestQueue.then(task, task);
|
|
8
14
|
requestQueue = run.catch(() => undefined);
|
|
@@ -14,9 +20,11 @@ async function main() {
|
|
|
14
20
|
const port = Number(process.env.CODEX_SWITCH_BRIDGE_PORT ?? "41415");
|
|
15
21
|
const apiKey = process.env.CODEX_SWITCH_BRIDGE_API_KEY ?? "";
|
|
16
22
|
const runtimesDir = process.env.CODEX_SWITCH_RUNTIMES_DIR || undefined;
|
|
23
|
+
logWorkerEvent(`worker startup provider=${provider} host=${host} port=${String(port)}`);
|
|
17
24
|
const runtimeClient = await (0, copilot_adapter_1.createCopilotRuntimeClient)(runtimesDir);
|
|
18
25
|
await (0, copilot_adapter_1.startCopilotRuntimeClient)(runtimeClient);
|
|
19
26
|
const stopRuntime = () => {
|
|
27
|
+
logWorkerEvent(`worker shutdown provider=${provider}`);
|
|
20
28
|
void (0, copilot_adapter_1.stopCopilotRuntimeClient)(runtimeClient).finally(() => process.exit(0));
|
|
21
29
|
};
|
|
22
30
|
process.once("SIGINT", stopRuntime);
|
|
@@ -41,9 +49,17 @@ async function main() {
|
|
|
41
49
|
},
|
|
42
50
|
})),
|
|
43
51
|
});
|
|
52
|
+
logWorkerEvent(`worker ready provider=${provider} host=${host} port=${String(port)}`);
|
|
44
53
|
}
|
|
45
54
|
if (require.main === module) {
|
|
55
|
+
process.on("uncaughtException", (error) => {
|
|
56
|
+
logWorkerEvent(`worker uncaught exception: ${error.message}`);
|
|
57
|
+
});
|
|
58
|
+
process.on("unhandledRejection", (reason) => {
|
|
59
|
+
logWorkerEvent(`worker unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
|
|
60
|
+
});
|
|
46
61
|
void main().catch((error) => {
|
|
62
|
+
logWorkerEvent(`worker startup failure: ${error instanceof Error ? error.message : String(error)}`);
|
|
47
63
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
48
64
|
process.exit(1);
|
|
49
65
|
});
|
|
@@ -50,6 +50,9 @@ const path = __importStar(require("node:path"));
|
|
|
50
50
|
const providers_1 = require("../domain/providers");
|
|
51
51
|
const errors_1 = require("../domain/errors");
|
|
52
52
|
const runtime_state_repo_1 = require("../storage/runtime-state-repo");
|
|
53
|
+
const BRIDGE_REUSE_ATTEMPTS = 2;
|
|
54
|
+
const BRIDGE_REUSE_TIMEOUT_MS = 2500;
|
|
55
|
+
const BRIDGE_REUSE_DELAY_MS = 250;
|
|
53
56
|
let spawnImplementation = node_child_process_1.spawn;
|
|
54
57
|
let cachedBridgeWorkerBuildId = null;
|
|
55
58
|
/**
|
|
@@ -69,13 +72,17 @@ function resetCopilotBridgeSpawnImplementation() {
|
|
|
69
72
|
*/
|
|
70
73
|
async function probeCopilotBridgeRuntime(provider, persistedState, runtimeDir) {
|
|
71
74
|
const state = persistedState === undefined ? (0, runtime_state_repo_1.readCopilotBridgeState)(runtimeDir) : persistedState;
|
|
75
|
+
const logPath = state?.logPath ?? (0, runtime_state_repo_1.getCopilotBridgeLogPath)(runtimeDir);
|
|
72
76
|
if (state && (!provider || !(0, providers_1.isCopilotBridgeProvider)(provider))) {
|
|
73
77
|
return {
|
|
74
78
|
ok: false,
|
|
75
79
|
runtime: "copilot-bridge",
|
|
76
80
|
reason: "failed",
|
|
77
81
|
cause: "Copilot bridge runtime state exists but no active Copilot bridge provider is selected.",
|
|
78
|
-
details:
|
|
82
|
+
details: {
|
|
83
|
+
...state,
|
|
84
|
+
logPath,
|
|
85
|
+
},
|
|
79
86
|
};
|
|
80
87
|
}
|
|
81
88
|
if (!provider || !(0, providers_1.isCopilotBridgeProvider)(provider)) {
|
|
@@ -100,6 +107,7 @@ async function probeCopilotBridgeRuntime(provider, persistedState, runtimeDir) {
|
|
|
100
107
|
cause: "Copilot bridge state manifest is missing.",
|
|
101
108
|
details: {
|
|
102
109
|
expectedBaseUrl: (0, providers_1.buildCopilotBridgeBaseUrl)(runtime),
|
|
110
|
+
logPath,
|
|
103
111
|
},
|
|
104
112
|
};
|
|
105
113
|
}
|
|
@@ -112,27 +120,44 @@ async function probeCopilotBridgeRuntime(provider, persistedState, runtimeDir) {
|
|
|
112
120
|
details: {
|
|
113
121
|
stateBaseUrl: state.baseUrl,
|
|
114
122
|
providerBaseUrl: (0, providers_1.buildCopilotBridgeBaseUrl)(runtime),
|
|
123
|
+
logPath,
|
|
115
124
|
},
|
|
116
125
|
};
|
|
117
126
|
}
|
|
118
|
-
const healthy = await
|
|
127
|
+
const healthy = await probeBridgeEndpoint({
|
|
128
|
+
host: state.host,
|
|
129
|
+
port: state.port,
|
|
130
|
+
stage: "health",
|
|
131
|
+
});
|
|
119
132
|
if (!healthy.ok) {
|
|
120
133
|
return {
|
|
121
134
|
ok: false,
|
|
122
135
|
runtime: "copilot-bridge",
|
|
123
136
|
reason: "failed",
|
|
124
|
-
cause: healthy.
|
|
125
|
-
details:
|
|
137
|
+
cause: healthy.message,
|
|
138
|
+
details: {
|
|
139
|
+
...state,
|
|
140
|
+
logPath,
|
|
141
|
+
probeStage: healthy.stage,
|
|
142
|
+
probeCause: healthy.cause,
|
|
143
|
+
retryable: healthy.retryable,
|
|
144
|
+
},
|
|
126
145
|
};
|
|
127
146
|
}
|
|
128
147
|
(0, runtime_state_repo_1.writeCopilotBridgeState)({
|
|
129
148
|
...state,
|
|
130
149
|
lastHealthcheckAt: new Date().toISOString(),
|
|
150
|
+
lastProbeAt: new Date().toISOString(),
|
|
151
|
+
logPath,
|
|
131
152
|
}, runtimeDir);
|
|
132
153
|
return {
|
|
133
154
|
ok: true,
|
|
134
155
|
runtime: "copilot-bridge",
|
|
135
|
-
details:
|
|
156
|
+
details: {
|
|
157
|
+
...state,
|
|
158
|
+
logPath,
|
|
159
|
+
lastProbeAt: new Date().toISOString(),
|
|
160
|
+
},
|
|
136
161
|
};
|
|
137
162
|
}
|
|
138
163
|
/**
|
|
@@ -160,47 +185,50 @@ async function startOrReuseCopilotBridge(providerName, provider, runtimeDir, run
|
|
|
160
185
|
const current = (0, runtime_state_repo_1.readCopilotBridgeState)(runtimeDir);
|
|
161
186
|
const workerBuildId = getCopilotBridgeWorkerBuildId();
|
|
162
187
|
let replaced = false;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
188
|
+
const logPath = current?.logPath ?? (0, runtime_state_repo_1.getCopilotBridgeLogPath)(runtimeDir);
|
|
189
|
+
const reuseDecision = await evaluateBridgeReuse({
|
|
190
|
+
current,
|
|
191
|
+
providerName,
|
|
192
|
+
expectedBaseUrl,
|
|
193
|
+
expectedApiKey: provider.apiKey,
|
|
194
|
+
workerBuildId,
|
|
195
|
+
runtimeDir,
|
|
196
|
+
logPath,
|
|
197
|
+
});
|
|
198
|
+
if (reuseDecision.reuse) {
|
|
199
|
+
(0, runtime_state_repo_1.writeCopilotBridgeState)({
|
|
200
|
+
...current,
|
|
201
|
+
lastHealthcheckAt: new Date().toISOString(),
|
|
202
|
+
lastProbeAt: reuseDecision.probeAt,
|
|
203
|
+
workerBuildId,
|
|
204
|
+
logPath: reuseDecision.logPath,
|
|
205
|
+
}, runtimeDir);
|
|
206
|
+
appendBridgeLifecycleLog(logPath, `startup success reused provider=${providerName} host=${current.host} port=${String(current.port)}`);
|
|
207
|
+
return {
|
|
208
|
+
baseUrl: expectedBaseUrl,
|
|
209
|
+
host: current.host,
|
|
210
|
+
port: current.port,
|
|
211
|
+
reused: true,
|
|
212
|
+
portChanged: false,
|
|
213
|
+
replaced: false,
|
|
214
|
+
logPath: reuseDecision.logPath,
|
|
215
|
+
};
|
|
191
216
|
}
|
|
192
|
-
if (
|
|
217
|
+
if (reuseDecision.replacedExisting) {
|
|
193
218
|
stopCopilotBridge(runtimeDir);
|
|
194
219
|
replaced = true;
|
|
220
|
+
appendBridgeLifecycleLog(logPath, `replacement reason=${reuseDecision.reason}`);
|
|
195
221
|
}
|
|
196
222
|
const selectedPort = await selectBridgePort(runtime.bridgeHost, runtime.bridgePort);
|
|
197
223
|
const selectedBaseUrl = `http://${runtime.bridgeHost}:${selectedPort}${runtime.bridgePath}`;
|
|
198
224
|
const workerPath = path.join(__dirname, "copilot-bridge-worker.js");
|
|
225
|
+
ensureBridgeLogFile(logPath);
|
|
226
|
+
appendBridgeLifecycleLog(logPath, `worker start provider=${providerName} host=${runtime.bridgeHost} port=${String(selectedPort)} replaced=${String(replaced)}`);
|
|
199
227
|
let child;
|
|
200
228
|
try {
|
|
201
229
|
child = spawnImplementation(process.execPath, [workerPath], {
|
|
202
230
|
detached: true,
|
|
203
|
-
stdio: "ignore",
|
|
231
|
+
stdio: ["ignore", openBridgeLogFd(logPath), openBridgeLogFd(logPath)],
|
|
204
232
|
env: {
|
|
205
233
|
...process.env,
|
|
206
234
|
CODEX_SWITCH_BRIDGE_PROVIDER: providerName,
|
|
@@ -210,6 +238,7 @@ async function startOrReuseCopilotBridge(providerName, provider, runtimeDir, run
|
|
|
210
238
|
CODEX_SWITCH_BRIDGE_BASE_URL: selectedBaseUrl,
|
|
211
239
|
CODEX_SWITCH_RUNTIME_DIR: runtimeDir ?? "",
|
|
212
240
|
CODEX_SWITCH_RUNTIMES_DIR: runtimesDir ?? "",
|
|
241
|
+
CODEX_SWITCH_BRIDGE_LOG_PATH: logPath,
|
|
213
242
|
},
|
|
214
243
|
});
|
|
215
244
|
}
|
|
@@ -218,6 +247,12 @@ async function startOrReuseCopilotBridge(providerName, provider, runtimeDir, run
|
|
|
218
247
|
provider: providerName,
|
|
219
248
|
host: runtime.bridgeHost,
|
|
220
249
|
port: selectedPort,
|
|
250
|
+
logPath,
|
|
251
|
+
probeStage: "startup",
|
|
252
|
+
probeCause: "startup-failed",
|
|
253
|
+
retryable: false,
|
|
254
|
+
replacedExisting: replaced,
|
|
255
|
+
providerName,
|
|
221
256
|
cause: error instanceof Error ? error.message : String(error),
|
|
222
257
|
});
|
|
223
258
|
}
|
|
@@ -228,17 +263,29 @@ async function startOrReuseCopilotBridge(providerName, provider, runtimeDir, run
|
|
|
228
263
|
if (!healthy.ok) {
|
|
229
264
|
(0, runtime_state_repo_1.clearCopilotBridgeState)(runtimeDir);
|
|
230
265
|
if (healthy.reason === "start-failed") {
|
|
266
|
+
appendBridgeLifecycleLog(logPath, `startup failure provider=${providerName} cause=${healthy.cause}`);
|
|
231
267
|
throw (0, errors_1.cliError)("BRIDGE_START_FAILED", "Copilot bridge worker exited before becoming healthy.", {
|
|
232
268
|
provider: providerName,
|
|
233
269
|
host: runtime.bridgeHost,
|
|
234
270
|
port: selectedPort,
|
|
271
|
+
logPath,
|
|
272
|
+
probeStage: "startup",
|
|
273
|
+
probeCause: "startup-failed",
|
|
274
|
+
retryable: false,
|
|
275
|
+
replacedExisting: replaced,
|
|
235
276
|
cause: healthy.cause,
|
|
236
277
|
});
|
|
237
278
|
}
|
|
279
|
+
appendBridgeLifecycleLog(logPath, `startup timeout provider=${providerName} host=${runtime.bridgeHost} port=${String(selectedPort)}`);
|
|
238
280
|
throw (0, errors_1.cliError)("BRIDGE_HEALTHCHECK_FAILED", "Copilot bridge did not become healthy after startup.", {
|
|
239
281
|
provider: providerName,
|
|
240
282
|
host: runtime.bridgeHost,
|
|
241
283
|
port: selectedPort,
|
|
284
|
+
logPath,
|
|
285
|
+
probeStage: "startup",
|
|
286
|
+
probeCause: "startup-timeout",
|
|
287
|
+
retryable: true,
|
|
288
|
+
replacedExisting: replaced,
|
|
242
289
|
cause: healthy.cause,
|
|
243
290
|
});
|
|
244
291
|
}
|
|
@@ -251,8 +298,12 @@ async function startOrReuseCopilotBridge(providerName, provider, runtimeDir, run
|
|
|
251
298
|
startedAt,
|
|
252
299
|
lastHealthcheckAt: new Date().toISOString(),
|
|
253
300
|
workerBuildId,
|
|
301
|
+
logPath,
|
|
302
|
+
lastProbeAt: new Date().toISOString(),
|
|
303
|
+
lastRestartReason: reuseDecision.reuse ? undefined : reuseDecision.reason,
|
|
254
304
|
};
|
|
255
305
|
(0, runtime_state_repo_1.writeCopilotBridgeState)(state, runtimeDir);
|
|
306
|
+
appendBridgeLifecycleLog(logPath, `startup success provider=${providerName} host=${runtime.bridgeHost} port=${String(selectedPort)}`);
|
|
256
307
|
return {
|
|
257
308
|
baseUrl: selectedBaseUrl,
|
|
258
309
|
host: runtime.bridgeHost,
|
|
@@ -260,6 +311,8 @@ async function startOrReuseCopilotBridge(providerName, provider, runtimeDir, run
|
|
|
260
311
|
reused: false,
|
|
261
312
|
portChanged: selectedPort !== runtime.bridgePort,
|
|
262
313
|
replaced,
|
|
314
|
+
logPath,
|
|
315
|
+
restartReason: replaced ? reuseDecision.reason : undefined,
|
|
263
316
|
};
|
|
264
317
|
}
|
|
265
318
|
/**
|
|
@@ -353,6 +406,9 @@ function createCopilotBridgeRequestHandler(context) {
|
|
|
353
406
|
});
|
|
354
407
|
clearInterval(heartbeat);
|
|
355
408
|
const outputText = text || getChatCompletionText(payload);
|
|
409
|
+
if (text.length === 0 && outputText.length > 0) {
|
|
410
|
+
writeResponsesTextDelta(response, messageId, outputText);
|
|
411
|
+
}
|
|
356
412
|
writeResponsesStreamDone(response, responseId, normalized.model, messageId, outputText);
|
|
357
413
|
response.end();
|
|
358
414
|
return;
|
|
@@ -809,9 +865,9 @@ function startCopilotBridgeServer(args) {
|
|
|
809
865
|
*/
|
|
810
866
|
async function waitForCopilotBridgeHealth(host, port, attempts = 10, delayMs = 150) {
|
|
811
867
|
for (let index = 0; index < attempts; index += 1) {
|
|
812
|
-
const result = await
|
|
868
|
+
const result = await probeBridgeEndpoint({ host, port, stage: "health" });
|
|
813
869
|
if (result.ok) {
|
|
814
|
-
return
|
|
870
|
+
return { ok: true };
|
|
815
871
|
}
|
|
816
872
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
817
873
|
}
|
|
@@ -896,9 +952,9 @@ async function waitForCopilotBridgeStartup(child, host, port, attempts, delayMs)
|
|
|
896
952
|
cause: startupFailure,
|
|
897
953
|
};
|
|
898
954
|
}
|
|
899
|
-
const result = await
|
|
955
|
+
const result = await probeBridgeEndpoint({ host, port, stage: "health" });
|
|
900
956
|
if (result.ok) {
|
|
901
|
-
return
|
|
957
|
+
return { ok: true };
|
|
902
958
|
}
|
|
903
959
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
904
960
|
}
|
|
@@ -920,74 +976,222 @@ async function waitForCopilotBridgeStartup(child, host, port, attempts, delayMs)
|
|
|
920
976
|
child.off("exit", onExit);
|
|
921
977
|
}
|
|
922
978
|
}
|
|
923
|
-
async function
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
979
|
+
async function evaluateBridgeReuse(args) {
|
|
980
|
+
const probeAt = new Date().toISOString();
|
|
981
|
+
if (!args.current) {
|
|
982
|
+
return {
|
|
983
|
+
reuse: false,
|
|
984
|
+
reason: "no persisted bridge state",
|
|
985
|
+
replacedExisting: false,
|
|
986
|
+
probeAt,
|
|
987
|
+
logPath: args.logPath,
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
if (args.current.provider !== args.providerName) {
|
|
991
|
+
return {
|
|
992
|
+
reuse: false,
|
|
993
|
+
reason: `provider mismatch: ${args.current.provider} -> ${args.providerName}`,
|
|
994
|
+
replacedExisting: true,
|
|
995
|
+
probeAt,
|
|
996
|
+
logPath: args.logPath,
|
|
997
|
+
probe: {
|
|
938
998
|
ok: false,
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
999
|
+
stage: "health",
|
|
1000
|
+
attempts: 0,
|
|
1001
|
+
cause: "provider-mismatch",
|
|
1002
|
+
retryable: false,
|
|
1003
|
+
message: "Persisted bridge provider does not match the requested provider.",
|
|
1004
|
+
},
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
if (args.current.baseUrl !== args.expectedBaseUrl) {
|
|
1008
|
+
return {
|
|
1009
|
+
reuse: false,
|
|
1010
|
+
reason: `base URL mismatch: ${args.current.baseUrl} -> ${args.expectedBaseUrl}`,
|
|
1011
|
+
replacedExisting: true,
|
|
1012
|
+
probeAt,
|
|
1013
|
+
logPath: args.logPath,
|
|
1014
|
+
probe: {
|
|
944
1015
|
ok: false,
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1016
|
+
stage: "health",
|
|
1017
|
+
attempts: 0,
|
|
1018
|
+
cause: "base-url-mismatch",
|
|
1019
|
+
retryable: false,
|
|
1020
|
+
message: "Persisted bridge base URL does not match the requested provider runtime base URL.",
|
|
1021
|
+
},
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
if (args.current.workerBuildId !== args.workerBuildId) {
|
|
1025
|
+
return {
|
|
1026
|
+
reuse: false,
|
|
1027
|
+
reason: "worker build changed",
|
|
1028
|
+
replacedExisting: true,
|
|
1029
|
+
probeAt,
|
|
1030
|
+
logPath: args.logPath,
|
|
1031
|
+
probe: {
|
|
1032
|
+
ok: false,
|
|
1033
|
+
stage: "health",
|
|
1034
|
+
attempts: 0,
|
|
1035
|
+
cause: "worker-build-stale",
|
|
1036
|
+
retryable: false,
|
|
1037
|
+
message: "Persisted bridge worker build is stale.",
|
|
1038
|
+
},
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
const health = await probeBridgeEndpoint({
|
|
1042
|
+
host: args.current.host,
|
|
1043
|
+
port: args.current.port,
|
|
1044
|
+
stage: "health",
|
|
1045
|
+
});
|
|
1046
|
+
appendBridgeLifecycleLog(args.logPath, `probe stage=health attempts=${String(health.attempts)} ok=${String(health.ok)}${health.ok ? "" : ` retryable=${String(health.retryable)} cause=${health.cause}`}`);
|
|
1047
|
+
if (!health.ok) {
|
|
1048
|
+
return {
|
|
1049
|
+
reuse: false,
|
|
1050
|
+
reason: `health probe failed: ${health.message}`,
|
|
1051
|
+
replacedExisting: true,
|
|
1052
|
+
probeAt,
|
|
1053
|
+
logPath: args.logPath,
|
|
1054
|
+
probe: health,
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
const auth = await probeBridgeEndpoint({
|
|
1058
|
+
host: args.current.host,
|
|
1059
|
+
port: args.current.port,
|
|
1060
|
+
stage: "auth",
|
|
1061
|
+
apiKey: args.expectedApiKey,
|
|
952
1062
|
});
|
|
1063
|
+
appendBridgeLifecycleLog(args.logPath, `probe stage=auth attempts=${String(auth.attempts)} ok=${String(auth.ok)}${auth.ok ? "" : ` retryable=${String(auth.retryable)} cause=${auth.cause}`}`);
|
|
1064
|
+
if (!auth.ok) {
|
|
1065
|
+
return {
|
|
1066
|
+
reuse: false,
|
|
1067
|
+
reason: `auth probe failed: ${auth.message}`,
|
|
1068
|
+
replacedExisting: true,
|
|
1069
|
+
probeAt,
|
|
1070
|
+
logPath: args.logPath,
|
|
1071
|
+
probe: auth,
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
return {
|
|
1075
|
+
reuse: true,
|
|
1076
|
+
health,
|
|
1077
|
+
auth,
|
|
1078
|
+
probeAt,
|
|
1079
|
+
logPath: args.logPath,
|
|
1080
|
+
};
|
|
953
1081
|
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1082
|
+
async function probeBridgeEndpoint(args) {
|
|
1083
|
+
for (let attempt = 1; attempt <= BRIDGE_REUSE_ATTEMPTS; attempt += 1) {
|
|
1084
|
+
const result = await requestBridgeProbe(args);
|
|
1085
|
+
if (result.ok) {
|
|
1086
|
+
return {
|
|
1087
|
+
ok: true,
|
|
1088
|
+
stage: args.stage,
|
|
1089
|
+
attempts: attempt,
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
if (!result.retryable || attempt === BRIDGE_REUSE_ATTEMPTS) {
|
|
1093
|
+
return {
|
|
1094
|
+
...result,
|
|
1095
|
+
stage: args.stage,
|
|
1096
|
+
attempts: attempt,
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
await new Promise((resolve) => setTimeout(resolve, BRIDGE_REUSE_DELAY_MS));
|
|
1100
|
+
}
|
|
1101
|
+
return {
|
|
1102
|
+
ok: false,
|
|
1103
|
+
stage: args.stage,
|
|
1104
|
+
attempts: BRIDGE_REUSE_ATTEMPTS,
|
|
1105
|
+
cause: "transport-timeout",
|
|
1106
|
+
retryable: true,
|
|
1107
|
+
message: "Bridge probe timed out.",
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
async function requestBridgeProbe(args) {
|
|
958
1111
|
return new Promise((resolve) => {
|
|
959
1112
|
const request = http.request({
|
|
960
|
-
host,
|
|
961
|
-
port,
|
|
1113
|
+
host: args.host,
|
|
1114
|
+
port: args.port,
|
|
962
1115
|
method: "GET",
|
|
963
|
-
path: "/v1/models",
|
|
964
|
-
timeout:
|
|
965
|
-
headers:
|
|
966
|
-
|
|
967
|
-
|
|
1116
|
+
path: args.stage === "health" ? "/healthz" : "/v1/models",
|
|
1117
|
+
timeout: BRIDGE_REUSE_TIMEOUT_MS,
|
|
1118
|
+
headers: args.stage === "auth"
|
|
1119
|
+
? {
|
|
1120
|
+
authorization: `Bearer ${args.apiKey ?? ""}`,
|
|
1121
|
+
}
|
|
1122
|
+
: undefined,
|
|
968
1123
|
}, (response) => {
|
|
969
1124
|
response.resume();
|
|
970
1125
|
if (response.statusCode === 200) {
|
|
971
1126
|
resolve({ ok: true });
|
|
972
1127
|
return;
|
|
973
1128
|
}
|
|
1129
|
+
if (args.stage === "auth" && (response.statusCode === 401 || response.statusCode === 403)) {
|
|
1130
|
+
resolve({
|
|
1131
|
+
ok: false,
|
|
1132
|
+
cause: "auth-rejected",
|
|
1133
|
+
retryable: false,
|
|
1134
|
+
statusCode: response.statusCode,
|
|
1135
|
+
message: `Authorization probe returned status ${String(response.statusCode)}.`,
|
|
1136
|
+
});
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
974
1139
|
resolve({
|
|
975
1140
|
ok: false,
|
|
976
|
-
cause:
|
|
1141
|
+
cause: args.stage === "health" ? "health-non-200" : "transport-error",
|
|
1142
|
+
retryable: false,
|
|
1143
|
+
statusCode: response.statusCode,
|
|
1144
|
+
message: args.stage === "health"
|
|
1145
|
+
? `Health endpoint returned status ${String(response.statusCode ?? 0)}.`
|
|
1146
|
+
: `Authorization probe returned status ${String(response.statusCode ?? 0)}.`,
|
|
977
1147
|
});
|
|
978
1148
|
});
|
|
979
1149
|
request.on("error", (error) => {
|
|
1150
|
+
const classified = classifyProbeTransportError(error);
|
|
980
1151
|
resolve({
|
|
981
1152
|
ok: false,
|
|
982
|
-
cause:
|
|
1153
|
+
cause: classified.cause,
|
|
1154
|
+
retryable: classified.retryable,
|
|
1155
|
+
message: error.message,
|
|
983
1156
|
});
|
|
984
1157
|
});
|
|
985
1158
|
request.on("timeout", () => {
|
|
986
|
-
request.destroy(new Error("Authorization probe timed out."));
|
|
1159
|
+
request.destroy(new Error(args.stage === "health" ? "Health endpoint timed out." : "Authorization probe timed out."));
|
|
987
1160
|
});
|
|
988
1161
|
request.end();
|
|
989
1162
|
});
|
|
990
1163
|
}
|
|
1164
|
+
function classifyProbeTransportError(error) {
|
|
1165
|
+
const message = error.message.toLowerCase();
|
|
1166
|
+
if (message.includes("timed out") ||
|
|
1167
|
+
message.includes("econnrefused") ||
|
|
1168
|
+
message.includes("econnreset") ||
|
|
1169
|
+
message.includes("socket hang up") ||
|
|
1170
|
+
message.includes("epipe")) {
|
|
1171
|
+
return {
|
|
1172
|
+
cause: message.includes("timed out") ? "transport-timeout" : "transport-error",
|
|
1173
|
+
retryable: true,
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
return {
|
|
1177
|
+
cause: "transport-error",
|
|
1178
|
+
retryable: false,
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
function ensureBridgeLogFile(logPath) {
|
|
1182
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
1183
|
+
if (!fs.existsSync(logPath)) {
|
|
1184
|
+
fs.writeFileSync(logPath, "", "utf8");
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
function openBridgeLogFd(logPath) {
|
|
1188
|
+
ensureBridgeLogFile(logPath);
|
|
1189
|
+
return fs.openSync(logPath, "a");
|
|
1190
|
+
}
|
|
1191
|
+
function appendBridgeLifecycleLog(logPath, message) {
|
|
1192
|
+
ensureBridgeLogFile(logPath);
|
|
1193
|
+
fs.appendFileSync(logPath, `[${new Date().toISOString()}] ${message}\n`, "utf8");
|
|
1194
|
+
}
|
|
991
1195
|
async function readJsonBody(request) {
|
|
992
1196
|
const chunks = [];
|
|
993
1197
|
for await (const chunk of request) {
|
|
@@ -37,6 +37,7 @@ exports.setCopilotCliSpawnImplementation = setCopilotCliSpawnImplementation;
|
|
|
37
37
|
exports.resetCopilotCliSpawnImplementation = resetCopilotCliSpawnImplementation;
|
|
38
38
|
exports.checkCopilotCliAvailable = checkCopilotCliAvailable;
|
|
39
39
|
exports.resolveCopilotCliInvocation = resolveCopilotCliInvocation;
|
|
40
|
+
exports.resolveCopilotSdkRuntimeInvocation = resolveCopilotSdkRuntimeInvocation;
|
|
40
41
|
exports.runCopilotLogin = runCopilotLogin;
|
|
41
42
|
const fs = __importStar(require("node:fs"));
|
|
42
43
|
const path = __importStar(require("node:path"));
|
|
@@ -85,6 +86,20 @@ function checkCopilotCliAvailable(runtimesDir) {
|
|
|
85
86
|
function resolveCopilotCliInvocation(args = [], runtimesDir) {
|
|
86
87
|
return getCopilotInvocation(args, runtimesDir);
|
|
87
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Resolves the explicit runtime entrypoint required by the Copilot SDK.
|
|
91
|
+
*/
|
|
92
|
+
function resolveCopilotSdkRuntimeInvocation(runtimesDir) {
|
|
93
|
+
const installDir = (0, copilot_installer_1.getCopilotRuntimeInstallDir)(runtimesDir);
|
|
94
|
+
const loaderPath = path.join(installDir, "node_modules", "@github", "copilot", "npm-loader.js");
|
|
95
|
+
if (!fs.existsSync(loaderPath)) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
path: loaderPath,
|
|
100
|
+
args: [],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
88
103
|
/**
|
|
89
104
|
* Launches the official `copilot login` flow in the current terminal.
|
|
90
105
|
*/
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.getCopilotBridgeStatePath = getCopilotBridgeStatePath;
|
|
37
|
+
exports.getCopilotBridgeLogPath = getCopilotBridgeLogPath;
|
|
37
38
|
exports.readCopilotBridgeState = readCopilotBridgeState;
|
|
38
39
|
exports.inspectCopilotBridgeState = inspectCopilotBridgeState;
|
|
39
40
|
exports.writeCopilotBridgeState = writeCopilotBridgeState;
|
|
@@ -54,6 +55,13 @@ function getCopilotBridgeStatePath(runtimeDir) {
|
|
|
54
55
|
const baseRuntimeDir = runtimeDir ? path.resolve(runtimeDir) : path.join((0, codex_paths_1.resolveCodexSwitchHome)(), "runtime");
|
|
55
56
|
return path.join(baseRuntimeDir, "copilot-bridge-state.json");
|
|
56
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Returns the persisted bridge runtime log path colocated with the bridge state manifest.
|
|
60
|
+
*/
|
|
61
|
+
function getCopilotBridgeLogPath(runtimeDir) {
|
|
62
|
+
const statePath = getCopilotBridgeStatePath(runtimeDir);
|
|
63
|
+
return path.join(path.dirname(statePath), "copilot-bridge.log");
|
|
64
|
+
}
|
|
57
65
|
/**
|
|
58
66
|
* Reads the Copilot bridge state manifest when present.
|
|
59
67
|
*/
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# codex-switch v0.1.3 Design
|
|
2
|
+
|
|
3
|
+
`0.1.3` is a targeted Copilot login compatibility repair release.
|
|
4
|
+
|
|
5
|
+
## Design Notes
|
|
6
|
+
|
|
7
|
+
- The SDK adapter constructs `CopilotClient` through `RuntimeConnection.forStdio({ path })`.
|
|
8
|
+
- The runtime path passed to the SDK resolves to the managed `@github/copilot/npm-loader.js` entrypoint.
|
|
9
|
+
- Human terminal commands such as `copilot --help` and `copilot login` continue to use the bundled `.bin` shim so interactive onboarding behavior remains unchanged.
|
|
10
|
+
- The release adds regression coverage for the constructor compatibility path and runtime loader resolution.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# codex-switch v0.1.4 Design
|
|
2
|
+
|
|
3
|
+
`0.1.4` is a bridge reliability and observability repair release.
|
|
4
|
+
|
|
5
|
+
## Design Notes
|
|
6
|
+
|
|
7
|
+
- Bridge reuse probing is refactored into a structured probe helper that distinguishes transient transport failures from deterministic mismatches.
|
|
8
|
+
- Health and auth reuse probes use 2 attempts, a 2500 ms per-request timeout, and a 250 ms delay between attempts.
|
|
9
|
+
- Existing bridge workers are retried before replacement only for transient timeout and transport failures. Deterministic mismatches still replace immediately: different provider, different base URL, stale worker build, or explicit `401`/`403` auth rejection.
|
|
10
|
+
- Bridge worker stdout/stderr are appended to a persisted runtime log file under the managed runtime-state directory, and that log path is surfaced in bridge results and bridge-related errors.
|
|
11
|
+
- Parent-side lifecycle logging records probe attempts, transient failures, replacement reasons, worker start, startup timeout, startup failure, and startup success without logging secrets.
|
|
12
|
+
- Worker-side stderr logging records startup, shutdown, uncaught exception, and unhandled rejection.
|
|
13
|
+
- The shared test runner discovers `tests/*.spec.js` in sorted order, excludes helper files, and supports both suite export styles already present in the repository: `{ run }` and `{ name, tests: [...] }`.
|
|
14
|
+
- Interactive provider selection uses the structured config once and resolves the visible current hint in this order:
|
|
15
|
+
1. unique managed mapping from top-level `model_provider`
|
|
16
|
+
2. fallback to legacy top-level `profile` when top-level `model_provider` is missing or unresolved and the legacy value exactly matches one provider name
|
|
17
|
+
3. ambiguous marker when multiple providers share the active `model_provider`
|
|
18
|
+
4. no marker otherwise
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# codex-switch v0.1.3 PRD
|
|
2
|
+
|
|
3
|
+
## Version
|
|
4
|
+
|
|
5
|
+
- Version line: `0.1.3`
|
|
6
|
+
- Current repository package version: `0.1.3`
|
|
7
|
+
|
|
8
|
+
## Summary
|
|
9
|
+
|
|
10
|
+
`0.1.3` is a narrow hotfix release for the broken `login copilot` path. The goal is to restore compatibility with the currently supported official Copilot SDK/runtime pairing without expanding the command surface or the experimental Copilot bridge scope.
|
|
11
|
+
|
|
12
|
+
## Required Outcome
|
|
13
|
+
|
|
14
|
+
- `codexs login copilot` must no longer fail during `CopilotClient` construction when the managed SDK/runtime is installed.
|
|
15
|
+
- The SDK integration must explicitly point at the managed Copilot runtime loader instead of relying on implicit package discovery.
|
|
16
|
+
- Existing direct-provider behavior and Copilot bridge behavior remain unchanged outside the constructor compatibility fix.
|
|
17
|
+
|
|
18
|
+
## Non-Goals
|
|
19
|
+
|
|
20
|
+
- No new Copilot features or new upstream families.
|
|
21
|
+
- No migration shims or backward-compatibility preservation for older local experimental runtime state.
|
|
22
|
+
- No changes to the managed SDK pin or Node version requirements.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# codex-switch v0.1.4 PRD
|
|
2
|
+
|
|
3
|
+
## Version
|
|
4
|
+
|
|
5
|
+
- Version line: `0.1.4`
|
|
6
|
+
- Target repository package version: `0.1.4`
|
|
7
|
+
|
|
8
|
+
## Summary
|
|
9
|
+
|
|
10
|
+
`0.1.4` is a bridge stability and observability release. It is a narrow reliability bridge between the existing Copilot bridge experiment and a stricter release gate, with no new provider families or migration behavior.
|
|
11
|
+
|
|
12
|
+
## Required Outcome
|
|
13
|
+
|
|
14
|
+
- Copilot bridge reuse must survive one transient health or auth probe failure before the existing worker is replaced.
|
|
15
|
+
- Bridge failures must expose actionable evidence, including the persisted runtime log path and the restart reason when an old worker is recycled.
|
|
16
|
+
- `npm test` must become the widened real release gate by discovering and running the broader repository suite set already present under `tests/`.
|
|
17
|
+
- Interactive provider selection must restore the visible `current` hint for legacy state where only the top-level `profile` is present and `model_provider` is absent.
|
|
18
|
+
|
|
19
|
+
## Release Scope
|
|
20
|
+
|
|
21
|
+
- Bridge reuse probe hardening for transient transport and timeout failures.
|
|
22
|
+
- Bridge log persistence and surfaced restart metadata across command results and diagnostics.
|
|
23
|
+
- Widened test-runner coverage plus any pre-existing red paths that block the widened suite from becoming the real ship gate.
|
|
24
|
+
- Interactive current-marker repair for legacy top-level `profile` fallback.
|
|
25
|
+
|
|
26
|
+
## Non-Goals
|
|
27
|
+
|
|
28
|
+
- No new provider families.
|
|
29
|
+
- No migration or backward-compatibility shims beyond the prompt-only legacy `profile` hint fallback.
|
|
30
|
+
- No command-surface expansion outside the new bridge diagnostics and status fields.
|
|
31
|
+
|
|
32
|
+
## Release Acceptance
|
|
33
|
+
|
|
34
|
+
- `switch` no longer flaps on one transient bridge probe failure.
|
|
35
|
+
- Bridge startup or reuse failures surface the log path and the worker replacement reason when applicable.
|
|
36
|
+
- `npm test` executes the widened deterministic suite set and passes.
|
|
37
|
+
- Interactive provider selection correctly marks the current provider from legacy top-level `profile` state when top-level `model_provider` is absent or unresolved.
|
package/docs/Tests/testing.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# codex-switch Testing Guide
|
|
2
2
|
|
|
3
3
|
This guide records the current `0.1.x` verification contract for release and review work.
|
|
4
|
+
|
|
5
|
+
The current repository line is `0.1.4` and remains an unreleased development line until an explicit release task says otherwise.
|
|
4
6
|
|
|
5
7
|
## Required checks
|
|
6
8
|
|
|
@@ -22,7 +24,8 @@ node dist/cli.js --version
|
|
|
22
24
|
- Ambiguous active profile: `list`, `status`, and provider pickers must surface ambiguity instead of inventing a unique current provider
|
|
23
25
|
- `--json` envelope: top-level `ok`, `command`, `data`, `warnings`, and `error` must remain stable
|
|
24
26
|
- `migrate`: advanced adopt helper only
|
|
25
|
-
- `setup`: deprecated entry only
|
|
27
|
+
- `setup`: deprecated entry only
|
|
28
|
+
- Release hygiene: `package.json`, `package-lock.json`, current-line docs, changelog top entry, and current PRD/Design fact sources must agree on the `0.1.4` development line
|
|
26
29
|
|
|
27
30
|
## Fixture guidance
|
|
28
31
|
|
package/docs/cli-usage.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# codex-switch CLI Usage
|
|
2
2
|
|
|
3
|
-
This document describes the current `0.1.
|
|
3
|
+
This document describes the current `0.1.4` repository development-line CLI contract for `@minniexcode/codex-switch`, including the bridge stability and observability boundary.
|
|
4
4
|
|
|
5
5
|
Executable command name:
|
|
6
6
|
|
|
@@ -10,9 +10,9 @@ codexs
|
|
|
10
10
|
|
|
11
11
|
## 1. Version Context
|
|
12
12
|
|
|
13
|
-
The current package version in this repository is `0.1.
|
|
13
|
+
The current package version in this repository is `0.1.4`.
|
|
14
14
|
|
|
15
|
-
This release line targets Codex `0.134.0+`. The public contract assumes runtime routing is selected by top-level `model` plus `model_provider`, while legacy `profile` and `[profiles.*]` remain inspect-and-adopt inputs instead of the recommended runtime path.
|
|
15
|
+
This release line targets Codex `0.134.0+`. The public contract assumes runtime routing is selected by top-level `model` plus `model_provider`, while legacy `profile` and `[profiles.*]` remain inspect-and-adopt inputs instead of the recommended runtime path. The current `0.1.4` development line tightens bridge reliability and release-hygiene verification without expanding the provider command surface.
|
|
16
16
|
|
|
17
17
|
## 2. Primary Workflows
|
|
18
18
|
|
|
@@ -287,4 +287,8 @@ codexs rollback [backup-id]
|
|
|
287
287
|
- [PRD 0.1.0](./PRD/codex-switch-prd-v0.1.0.md)
|
|
288
288
|
- [PRD 0.1.1](./PRD/codex-switch-prd-v0.1.1.md)
|
|
289
289
|
- [PRD 0.1.2](./PRD/codex-switch-prd-v0.1.2.md)
|
|
290
|
+
- [PRD 0.1.3](./PRD/codex-switch-prd-v0.1.3.md)
|
|
291
|
+
- [PRD 0.1.4](./PRD/codex-switch-prd-v0.1.4.md)
|
|
290
292
|
- [Design 0.1.2](./Design/codex-switch-v0.1.2-design.md)
|
|
293
|
+
- [Design 0.1.3](./Design/codex-switch-v0.1.3-design.md)
|
|
294
|
+
- [Design 0.1.4](./Design/codex-switch-v0.1.4-design.md)
|
|
@@ -4,13 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
这份文档介绍当前活跃产品事实源下的 `codex-switch` 产品定位。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
当前仓库开发线 fact source 以这些文档为准:
|
|
8
8
|
|
|
9
9
|
- [`cli-usage.md`](./cli-usage.md)
|
|
10
10
|
- [`PRD/codex-switch-prd-v0.1.0.md`](./PRD/codex-switch-prd-v0.1.0.md)
|
|
11
11
|
- [`PRD/codex-switch-prd-v0.1.1.md`](./PRD/codex-switch-prd-v0.1.1.md)
|
|
12
|
-
- [`PRD/codex-switch-prd-v0.1.2.md`](./PRD/codex-switch-prd-v0.1.2.md)
|
|
13
|
-
- [`
|
|
12
|
+
- [`PRD/codex-switch-prd-v0.1.2.md`](./PRD/codex-switch-prd-v0.1.2.md)
|
|
13
|
+
- [`PRD/codex-switch-prd-v0.1.3.md`](./PRD/codex-switch-prd-v0.1.3.md)
|
|
14
|
+
- [`PRD/codex-switch-prd-v0.1.4.md`](./PRD/codex-switch-prd-v0.1.4.md)
|
|
15
|
+
- [`Design/codex-switch-v0.1.2-design.md`](./Design/codex-switch-v0.1.2-design.md)
|
|
16
|
+
- [`Design/codex-switch-v0.1.3-design.md`](./Design/codex-switch-v0.1.3-design.md)
|
|
17
|
+
- [`Design/codex-switch-v0.1.4-design.md`](./Design/codex-switch-v0.1.4-design.md)
|
|
14
18
|
|
|
15
19
|
## 产品概述
|
|
16
20
|
|
|
@@ -88,4 +92,4 @@ codexs migrate
|
|
|
88
92
|
- `status` / `doctor` 如何帮助定位下一步
|
|
89
93
|
- 当前运行态是用顶层 `model` 与 `model_provider` 选择活动路由
|
|
90
94
|
|
|
91
|
-
`0.1.
|
|
95
|
+
`0.1.4` 是当前仓库开发线,重点不是扩展 provider 面,而是把已有 Copilot bridge 实验路径变得更稳、更可诊断,并把 release hygiene 拉进真实门禁。当前实现边界仍然是:Copilot 路径要求 Node.js `>=20`,受管安装默认固定到 `@github/copilot-sdk@1.0.2`,本地 bridge 仍然只是面向 simple text-oriented turns 的 experimental bridge;Direct provider 路径继续支持 Node.js `>=18`。
|
|
@@ -5,10 +5,14 @@
|
|
|
5
5
|
- [`cli-usage.md`](./cli-usage.md)
|
|
6
6
|
- [`PRD/codex-switch-prd-v0.1.0.md`](./PRD/codex-switch-prd-v0.1.0.md)
|
|
7
7
|
- [`PRD/codex-switch-prd-v0.1.1.md`](./PRD/codex-switch-prd-v0.1.1.md)
|
|
8
|
-
- [`PRD/codex-switch-prd-v0.1.2.md`](./PRD/codex-switch-prd-v0.1.2.md)
|
|
8
|
+
- [`PRD/codex-switch-prd-v0.1.2.md`](./PRD/codex-switch-prd-v0.1.2.md)
|
|
9
|
+
- [`PRD/codex-switch-prd-v0.1.3.md`](./PRD/codex-switch-prd-v0.1.3.md)
|
|
10
|
+
- [`PRD/codex-switch-prd-v0.1.4.md`](./PRD/codex-switch-prd-v0.1.4.md)
|
|
9
11
|
- [`Design/codex-switch-v0.1.0-design.md`](./Design/codex-switch-v0.1.0-design.md)
|
|
10
12
|
- [`Design/codex-switch-v0.1.1-design.md`](./Design/codex-switch-v0.1.1-design.md)
|
|
11
|
-
- [`Design/codex-switch-v0.1.2-design.md`](./Design/codex-switch-v0.1.2-design.md)
|
|
13
|
+
- [`Design/codex-switch-v0.1.2-design.md`](./Design/codex-switch-v0.1.2-design.md)
|
|
14
|
+
- [`Design/codex-switch-v0.1.3-design.md`](./Design/codex-switch-v0.1.3-design.md)
|
|
15
|
+
- [`Design/codex-switch-v0.1.4-design.md`](./Design/codex-switch-v0.1.4-design.md)
|
|
12
16
|
|
|
13
17
|
## Layers
|
|
14
18
|
|