@minniexcode/codex-switch 0.1.1 → 0.1.3
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.CN.md +6 -4
- package/README.md +15 -4
- package/dist/app/add-provider.js +1 -0
- package/dist/app/bridge.js +2 -1
- package/dist/app/switch-provider.js +2 -1
- package/dist/commands/handlers.js +1 -0
- package/dist/domain/config.js +45 -1
- package/dist/domain/providers.js +1 -0
- package/dist/runtime/copilot-adapter.js +346 -70
- package/dist/runtime/copilot-bridge-worker.js +27 -2
- package/dist/runtime/copilot-bridge.js +192 -10
- package/dist/runtime/copilot-cli.js +22 -0
- package/dist/runtime/copilot-installer.js +59 -1
- package/dist/runtime/copilot-sdk-loader.js +4 -1
- package/docs/Design/codex-switch-v0.1.0-design.md +32 -152
- package/docs/Design/codex-switch-v0.1.1-design.md +15 -26
- package/docs/Design/codex-switch-v0.1.2-design.md +65 -0
- package/docs/Design/codex-switch-v0.1.3-design.md +10 -0
- package/docs/PRD/codex-switch-prd-v0.1.0.md +65 -217
- package/docs/PRD/codex-switch-prd-v0.1.1.md +26 -0
- package/docs/PRD/codex-switch-prd-v0.1.2.md +41 -0
- package/docs/PRD/codex-switch-prd-v0.1.3.md +22 -0
- package/docs/Tests/testing.md +1 -1
- package/docs/cli-usage.md +14 -4
- package/docs/codex-switch-command-design.md +1 -1
- package/docs/codex-switch-product-overview.md +7 -3
- package/docs/codex-switch-product-research.md +2 -2
- package/docs/codex-switch-technical-architecture.md +86 -1115
- package/package.json +1 -1
- package/docs/Design/codex-switch-copilot-integration-design.md +0 -517
- package/docs/Design/codex-switch-v0.0.10-design.md +0 -669
- package/docs/Design/codex-switch-v0.0.11-design.md +0 -824
- package/docs/Design/codex-switch-v0.0.12-design.md +0 -343
- package/docs/Design/codex-switch-v0.0.4-design.md +0 -874
- package/docs/Design/codex-switch-v0.0.5-design.md +0 -932
- package/docs/Design/codex-switch-v0.0.6-design.md +0 -708
- package/docs/Design/codex-switch-v0.0.7-design.md +0 -862
- package/docs/Design/codex-switch-v0.0.8-design.md +0 -132
- package/docs/Design/codex-switch-v0.0.9-design.md +0 -182
- package/docs/Design/codex-switch-v0.0.9-to-v0.0.12-roadmap.md +0 -413
- package/docs/PRD/codex-switch-prd-v0.0.10.md +0 -406
- package/docs/PRD/codex-switch-prd-v0.0.11.md +0 -577
- package/docs/PRD/codex-switch-prd-v0.0.12.md +0 -279
- package/docs/PRD/codex-switch-prd-v0.0.5-to-v0.1.0.md +0 -446
- package/docs/PRD/codex-switch-prd-v0.0.8.md +0 -62
- package/docs/PRD/codex-switch-prd-v0.0.9.md +0 -166
- package/docs/PRD/codex-switch-prd.md +0 -650
- package/docs/Tests/test-report-0.0.5.md +0 -163
- package/docs/Tests/test-report-0.0.7.md +0 -118
- package/docs/Tests/testing-bridge-v0.0.9.md +0 -367
|
@@ -138,13 +138,13 @@ async function probeCopilotBridgeRuntime(provider, persistedState, runtimeDir) {
|
|
|
138
138
|
/**
|
|
139
139
|
* Starts or reuses a Copilot bridge worker, then verifies its health before returning.
|
|
140
140
|
*/
|
|
141
|
-
async function ensureCopilotBridge(providerName, provider, runtimeDir) {
|
|
142
|
-
return startOrReuseCopilotBridge(providerName, provider, runtimeDir);
|
|
141
|
+
async function ensureCopilotBridge(providerName, provider, runtimeDir, runtimesDir) {
|
|
142
|
+
return startOrReuseCopilotBridge(providerName, provider, runtimeDir, runtimesDir);
|
|
143
143
|
}
|
|
144
144
|
/**
|
|
145
145
|
* Starts or reuses a Copilot bridge worker and reports the chosen port.
|
|
146
146
|
*/
|
|
147
|
-
async function startOrReuseCopilotBridge(providerName, provider, runtimeDir) {
|
|
147
|
+
async function startOrReuseCopilotBridge(providerName, provider, runtimeDir, runtimesDir) {
|
|
148
148
|
if (!(0, providers_1.isCopilotBridgeProvider)(provider)) {
|
|
149
149
|
throw (0, errors_1.cliError)("RUNTIME_PROVIDER_INVALID", "Provider is not backed by a Copilot bridge runtime.", {
|
|
150
150
|
provider: providerName,
|
|
@@ -208,6 +208,8 @@ async function startOrReuseCopilotBridge(providerName, provider, runtimeDir) {
|
|
|
208
208
|
CODEX_SWITCH_BRIDGE_PORT: String(selectedPort),
|
|
209
209
|
CODEX_SWITCH_BRIDGE_API_KEY: provider.apiKey,
|
|
210
210
|
CODEX_SWITCH_BRIDGE_BASE_URL: selectedBaseUrl,
|
|
211
|
+
CODEX_SWITCH_RUNTIME_DIR: runtimeDir ?? "",
|
|
212
|
+
CODEX_SWITCH_RUNTIMES_DIR: runtimesDir ?? "",
|
|
211
213
|
},
|
|
212
214
|
});
|
|
213
215
|
}
|
|
@@ -285,19 +287,35 @@ function createCopilotBridgeRequestHandler(context) {
|
|
|
285
287
|
}
|
|
286
288
|
if (method === "POST" && url === "/v1/chat/completions") {
|
|
287
289
|
const body = await readJsonBody(request);
|
|
290
|
+
const timeoutMs = parseBridgeRequestTimeoutMs(body, "/v1/chat/completions");
|
|
288
291
|
const stream = Boolean(body.stream);
|
|
289
|
-
const payload = await context.executeChatCompletion(body);
|
|
290
292
|
if (stream) {
|
|
291
293
|
response.writeHead(200, {
|
|
292
294
|
"content-type": "text/event-stream",
|
|
293
295
|
"cache-control": "no-cache",
|
|
294
296
|
connection: "keep-alive",
|
|
295
297
|
});
|
|
296
|
-
|
|
298
|
+
const heartbeat = startSseHeartbeat(response);
|
|
299
|
+
const payload = await context.executeChatCompletion(body, {
|
|
300
|
+
timeoutMs,
|
|
301
|
+
onTextDelta: (delta) => {
|
|
302
|
+
response.write(`data: ${JSON.stringify({
|
|
303
|
+
choices: [
|
|
304
|
+
{
|
|
305
|
+
index: 0,
|
|
306
|
+
delta: { content: delta },
|
|
307
|
+
finish_reason: null,
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
})}\n\n`);
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
clearInterval(heartbeat);
|
|
297
314
|
response.write("data: [DONE]\n\n");
|
|
298
315
|
response.end();
|
|
299
316
|
return;
|
|
300
317
|
}
|
|
318
|
+
const payload = await context.executeChatCompletion(body, { timeoutMs });
|
|
301
319
|
response.writeHead(200, { "content-type": "application/json" });
|
|
302
320
|
response.end(JSON.stringify(payload));
|
|
303
321
|
return;
|
|
@@ -305,20 +323,43 @@ function createCopilotBridgeRequestHandler(context) {
|
|
|
305
323
|
if (method === "POST" && url === "/v1/responses") {
|
|
306
324
|
const body = await readJsonBody(request);
|
|
307
325
|
const normalized = normalizeResponsesRequest(body);
|
|
308
|
-
const
|
|
326
|
+
const chatPayload = {
|
|
309
327
|
model: normalized.model,
|
|
310
328
|
messages: normalized.messages,
|
|
311
|
-
}
|
|
329
|
+
};
|
|
312
330
|
if (normalized.stream) {
|
|
313
331
|
response.writeHead(200, {
|
|
314
332
|
"content-type": "text/event-stream",
|
|
315
333
|
"cache-control": "no-cache",
|
|
316
334
|
connection: "keep-alive",
|
|
317
335
|
});
|
|
318
|
-
|
|
336
|
+
const responseId = `resp_${Date.now()}`;
|
|
337
|
+
const messageId = buildResponsesMessageId(responseId);
|
|
338
|
+
writeResponsesStreamStart(response, responseId, normalized.model, messageId);
|
|
339
|
+
const heartbeat = startSseHeartbeat(response);
|
|
340
|
+
let text = "";
|
|
341
|
+
const payload = await context.executeChatCompletion(chatPayload, {
|
|
342
|
+
timeoutMs: normalized.timeoutMs,
|
|
343
|
+
onTextDelta: (delta) => {
|
|
344
|
+
text += delta;
|
|
345
|
+
writeResponsesTextDelta(response, messageId, delta);
|
|
346
|
+
},
|
|
347
|
+
onTextDone: (doneText) => {
|
|
348
|
+
if (text.length === 0) {
|
|
349
|
+
text = doneText;
|
|
350
|
+
writeResponsesTextDelta(response, messageId, doneText);
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
clearInterval(heartbeat);
|
|
355
|
+
const outputText = text || getChatCompletionText(payload);
|
|
356
|
+
writeResponsesStreamDone(response, responseId, normalized.model, messageId, outputText);
|
|
319
357
|
response.end();
|
|
320
358
|
return;
|
|
321
359
|
}
|
|
360
|
+
const payload = await context.executeChatCompletion(chatPayload, {
|
|
361
|
+
timeoutMs: normalized.timeoutMs,
|
|
362
|
+
});
|
|
322
363
|
response.writeHead(200, { "content-type": "application/json" });
|
|
323
364
|
response.end(JSON.stringify(buildResponsesPayload(payload)));
|
|
324
365
|
return;
|
|
@@ -332,9 +373,9 @@ function createCopilotBridgeRequestHandler(context) {
|
|
|
332
373
|
response.end(JSON.stringify({ error: { message: "Not found" } }));
|
|
333
374
|
}
|
|
334
375
|
catch (error) {
|
|
335
|
-
const statusCode =
|
|
376
|
+
const statusCode = mapBridgeErrorStatus(error);
|
|
336
377
|
response.writeHead(statusCode, { "content-type": "application/json" });
|
|
337
|
-
response.end(JSON.stringify({ error: { message: error instanceof Error ? error.message : String(error) } }));
|
|
378
|
+
response.end(JSON.stringify({ error: { message: error instanceof Error ? error.message : String(error), code: isCliError(error) ? error.code : "BRIDGE_RUNTIME_FAILURE" } }));
|
|
338
379
|
}
|
|
339
380
|
};
|
|
340
381
|
}
|
|
@@ -354,8 +395,22 @@ function normalizeResponsesRequest(body) {
|
|
|
354
395
|
model: payload.model,
|
|
355
396
|
messages,
|
|
356
397
|
stream: payload.stream === true,
|
|
398
|
+
timeoutMs: parseBridgeRequestTimeoutMs(body, "/v1/responses"),
|
|
357
399
|
};
|
|
358
400
|
}
|
|
401
|
+
/**
|
|
402
|
+
* Extracts one optional request timeout for bridge-backed completions.
|
|
403
|
+
*/
|
|
404
|
+
function parseBridgeRequestTimeoutMs(body, endpoint) {
|
|
405
|
+
const timeoutMsValue = body.timeout_ms ?? body.timeoutMs;
|
|
406
|
+
if (timeoutMsValue === undefined) {
|
|
407
|
+
return undefined;
|
|
408
|
+
}
|
|
409
|
+
if (typeof timeoutMsValue !== "number" || !Number.isFinite(timeoutMsValue) || timeoutMsValue <= 0) {
|
|
410
|
+
throw (0, errors_1.cliError)("BRIDGE_UNSUPPORTED_REQUEST", `Copilot bridge ${endpoint} timeout must be a positive number when provided.`);
|
|
411
|
+
}
|
|
412
|
+
return timeoutMsValue;
|
|
413
|
+
}
|
|
359
414
|
function normalizeResponsesInput(input) {
|
|
360
415
|
if (typeof input === "string") {
|
|
361
416
|
return [{ role: "user", content: input }];
|
|
@@ -575,6 +630,118 @@ function writeResponsesStream(response, payload) {
|
|
|
575
630
|
},
|
|
576
631
|
});
|
|
577
632
|
}
|
|
633
|
+
function writeResponsesStreamStart(response, responseId, model, messageId) {
|
|
634
|
+
const createdAt = Math.floor(Date.now() / 1000);
|
|
635
|
+
const inProgressResponse = {
|
|
636
|
+
id: responseId,
|
|
637
|
+
object: "response",
|
|
638
|
+
created_at: createdAt,
|
|
639
|
+
model,
|
|
640
|
+
status: "in_progress",
|
|
641
|
+
output: [],
|
|
642
|
+
output_text: "",
|
|
643
|
+
};
|
|
644
|
+
writeSseEvent(response, "response.created", {
|
|
645
|
+
type: "response.created",
|
|
646
|
+
response: inProgressResponse,
|
|
647
|
+
});
|
|
648
|
+
writeSseEvent(response, "response.in_progress", {
|
|
649
|
+
type: "response.in_progress",
|
|
650
|
+
response: inProgressResponse,
|
|
651
|
+
});
|
|
652
|
+
writeSseEvent(response, "response.output_item.added", {
|
|
653
|
+
type: "response.output_item.added",
|
|
654
|
+
output_index: 0,
|
|
655
|
+
item: {
|
|
656
|
+
id: messageId,
|
|
657
|
+
type: "message",
|
|
658
|
+
status: "in_progress",
|
|
659
|
+
role: "assistant",
|
|
660
|
+
content: [],
|
|
661
|
+
},
|
|
662
|
+
});
|
|
663
|
+
writeSseEvent(response, "response.content_part.added", {
|
|
664
|
+
type: "response.content_part.added",
|
|
665
|
+
item_id: messageId,
|
|
666
|
+
output_index: 0,
|
|
667
|
+
content_index: 0,
|
|
668
|
+
part: {
|
|
669
|
+
type: "output_text",
|
|
670
|
+
text: "",
|
|
671
|
+
annotations: [],
|
|
672
|
+
},
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
function writeResponsesTextDelta(response, messageId, delta) {
|
|
676
|
+
if (delta.length === 0) {
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
writeSseEvent(response, "response.output_text.delta", {
|
|
680
|
+
type: "response.output_text.delta",
|
|
681
|
+
item_id: messageId,
|
|
682
|
+
output_index: 0,
|
|
683
|
+
content_index: 0,
|
|
684
|
+
delta,
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
function writeResponsesStreamDone(response, responseId, model, messageId, outputText) {
|
|
688
|
+
const completedMessage = {
|
|
689
|
+
id: messageId,
|
|
690
|
+
type: "message",
|
|
691
|
+
status: "completed",
|
|
692
|
+
role: "assistant",
|
|
693
|
+
content: [
|
|
694
|
+
{
|
|
695
|
+
type: "output_text",
|
|
696
|
+
text: outputText,
|
|
697
|
+
annotations: [],
|
|
698
|
+
},
|
|
699
|
+
],
|
|
700
|
+
};
|
|
701
|
+
writeSseEvent(response, "response.output_text.done", {
|
|
702
|
+
type: "response.output_text.done",
|
|
703
|
+
item_id: messageId,
|
|
704
|
+
output_index: 0,
|
|
705
|
+
content_index: 0,
|
|
706
|
+
text: outputText,
|
|
707
|
+
});
|
|
708
|
+
writeSseEvent(response, "response.content_part.done", {
|
|
709
|
+
type: "response.content_part.done",
|
|
710
|
+
item_id: messageId,
|
|
711
|
+
output_index: 0,
|
|
712
|
+
content_index: 0,
|
|
713
|
+
part: {
|
|
714
|
+
type: "output_text",
|
|
715
|
+
text: outputText,
|
|
716
|
+
annotations: [],
|
|
717
|
+
},
|
|
718
|
+
});
|
|
719
|
+
writeSseEvent(response, "response.output_item.done", {
|
|
720
|
+
type: "response.output_item.done",
|
|
721
|
+
output_index: 0,
|
|
722
|
+
item: completedMessage,
|
|
723
|
+
});
|
|
724
|
+
writeSseEvent(response, "response.completed", {
|
|
725
|
+
type: "response.completed",
|
|
726
|
+
response: {
|
|
727
|
+
id: responseId,
|
|
728
|
+
object: "response",
|
|
729
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
730
|
+
model,
|
|
731
|
+
status: "completed",
|
|
732
|
+
output: [completedMessage],
|
|
733
|
+
output_text: outputText,
|
|
734
|
+
},
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
function startSseHeartbeat(response) {
|
|
738
|
+
return setInterval(() => {
|
|
739
|
+
response.write(": keep-alive\n\n");
|
|
740
|
+
}, 15000);
|
|
741
|
+
}
|
|
742
|
+
function getChatCompletionText(payload) {
|
|
743
|
+
return payload.choices?.[0]?.message?.content ?? "";
|
|
744
|
+
}
|
|
578
745
|
/**
|
|
579
746
|
* Formats and writes one server-sent event frame.
|
|
580
747
|
*/
|
|
@@ -594,6 +761,21 @@ function buildResponsesMessageId(responseId) {
|
|
|
594
761
|
function isCliError(error) {
|
|
595
762
|
return Boolean(error && typeof error === "object" && typeof error.code === "string");
|
|
596
763
|
}
|
|
764
|
+
function mapBridgeErrorStatus(error) {
|
|
765
|
+
if (!isCliError(error)) {
|
|
766
|
+
return 500;
|
|
767
|
+
}
|
|
768
|
+
if (error.code === "BRIDGE_UNSUPPORTED_REQUEST") {
|
|
769
|
+
return 400;
|
|
770
|
+
}
|
|
771
|
+
if (error.code === "COPILOT_AUTH_REQUIRED") {
|
|
772
|
+
return 401;
|
|
773
|
+
}
|
|
774
|
+
if (error.code === "BRIDGE_UPSTREAM_TIMEOUT") {
|
|
775
|
+
return 504;
|
|
776
|
+
}
|
|
777
|
+
return 500;
|
|
778
|
+
}
|
|
597
779
|
/**
|
|
598
780
|
* Returns a stable build identifier for the compiled bridge worker bundle.
|
|
599
781
|
*/
|
|
@@ -36,6 +36,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.setCopilotCliSpawnImplementation = setCopilotCliSpawnImplementation;
|
|
37
37
|
exports.resetCopilotCliSpawnImplementation = resetCopilotCliSpawnImplementation;
|
|
38
38
|
exports.checkCopilotCliAvailable = checkCopilotCliAvailable;
|
|
39
|
+
exports.resolveCopilotCliInvocation = resolveCopilotCliInvocation;
|
|
40
|
+
exports.resolveCopilotSdkRuntimeInvocation = resolveCopilotSdkRuntimeInvocation;
|
|
39
41
|
exports.runCopilotLogin = runCopilotLogin;
|
|
40
42
|
const fs = __importStar(require("node:fs"));
|
|
41
43
|
const path = __importStar(require("node:path"));
|
|
@@ -78,6 +80,26 @@ function checkCopilotCliAvailable(runtimesDir) {
|
|
|
78
80
|
command: formatInvocation(invocation),
|
|
79
81
|
};
|
|
80
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Resolves the Copilot CLI invocation used by SDK clients and command probes.
|
|
85
|
+
*/
|
|
86
|
+
function resolveCopilotCliInvocation(args = [], runtimesDir) {
|
|
87
|
+
return getCopilotInvocation(args, runtimesDir);
|
|
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
|
+
}
|
|
81
103
|
/**
|
|
82
104
|
* Launches the official `copilot login` flow in the current terminal.
|
|
83
105
|
*/
|
|
@@ -37,6 +37,10 @@ exports.setCopilotInstallerSpawnImplementation = setCopilotInstallerSpawnImpleme
|
|
|
37
37
|
exports.resetCopilotInstallerSpawnImplementation = resetCopilotInstallerSpawnImplementation;
|
|
38
38
|
exports.getCopilotRuntimeInstallDir = getCopilotRuntimeInstallDir;
|
|
39
39
|
exports.getCopilotSdkPackageName = getCopilotSdkPackageName;
|
|
40
|
+
exports.getSupportedCopilotSdkVersion = getSupportedCopilotSdkVersion;
|
|
41
|
+
exports.getCopilotNodeRuntimeStatus = getCopilotNodeRuntimeStatus;
|
|
42
|
+
exports.assertCopilotNodeRuntimeSupported = assertCopilotNodeRuntimeSupported;
|
|
43
|
+
exports.isSupportedCopilotSdkVersion = isSupportedCopilotSdkVersion;
|
|
40
44
|
exports.probeCopilotSdkInstall = probeCopilotSdkInstall;
|
|
41
45
|
exports.installCopilotSdk = installCopilotSdk;
|
|
42
46
|
const fs = __importStar(require("node:fs"));
|
|
@@ -45,7 +49,8 @@ const node_child_process_1 = require("node:child_process");
|
|
|
45
49
|
const errors_1 = require("../domain/errors");
|
|
46
50
|
const codex_paths_1 = require("../storage/codex-paths");
|
|
47
51
|
const COPILOT_SDK_PACKAGE = "@github/copilot-sdk";
|
|
48
|
-
const COPILOT_SDK_VERSION = "
|
|
52
|
+
const COPILOT_SDK_VERSION = "1.0.2";
|
|
53
|
+
const COPILOT_MIN_NODE_MAJOR = 20;
|
|
49
54
|
let spawnImplementation = node_child_process_1.spawnSync;
|
|
50
55
|
/**
|
|
51
56
|
* Overrides the spawn implementation for runtime installer tests.
|
|
@@ -76,6 +81,47 @@ function getCopilotRuntimeInstallDir(runtimesDir) {
|
|
|
76
81
|
function getCopilotSdkPackageName() {
|
|
77
82
|
return COPILOT_SDK_PACKAGE;
|
|
78
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Returns the supported Copilot SDK package version installed by this release.
|
|
86
|
+
*/
|
|
87
|
+
function getSupportedCopilotSdkVersion() {
|
|
88
|
+
return COPILOT_SDK_VERSION;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Returns whether the active Node.js runtime can run the Copilot SDK path.
|
|
92
|
+
*/
|
|
93
|
+
function getCopilotNodeRuntimeStatus(version = process.versions.node) {
|
|
94
|
+
const major = Number(version.split(".")[0]);
|
|
95
|
+
if (Number.isInteger(major) && major >= COPILOT_MIN_NODE_MAJOR) {
|
|
96
|
+
return { ok: true, version };
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
ok: false,
|
|
100
|
+
version,
|
|
101
|
+
required: `>=${String(COPILOT_MIN_NODE_MAJOR)}`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Fails early when a command path requires the Copilot SDK runtime under Node.js <20.
|
|
106
|
+
*/
|
|
107
|
+
function assertCopilotNodeRuntimeSupported(version = process.versions.node) {
|
|
108
|
+
const status = getCopilotNodeRuntimeStatus(version);
|
|
109
|
+
if (!status.ok) {
|
|
110
|
+
throw (0, errors_1.cliError)("COPILOT_RUNTIME_NODE_UNSUPPORTED", "Copilot runtime support requires Node.js >=20. Direct providers continue to support Node.js >=18.", {
|
|
111
|
+
nodeVersion: status.version,
|
|
112
|
+
requiredNode: status.required,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Returns whether an installed Copilot SDK version is supported by this release.
|
|
118
|
+
*/
|
|
119
|
+
function isSupportedCopilotSdkVersion(version) {
|
|
120
|
+
if (!version || version.includes("-")) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
return compareSemver(version, COPILOT_SDK_VERSION) >= 0;
|
|
124
|
+
}
|
|
79
125
|
/**
|
|
80
126
|
* Reports whether the optional Copilot SDK runtime is currently installed.
|
|
81
127
|
*/
|
|
@@ -153,6 +199,18 @@ function resolveNpmInstallCommand() {
|
|
|
153
199
|
args: installArgs,
|
|
154
200
|
};
|
|
155
201
|
}
|
|
202
|
+
function compareSemver(left, right) {
|
|
203
|
+
const leftParts = left.split(".").map((part) => Number(part));
|
|
204
|
+
const rightParts = right.split(".").map((part) => Number(part));
|
|
205
|
+
for (let index = 0; index < 3; index += 1) {
|
|
206
|
+
const leftPart = Number.isFinite(leftParts[index]) ? leftParts[index] : 0;
|
|
207
|
+
const rightPart = Number.isFinite(rightParts[index]) ? rightParts[index] : 0;
|
|
208
|
+
if (leftPart !== rightPart) {
|
|
209
|
+
return leftPart > rightPart ? 1 : -1;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
156
214
|
/**
|
|
157
215
|
* Finds a locally available npm CLI script near the active Node runtime.
|
|
158
216
|
*/
|
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.getCopilotSdkEntrypoint = getCopilotSdkEntrypoint;
|
|
37
37
|
exports.loadCopilotSdk = loadCopilotSdk;
|
|
38
38
|
const path = __importStar(require("node:path"));
|
|
39
|
+
const node_module_1 = require("node:module");
|
|
39
40
|
const errors_1 = require("../domain/errors");
|
|
40
41
|
const copilot_installer_1 = require("./copilot-installer");
|
|
41
42
|
/**
|
|
@@ -55,5 +56,7 @@ async function loadCopilotSdk(runtimesDir) {
|
|
|
55
56
|
packageName: status.packageName,
|
|
56
57
|
});
|
|
57
58
|
}
|
|
58
|
-
|
|
59
|
+
const runtimePackageJson = path.join(status.installDir, "package.json");
|
|
60
|
+
const runtimeRequire = (0, node_module_1.createRequire)(runtimePackageJson);
|
|
61
|
+
return runtimeRequire("@github/copilot-sdk");
|
|
59
62
|
}
|
|
@@ -1,152 +1,32 @@
|
|
|
1
|
-
# codex-switch
|
|
2
|
-
|
|
3
|
-
##
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
`
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
## 3. 收口矩阵
|
|
34
|
-
|
|
35
|
-
以下子系统必须完成对应收口。
|
|
36
|
-
|
|
37
|
-
| 子系统 | 必须稳定的内容 | 约束 |
|
|
38
|
-
| --- | --- | --- |
|
|
39
|
-
| 文档 | PRD、design、README、CLI usage、product overview、changelog、testing guide | 所有面向用户的文档必须与 `0.1.0` 事实一致 |
|
|
40
|
-
| 帮助 | 顶层 help、命令 help、示例顺序 | direct/Copilot 主路径优先,`migrate` 降级,`setup` 仅保留 deprecated 语义 |
|
|
41
|
-
| 输出 | `init`、`list`、`status`、`doctor`、`login` 的 human-readable 文案 | 只收口语义,不改 JSON envelope |
|
|
42
|
-
| 读路径 | tool home / runtime separation、dual-path model、ambiguous active profile 处理 | 不新增兼容层,不伪造 current 状态 |
|
|
43
|
-
| 测试 | release gate、回归测试、fixture 检查 | 回归测试必须落仓库,不能继续停留在忽略状态 |
|
|
44
|
-
|
|
45
|
-
## 4. 必须稳定的用户可见语义
|
|
46
|
-
|
|
47
|
-
### 4.1 `list`
|
|
48
|
-
|
|
49
|
-
`list` 必须能让用户直接看出:
|
|
50
|
-
|
|
51
|
-
- provider 属于 `direct` 还是 `copilot`
|
|
52
|
-
- 哪个 provider 是 current
|
|
53
|
-
- 当前 active profile 是否可唯一解析
|
|
54
|
-
|
|
55
|
-
如果当前 active profile 对应多个 provider,就必须显式表现为 ambiguous,而不是把任何一个 provider 假装成 current。
|
|
56
|
-
|
|
57
|
-
`list --json` 仍然使用既有 envelope,只允许追加字段,不允许改顶层契约。
|
|
58
|
-
|
|
59
|
-
### 4.2 `status`
|
|
60
|
-
|
|
61
|
-
`status` 必须把以下内容讲清楚:
|
|
62
|
-
|
|
63
|
-
- tool home 是什么
|
|
64
|
-
- target runtime 是什么
|
|
65
|
-
- 当前 active provider 是什么
|
|
66
|
-
- 当前路径是 direct 还是 Copilot
|
|
67
|
-
- 下一步应该做什么
|
|
68
|
-
|
|
69
|
-
`status` 是摘要,不是字段堆叠。输出顺序必须围绕“当前状态 -> 影响 -> 下一步”组织。
|
|
70
|
-
|
|
71
|
-
### 4.3 `doctor`
|
|
72
|
-
|
|
73
|
-
`doctor` 必须先给整体健康结论,再列 issue,再给修复建议。
|
|
74
|
-
|
|
75
|
-
每条 issue 至少要表达:
|
|
76
|
-
|
|
77
|
-
- 问题是什么
|
|
78
|
-
- 为什么重要
|
|
79
|
-
- 下一步怎么修
|
|
80
|
-
|
|
81
|
-
`doctor` 的目标不是罗列内部数据结构,而是把用户推到下一步修复动作。
|
|
82
|
-
|
|
83
|
-
### 4.4 provider picker
|
|
84
|
-
|
|
85
|
-
list 和 provider picker 必须一致处理 ambiguous active profile。
|
|
86
|
-
|
|
87
|
-
选择器提示至少要包含:
|
|
88
|
-
|
|
89
|
-
- `profile`
|
|
90
|
-
- `providerType`
|
|
91
|
-
- `current` 标记,仅在唯一解析时出现
|
|
92
|
-
|
|
93
|
-
### 4.5 命令定位
|
|
94
|
-
|
|
95
|
-
`0.1.0` 还必须稳定以下产品定位:
|
|
96
|
-
|
|
97
|
-
- 稳定命令面以 `init`、`login`、`list`、`show`、`current`、`status`、`doctor`、`config`、`add`、`edit`、`switch`、`remove`、`import`、`export`、`bridge`、`backups`、`rollback` 为准。
|
|
98
|
-
- `migrate` 只能被表述为高级 adopt helper。
|
|
99
|
-
- `setup` 只能被表述为 deprecated entry。
|
|
100
|
-
- `--json` 顶层 envelope 继续固定为 `ok / command / data / warnings / error`。
|
|
101
|
-
|
|
102
|
-
## 5. 文档同步要求
|
|
103
|
-
|
|
104
|
-
以下面向用户的文档必须与 `0.1.0` 事实一致:
|
|
105
|
-
|
|
106
|
-
- `README.md`
|
|
107
|
-
- `README.CN.md`
|
|
108
|
-
- `README.AI.md`
|
|
109
|
-
- `docs/cli-usage.md`
|
|
110
|
-
- `docs/codex-switch-product-overview.md`
|
|
111
|
-
- `docs/PRD/codex-switch-prd-v0.1.0.md`
|
|
112
|
-
- `docs/Design/codex-switch-v0.1.0-design.md`
|
|
113
|
-
- `CHANGELOG.md`
|
|
114
|
-
- `docs/Tests/testing.md`
|
|
115
|
-
|
|
116
|
-
历史大文档不需要在本版全文重写,但必须明确它们只是历史参考,不是当前 release contract。
|
|
117
|
-
|
|
118
|
-
## 6. 最小测试计划
|
|
119
|
-
|
|
120
|
-
`0.1.0` 的最小测试计划必须包含以下内容:
|
|
121
|
-
|
|
122
|
-
1. `npm run build`
|
|
123
|
-
2. `npm test`
|
|
124
|
-
3. `npx tsc --noEmit`
|
|
125
|
-
4. `npm pack --dry-run`
|
|
126
|
-
5. built CLI `--help`
|
|
127
|
-
6. built CLI `--version`
|
|
128
|
-
7. fresh direct provider flow
|
|
129
|
-
8. fresh Copilot provider flow
|
|
130
|
-
9. `list/status/doctor` 输出语义检查
|
|
131
|
-
10. `migrate` 高级 adopt helper 检查
|
|
132
|
-
11. `setup` deprecated entry 检查
|
|
133
|
-
|
|
134
|
-
测试结论必须落在仓库中的正式测试内容里,不能继续依赖忽略目录或口头约定。
|
|
135
|
-
|
|
136
|
-
## 7. 明确不做
|
|
137
|
-
|
|
138
|
-
本版不做以下事情:
|
|
139
|
-
|
|
140
|
-
- 新 upstream
|
|
141
|
-
- GUI / TUI
|
|
142
|
-
- daemon
|
|
143
|
-
- plugin system
|
|
144
|
-
- auto migration
|
|
145
|
-
- 兼容层
|
|
146
|
-
- dual-read / dual-write
|
|
147
|
-
- 重新设计公开 JSON envelope
|
|
148
|
-
- 重新扩张命令面
|
|
149
|
-
|
|
150
|
-
## 8. 结论
|
|
151
|
-
|
|
152
|
-
`0.1.0` 设计的核心不是“再造一个版本叙事”,而是把已存在的实现收口成稳定合同。实现只要偏离这条线,就不应被视为 `0.1.0` 的合理内容。
|
|
1
|
+
# codex-switch v0.1.0 Design
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
`0.1.0` freezes the stable CLI shape and documents the current architecture instead of introducing a new runtime model.
|
|
6
|
+
|
|
7
|
+
## Architecture
|
|
8
|
+
|
|
9
|
+
- `src/commands/` parses and dispatches public commands.
|
|
10
|
+
- `src/app/` owns use cases such as add, switch, status, doctor, bridge, and rollback.
|
|
11
|
+
- `src/domain/` owns provider, config, setup, and error contracts.
|
|
12
|
+
- `src/storage/` owns filesystem persistence for tool home and target Codex state.
|
|
13
|
+
- `src/runtime/` owns Codex CLI and optional Copilot runtime integrations.
|
|
14
|
+
|
|
15
|
+
## State Separation
|
|
16
|
+
|
|
17
|
+
The tool home stores managed state and backups. The target Codex home receives runtime projections. This keeps provider registry edits transactional and makes rollback meaningful without claiming ownership of unrelated Codex files.
|
|
18
|
+
|
|
19
|
+
## Projection
|
|
20
|
+
|
|
21
|
+
Switching a provider projects:
|
|
22
|
+
|
|
23
|
+
- top-level `model`
|
|
24
|
+
- top-level `model_provider`
|
|
25
|
+
- `[model_providers.<id>]`
|
|
26
|
+
- `auth.json` with `OPENAI_API_KEY`
|
|
27
|
+
|
|
28
|
+
Direct providers project the provider API key. Copilot providers project only the local bridge bearer secret; upstream GitHub/Copilot auth remains in the official runtime.
|
|
29
|
+
|
|
30
|
+
## Error Model
|
|
31
|
+
|
|
32
|
+
Command errors use stable CLI error codes and optional `details`. Command-specific errors may remain close to the command implementation when that keeps the public contract clearer than forcing every case into a shared diagnostic taxonomy.
|
|
@@ -1,33 +1,22 @@
|
|
|
1
1
|
# codex-switch v0.1.1 Design
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Purpose
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
- Treat top-level `model` and `model_provider` as the runtime routing source of truth.
|
|
7
|
-
- Treat legacy top-level `profile` and legacy `[profiles.*]` sections as adopt-only and diagnostic inputs.
|
|
8
|
-
- Project provider auth through `auth.json` with `OPENAI_API_KEY`.
|
|
9
|
-
- Do not write `env_key` or `env_key_instructions` into managed `[model_providers.<id>]` sections.
|
|
5
|
+
`0.1.1` is a documentation alignment release. The design work is to make the active docs tree match the stable implementation and remove obsolete development-version transition docs from the current fact surface.
|
|
10
6
|
|
|
11
|
-
##
|
|
7
|
+
## Documentation Structure
|
|
12
8
|
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
- `switch
|
|
16
|
-
- `
|
|
17
|
-
- `
|
|
9
|
+
- `README.md`, `README.CN.md`, and `README.AI.md` describe the user-facing product.
|
|
10
|
+
- `docs/cli-usage.md` is the command reference.
|
|
11
|
+
- `docs/codex-switch-product-overview.md` is the product-level summary.
|
|
12
|
+
- `docs/codex-switch-technical-architecture.md` is the implementation map.
|
|
13
|
+
- `docs/PRD/` contains active version fact sources for `0.1.0`, `0.1.1`, and planned `0.1.2`.
|
|
14
|
+
- `docs/Design/` contains matching design fact sources.
|
|
15
|
+
- `docs/Reference/` keeps Codex configuration references.
|
|
16
|
+
- `docs/Tests/testing.md` keeps the current verification contract.
|
|
18
17
|
|
|
19
|
-
##
|
|
18
|
+
## Rules
|
|
20
19
|
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
- `base_url`
|
|
25
|
-
- `name`
|
|
26
|
-
- `requires_openai_auth = true`
|
|
27
|
-
- `wire_api = "responses"`
|
|
28
|
-
|
|
29
|
-
## Legacy Handling
|
|
30
|
-
|
|
31
|
-
- `config show` and `config list-profiles` expose legacy profile inspection views.
|
|
32
|
-
- `migrate` remains a legacy adoption helper and does not modify the active top-level route.
|
|
33
|
-
- `doctor` flags missing top-level route fields, legacy selectors/sections, and legacy `env_key` wiring in the active model provider section.
|
|
20
|
+
- Public links should not point to removed `0.0.x` development documents.
|
|
21
|
+
- The active documentation set should describe current behavior, not historical migration intent.
|
|
22
|
+
- Copilot documentation should explicitly mark the bridge as local, bearer-protected, and backed by official GitHub Copilot runtime state.
|