@minniexcode/codex-switch 0.1.1 → 0.1.2

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.
Files changed (48) hide show
  1. package/README.CN.md +6 -4
  2. package/README.md +13 -4
  3. package/dist/app/add-provider.js +1 -0
  4. package/dist/app/bridge.js +2 -1
  5. package/dist/app/switch-provider.js +2 -1
  6. package/dist/commands/handlers.js +1 -0
  7. package/dist/domain/config.js +45 -1
  8. package/dist/domain/providers.js +1 -0
  9. package/dist/runtime/copilot-adapter.js +326 -70
  10. package/dist/runtime/copilot-bridge-worker.js +27 -2
  11. package/dist/runtime/copilot-bridge.js +192 -10
  12. package/dist/runtime/copilot-cli.js +7 -0
  13. package/dist/runtime/copilot-installer.js +59 -1
  14. package/dist/runtime/copilot-sdk-loader.js +4 -1
  15. package/docs/Design/codex-switch-v0.1.0-design.md +32 -152
  16. package/docs/Design/codex-switch-v0.1.1-design.md +15 -26
  17. package/docs/Design/codex-switch-v0.1.2-design.md +65 -0
  18. package/docs/PRD/codex-switch-prd-v0.1.0.md +65 -217
  19. package/docs/PRD/codex-switch-prd-v0.1.1.md +26 -0
  20. package/docs/PRD/codex-switch-prd-v0.1.2.md +41 -0
  21. package/docs/Tests/testing.md +1 -1
  22. package/docs/cli-usage.md +12 -4
  23. package/docs/codex-switch-command-design.md +1 -1
  24. package/docs/codex-switch-product-overview.md +7 -3
  25. package/docs/codex-switch-product-research.md +2 -2
  26. package/docs/codex-switch-technical-architecture.md +84 -1115
  27. package/package.json +1 -1
  28. package/docs/Design/codex-switch-copilot-integration-design.md +0 -517
  29. package/docs/Design/codex-switch-v0.0.10-design.md +0 -669
  30. package/docs/Design/codex-switch-v0.0.11-design.md +0 -824
  31. package/docs/Design/codex-switch-v0.0.12-design.md +0 -343
  32. package/docs/Design/codex-switch-v0.0.4-design.md +0 -874
  33. package/docs/Design/codex-switch-v0.0.5-design.md +0 -932
  34. package/docs/Design/codex-switch-v0.0.6-design.md +0 -708
  35. package/docs/Design/codex-switch-v0.0.7-design.md +0 -862
  36. package/docs/Design/codex-switch-v0.0.8-design.md +0 -132
  37. package/docs/Design/codex-switch-v0.0.9-design.md +0 -182
  38. package/docs/Design/codex-switch-v0.0.9-to-v0.0.12-roadmap.md +0 -413
  39. package/docs/PRD/codex-switch-prd-v0.0.10.md +0 -406
  40. package/docs/PRD/codex-switch-prd-v0.0.11.md +0 -577
  41. package/docs/PRD/codex-switch-prd-v0.0.12.md +0 -279
  42. package/docs/PRD/codex-switch-prd-v0.0.5-to-v0.1.0.md +0 -446
  43. package/docs/PRD/codex-switch-prd-v0.0.8.md +0 -62
  44. package/docs/PRD/codex-switch-prd-v0.0.9.md +0 -166
  45. package/docs/PRD/codex-switch-prd.md +0 -650
  46. package/docs/Tests/test-report-0.0.5.md +0 -163
  47. package/docs/Tests/test-report-0.0.7.md +0 -118
  48. 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
- response.write(`data: ${JSON.stringify(payload)}\n\n`);
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 payload = await context.executeChatCompletion({
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
- writeResponsesStream(response, payload);
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 = isCliError(error) && error.code === "BRIDGE_UNSUPPORTED_REQUEST" ? 400 : 500;
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,7 @@ 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;
39
40
  exports.runCopilotLogin = runCopilotLogin;
40
41
  const fs = __importStar(require("node:fs"));
41
42
  const path = __importStar(require("node:path"));
@@ -78,6 +79,12 @@ function checkCopilotCliAvailable(runtimesDir) {
78
79
  command: formatInvocation(invocation),
79
80
  };
80
81
  }
82
+ /**
83
+ * Resolves the Copilot CLI invocation used by SDK clients and command probes.
84
+ */
85
+ function resolveCopilotCliInvocation(args = [], runtimesDir) {
86
+ return getCopilotInvocation(args, runtimesDir);
87
+ }
81
88
  /**
82
89
  * Launches the official `copilot login` flow in the current terminal.
83
90
  */
@@ -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 = "latest";
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
- return Promise.resolve(`${getCopilotSdkEntrypoint(runtimesDir)}`).then(s => __importStar(require(s)));
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 `0.1.0` Design
2
-
3
- ## 文档信息
4
-
5
- - 文档类型:实现约束设计文档
6
- - 适用版本:`0.1.0`
7
- - 当前定位:release-hardening only
8
- - 关联 PRD:[`../PRD/codex-switch-prd-v0.1.0.md`](../PRD/codex-switch-prd-v0.1.0.md)
9
- - 关联 beta 设计:[`./codex-switch-v0.0.12-design.md`](./codex-switch-v0.0.12-design.md)
10
-
11
- ## 1. 设计原则
12
-
13
- `0.1.0` 的实现只做 release-hardening,不新增命令面,不改 JSON envelope,不引入兼容层,也不把历史草案继续扩成平台设想。
14
-
15
- 这个版本的实现目标是收口,不是扩张:
16
-
17
- - 把用户看见的主路径讲清楚。
18
- - 把 `list`、`status`、`doctor` 的可读语义讲清楚。
19
- - 把 `migrate` 和 `setup` 的产品定位讲清楚。
20
- - 把文档、help、输出、测试和包内容收口成同一套事实。
21
-
22
- ## 2. 当前阻塞项
23
-
24
- 实现收口前必须先正视以下阻塞项:
25
-
26
- 1. `tests/` 仍被忽略,导致回归测试无法稳定版本化。
27
- 2. README 仍引用不存在的 `docs/Tests/testing.md`,说明文档入口还未收口。
28
- 3. 版本叙事仍以 `0.0.12` 为中心,`0.1.0` 还没有被写成稳定发布线。
29
- 4. 主工作流、`migrate` 定位、`setup` 定位和真实实现状态还没有完全对齐。
30
-
31
- 只要这些阻塞项还存在,就不应把当前实现视为 `0.1.0` ready。
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
- ## Scope
3
+ ## Purpose
4
4
 
5
- - Support Codex `0.134.0+` only.
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
- ## Command Contract
7
+ ## Documentation Structure
12
8
 
13
- - `--profile <name>` remains a CLI alias for the stored `model_provider` id.
14
- - `--model <name>` stores the provider default switch model in `providers.json`.
15
- - `switch` writes top-level `model` and `model_provider`, repairs `[model_providers.<id>]`, rewrites `auth.json`, and removes the targeted legacy route selector/profile section.
16
- - `add` and `edit` repair `[model_providers.<id>]` and scrub `env_key` / `env_key_instructions`.
17
- - `remove --switch-to <provider-name>` switches by managed provider name, not by profile id.
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
- ## Persistence
18
+ ## Rules
20
19
 
21
- - `providers.json` keeps `profile` as the persisted `model_provider` id alias.
22
- - `providers.json` adds `model` for default route projection.
23
- - Managed `[model_providers.<id>]` sections write:
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.