@minniexcode/codex-switch 0.0.8 → 0.0.9

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.
@@ -37,6 +37,7 @@ exports.setCopilotBridgeSpawnImplementation = setCopilotBridgeSpawnImplementatio
37
37
  exports.resetCopilotBridgeSpawnImplementation = resetCopilotBridgeSpawnImplementation;
38
38
  exports.probeCopilotBridgeRuntime = probeCopilotBridgeRuntime;
39
39
  exports.ensureCopilotBridge = ensureCopilotBridge;
40
+ exports.startOrReuseCopilotBridge = startOrReuseCopilotBridge;
40
41
  exports.createCopilotBridgeRequestHandler = createCopilotBridgeRequestHandler;
41
42
  exports.startCopilotBridgeServer = startCopilotBridgeServer;
42
43
  exports.waitForCopilotBridgeHealth = waitForCopilotBridgeHealth;
@@ -66,6 +67,15 @@ function resetCopilotBridgeSpawnImplementation() {
66
67
  */
67
68
  async function probeCopilotBridgeRuntime(provider) {
68
69
  const state = (0, runtime_state_repo_1.readCopilotBridgeState)();
70
+ if (state && (!provider || !(0, providers_1.isCopilotBridgeProvider)(provider))) {
71
+ return {
72
+ ok: false,
73
+ runtime: "copilot-bridge",
74
+ reason: "failed",
75
+ cause: "Copilot bridge runtime state exists but no active Copilot bridge provider is selected.",
76
+ details: state,
77
+ };
78
+ }
69
79
  if (!provider || !(0, providers_1.isCopilotBridgeProvider)(provider)) {
70
80
  return {
71
81
  ok: false,
@@ -127,6 +137,12 @@ async function probeCopilotBridgeRuntime(provider) {
127
137
  * Starts or reuses a Copilot bridge worker, then verifies its health before returning.
128
138
  */
129
139
  async function ensureCopilotBridge(providerName, provider) {
140
+ return startOrReuseCopilotBridge(providerName, provider);
141
+ }
142
+ /**
143
+ * Starts or reuses a Copilot bridge worker and reports the chosen port.
144
+ */
145
+ async function startOrReuseCopilotBridge(providerName, provider) {
130
146
  if (!(0, providers_1.isCopilotBridgeProvider)(provider)) {
131
147
  throw (0, errors_1.cliError)("RUNTIME_PROVIDER_INVALID", "Provider is not backed by a Copilot bridge runtime.", {
132
148
  provider: providerName,
@@ -140,6 +156,7 @@ async function ensureCopilotBridge(providerName, provider) {
140
156
  }
141
157
  const expectedBaseUrl = (0, providers_1.buildCopilotBridgeBaseUrl)(runtime);
142
158
  const current = (0, runtime_state_repo_1.readCopilotBridgeState)();
159
+ let replaced = false;
143
160
  if (current && current.provider === providerName && current.baseUrl === expectedBaseUrl) {
144
161
  const healthy = await healthcheckCopilotBridge(current.host, current.port);
145
162
  if (healthy.ok) {
@@ -149,19 +166,20 @@ async function ensureCopilotBridge(providerName, provider) {
149
166
  });
150
167
  return {
151
168
  baseUrl: expectedBaseUrl,
169
+ host: current.host,
170
+ port: current.port,
152
171
  reused: true,
172
+ portChanged: false,
173
+ replaced: false,
153
174
  };
154
175
  }
155
176
  }
156
- const portCheck = await checkPortAvailability(runtime.bridgeHost, runtime.bridgePort);
157
- if (!portCheck.ok) {
158
- throw (0, errors_1.cliError)("BRIDGE_PORT_CONFLICT", "Copilot bridge port is already in use.", {
159
- provider: providerName,
160
- host: runtime.bridgeHost,
161
- port: runtime.bridgePort,
162
- cause: portCheck.cause,
163
- });
177
+ if (current && current.provider !== providerName) {
178
+ stopCopilotBridge();
179
+ replaced = true;
164
180
  }
181
+ const selectedPort = await selectBridgePort(runtime.bridgeHost, runtime.bridgePort);
182
+ const selectedBaseUrl = `http://${runtime.bridgeHost}:${selectedPort}${runtime.bridgePath}`;
165
183
  const workerPath = path.join(__dirname, "copilot-bridge-worker.js");
166
184
  let child;
167
185
  try {
@@ -172,9 +190,9 @@ async function ensureCopilotBridge(providerName, provider) {
172
190
  ...process.env,
173
191
  CODEX_SWITCH_BRIDGE_PROVIDER: providerName,
174
192
  CODEX_SWITCH_BRIDGE_HOST: runtime.bridgeHost,
175
- CODEX_SWITCH_BRIDGE_PORT: String(runtime.bridgePort),
193
+ CODEX_SWITCH_BRIDGE_PORT: String(selectedPort),
176
194
  CODEX_SWITCH_BRIDGE_API_KEY: provider.apiKey,
177
- CODEX_SWITCH_BRIDGE_BASE_URL: expectedBaseUrl,
195
+ CODEX_SWITCH_BRIDGE_BASE_URL: selectedBaseUrl,
178
196
  },
179
197
  });
180
198
  }
@@ -182,27 +200,27 @@ async function ensureCopilotBridge(providerName, provider) {
182
200
  throw (0, errors_1.cliError)("BRIDGE_START_FAILED", "Failed to start the Copilot bridge worker.", {
183
201
  provider: providerName,
184
202
  host: runtime.bridgeHost,
185
- port: runtime.bridgePort,
203
+ port: selectedPort,
186
204
  cause: error instanceof Error ? error.message : String(error),
187
205
  });
188
206
  }
189
207
  child.unref();
190
208
  const startedAt = new Date().toISOString();
191
- const healthy = await waitForCopilotBridgeStartup(child, runtime.bridgeHost, runtime.bridgePort, 15, 200);
209
+ const healthy = await waitForCopilotBridgeStartup(child, runtime.bridgeHost, selectedPort, 15, 200);
192
210
  if (!healthy.ok) {
193
211
  (0, runtime_state_repo_1.clearCopilotBridgeState)();
194
212
  if (healthy.reason === "start-failed") {
195
213
  throw (0, errors_1.cliError)("BRIDGE_START_FAILED", "Copilot bridge worker exited before becoming healthy.", {
196
214
  provider: providerName,
197
215
  host: runtime.bridgeHost,
198
- port: runtime.bridgePort,
216
+ port: selectedPort,
199
217
  cause: healthy.cause,
200
218
  });
201
219
  }
202
220
  throw (0, errors_1.cliError)("BRIDGE_HEALTHCHECK_FAILED", "Copilot bridge did not become healthy after startup.", {
203
221
  provider: providerName,
204
222
  host: runtime.bridgeHost,
205
- port: runtime.bridgePort,
223
+ port: selectedPort,
206
224
  cause: healthy.cause,
207
225
  });
208
226
  }
@@ -210,15 +228,19 @@ async function ensureCopilotBridge(providerName, provider) {
210
228
  provider: providerName,
211
229
  pid: child.pid ?? null,
212
230
  host: runtime.bridgeHost,
213
- port: runtime.bridgePort,
214
- baseUrl: expectedBaseUrl,
231
+ port: selectedPort,
232
+ baseUrl: selectedBaseUrl,
215
233
  startedAt,
216
234
  lastHealthcheckAt: new Date().toISOString(),
217
235
  };
218
236
  (0, runtime_state_repo_1.writeCopilotBridgeState)(state);
219
237
  return {
220
- baseUrl: expectedBaseUrl,
238
+ baseUrl: selectedBaseUrl,
239
+ host: runtime.bridgeHost,
240
+ port: selectedPort,
221
241
  reused: false,
242
+ portChanged: selectedPort !== runtime.bridgePort,
243
+ replaced,
222
244
  };
223
245
  }
224
246
  /**
@@ -342,6 +364,25 @@ async function checkPortAvailability(host, port) {
342
364
  });
343
365
  });
344
366
  }
367
+ async function selectBridgePort(host, preferredPort) {
368
+ const preferred = await checkPortAvailability(host, preferredPort);
369
+ if (preferred.ok) {
370
+ return preferredPort;
371
+ }
372
+ for (let port = 10000; port <= 99999; port += 1) {
373
+ if (port === preferredPort) {
374
+ continue;
375
+ }
376
+ const available = await checkPortAvailability(host, port);
377
+ if (available.ok) {
378
+ return port;
379
+ }
380
+ }
381
+ throw (0, errors_1.cliError)("BRIDGE_PORT_CONFLICT", "Unable to find a free 5-digit bridge port.", {
382
+ host,
383
+ port: preferredPort,
384
+ });
385
+ }
345
386
  async function waitForCopilotBridgeStartup(child, host, port, attempts, delayMs) {
346
387
  let startupFailure = null;
347
388
  const onError = (error) => {
@@ -0,0 +1,182 @@
1
+ # codex-switch `0.0.9` Design
2
+
3
+ ## 1. Purpose
4
+
5
+ This design turns the `0.0.9` PRD into an implementable CLI and runtime spec for the local Copilot bridge path.
6
+
7
+ The release goal is not to introduce a background service product. It is to make the existing local Copilot bridge safe to start, stop, inspect, and reuse from a single-process user-space workflow.
8
+
9
+ ## 2. Scope
10
+
11
+ ### In scope
12
+
13
+ - `codexs bridge start [provider]`
14
+ - `codexs bridge stop [provider]`
15
+ - `codexs bridge status [provider]`
16
+ - single-instance bridge reuse and replacement
17
+ - detached user-space bridge workers
18
+ - 5-digit default port selection with recovery when the preferred port is occupied
19
+ - runtime state persistence outside the managed backup transaction boundary
20
+ - Copilot-only target selection helpers
21
+ - switch-time bridge reuse through the same lifecycle code path
22
+
23
+ ### Out of scope
24
+
25
+ - boot autostart
26
+ - login autostart
27
+ - Windows Service support
28
+ - multiple concurrent managed bridge instances
29
+ - non-Copilot runtime families
30
+ - any new auth storage scheme for upstream Copilot login state
31
+
32
+ ## 3. Command Surface
33
+
34
+ ### 3.1 New commands
35
+
36
+ - `codexs bridge start [provider]`
37
+ - `codexs bridge stop [provider]`
38
+ - `codexs bridge status [provider]`
39
+
40
+ The command registry must add nested tokens under the public `bridge` namespace, with the longest token sequence winning during argument resolution.
41
+
42
+ ### 3.2 Command semantics
43
+
44
+ - `start` is a write command
45
+ - `stop` is an operational write/recovery command
46
+ - `status` is a read command
47
+
48
+ ### 3.3 Target resolution
49
+
50
+ For `bridge start`, provider resolution proceeds in this order:
51
+
52
+ 1. explicit provider argument
53
+ 2. current active provider, if it is a Copilot bridge provider
54
+ 3. sole configured Copilot bridge provider
55
+ 4. interactive Copilot-only provider picker in a TTY
56
+
57
+ If none of those paths resolves a target, the command fails with a design-level bridge target error.
58
+
59
+ For `bridge stop` and `bridge status`, resolution prefers the current runtime-state instance when present. An explicit provider argument acts as a guard and must not silently target a different provider instance.
60
+
61
+ ## 4. Runtime Model
62
+
63
+ ### 4.1 Single-instance policy
64
+
65
+ The bridge runtime is a single detached user-space worker.
66
+
67
+ - If the bridge is already healthy for the same provider, `start` and `switch` reuse it.
68
+ - If the bridge is healthy for a different provider, the current managed instance is stopped before a new one is started.
69
+ - Runtime state is stored separately from the managed file backup transaction, so the bridge can survive or be inspected outside file rollbacks.
70
+
71
+ ### 4.2 Health checks
72
+
73
+ Bridge startup must verify the worker by probing the local `/healthz` endpoint before the command reports success.
74
+
75
+ Bridge status must report:
76
+
77
+ - the last known runtime state
78
+ - whether the provider binding matches the expected provider
79
+ - whether the live worker is healthy
80
+ - the reason if the state is stale or mismatched
81
+
82
+ ### 4.3 Stop behavior
83
+
84
+ `bridge stop` is protection-first.
85
+
86
+ - It stops the managed worker when one exists.
87
+ - It clears the runtime-state manifest.
88
+ - It must not mutate `providers.json`, `config.toml`, or `auth.json`.
89
+ - If the caller supplies a provider that does not match the tracked runtime state, the command fails rather than stopping the wrong instance.
90
+
91
+ ## 5. Port Policy
92
+
93
+ - The default bridge host is `127.0.0.1`.
94
+ - The default bridge port is fixed at `41415`.
95
+ - The port must always remain in the 5-digit range.
96
+ - If the preferred port is occupied, the runtime must automatically select another free 5-digit port.
97
+ - Any recovered port must be persisted back into the provider record and the corresponding `config.toml` runtime projection before the command reports success.
98
+ - The persisted port must match the live worker port.
99
+
100
+ The key design constraint is that runtime state and managed config cannot diverge after a successful start.
101
+
102
+ ## 6. Persistence Boundaries
103
+
104
+ `bridge start` may update:
105
+
106
+ - `providers.json`
107
+ - the matching `config.toml` bridge projection
108
+ - runtime state manifest
109
+
110
+ `bridge start` must not rewrite:
111
+
112
+ - `auth.json`
113
+ - active profile state except when invoked through the shared `switch` flow
114
+
115
+ `switch` keeps using the same bridge lifecycle path and performs its own auth/config transaction afterward.
116
+
117
+ Runtime state is explicitly outside backup transactions. If file persistence fails after the worker has started, the implementation must clean up the new worker unless it was reused from a previous healthy instance.
118
+
119
+ ## 7. Error Model
120
+
121
+ The implementation should reuse existing error families where possible and add bridge-specific errors for unresolved target and provider mismatch cases.
122
+
123
+ Relevant bridge/runtime errors:
124
+
125
+ - `BRIDGE_TARGET_UNRESOLVED`
126
+ - `BRIDGE_PROVIDER_MISMATCH`
127
+ - `BRIDGE_PORT_CONFLICT`
128
+ - `BRIDGE_START_FAILED`
129
+ - `BRIDGE_HEALTHCHECK_FAILED`
130
+ - `RUNTIME_PROVIDER_INVALID`
131
+ - `PROVIDER_BASE_URL_MISMATCH`
132
+
133
+ The design intent is that the bridge commands fail clearly before mutating managed files when the provider cannot be resolved or the provider/runtime binding is inconsistent.
134
+
135
+ ## 8. Implementation Shape
136
+
137
+ The implementation should be split into three layers:
138
+
139
+ - command wiring in `src/commands/`
140
+ - app use cases in `src/app/`
141
+ - worker and healthcheck code in `src/runtime/`
142
+
143
+ The bridge-specific app layer should own:
144
+
145
+ - target resolution
146
+ - port recovery
147
+ - provider/config projection updates
148
+ - runtime-state cleanup
149
+
150
+ The runtime layer should own:
151
+
152
+ - worker spawning
153
+ - healthcheck probing
154
+ - detached-process cleanup
155
+ - runtime-state manifest read/write
156
+
157
+ ## 9. Tests
158
+
159
+ Minimum coverage for `0.0.9`:
160
+
161
+ - command parsing and help for `bridge start|stop|status`
162
+ - longest-token resolution for nested commands
163
+ - explicit provider, active-provider, sole-provider, and TTY fallback selection
164
+ - current-runtime-state preference for stop/status
165
+ - mismatch-guard behavior for stop/status
166
+ - single-instance reuse and replacement
167
+ - stale runtime-state cleanup
168
+ - port conflict recovery
169
+ - persisted port updates
170
+ - cleanup when file mutation fails after a successful bridge start
171
+ - direct-provider regressions
172
+
173
+ ## 10. Release Criteria
174
+
175
+ `0.0.9` is complete when the following are true:
176
+
177
+ - Copilot bridge can be managed manually with `bridge start|stop|status`
178
+ - `switch` reuses the same lifecycle path
179
+ - the default bridge port is 5 digits and port conflicts recover automatically
180
+ - runtime state is durable enough for status and stop behavior, but remains outside managed backup transactions
181
+ - direct providers keep their existing behavior
182
+
@@ -0,0 +1,166 @@
1
+ # codex-switch `0.0.9` PRD
2
+
3
+ ## 文档信息
4
+
5
+ - 状态:Active PRD
6
+ - 产品名:`codex-switch`
7
+ - CLI 命令名:`codexs`
8
+ - 当前基线版本:`0.0.8`
9
+ - 目标版本:`0.0.9`
10
+ - 文档定位:定义 `0.0.8 -> 0.0.9` 的直接需求范围
11
+ - 关联设计文档:`../Design/codex-switch-v0.0.9-design.md`(待产出)
12
+ - 关联上一版设计:[`../Design/codex-switch-v0.0.8-design.md`](../Design/codex-switch-v0.0.8-design.md)
13
+
14
+ ## 一句话定义
15
+
16
+ `0.0.9` 的目标,是把 GitHub Copilot 的本地 bridge 从“切换时临时可用”收敛成“可按需自动拉起、可手动管理、可诊断恢复”的稳定运行态能力。
17
+
18
+ ## In Scope
19
+
20
+ - 官方 `@github/copilot-sdk`
21
+ - 本地 Node/TypeScript bridge
22
+ - detached 用户态 bridge 守护进程
23
+ - `switch` 的 bridge 自动启动 / 复用
24
+ - `bridge start`
25
+ - `bridge stop`
26
+ - `bridge status`
27
+ - 单实例 bridge 生命周期管理
28
+ - 5 位数 bridge 端口默认策略
29
+ - 端口冲突自动探测与自动换端口
30
+ - runtime state manifest 的持久化与诊断
31
+ - `status` / `doctor` 的 bridge 生命周期诊断
32
+ - direct provider 回归稳定性
33
+
34
+ ## Out of Scope
35
+
36
+ - 开机自启动
37
+ - 登录自启动
38
+ - Windows Service / 系统服务注册
39
+ - 多实例 bridge 编排
40
+ - 非官方 SDK
41
+ - 独立 GUI / TUI
42
+ - Responses API、Embeddings、图像/音频接口
43
+ - 持久化 GitHub token 到 `providers.json` / `auth.json`
44
+
45
+ ## 运行形态
46
+
47
+ - bridge 是由 `codex-switch` 启动和管理的本地后台进程
48
+ - bridge 不是系统服务,也不注册开机或登录自动启动项
49
+ - `switch` 到 Copilot provider 时,bridge 应自动启动或复用
50
+ - 用户也可以通过 `bridge` 命令族手动启动、停止和查看状态
51
+ - `0.0.9` 的“守护进程”语义仅指 detached 用户态后台进程,不等同于 OS service
52
+
53
+ ## 命令面
54
+
55
+ 新增命令:
56
+
57
+ - `codexs bridge start [provider]`
58
+ - `codexs bridge stop [provider]`
59
+ - `codexs bridge status [provider]`
60
+
61
+ 命令规则:
62
+
63
+ - `bridge` 命令族只服务于 Copilot runtime-backed provider
64
+ - `provider` 可显式传入;未传入时优先使用当前 active provider
65
+ - 如果未传入且当前 active provider 不是 Copilot provider:
66
+ - TTY 下允许交互选择目标 Copilot provider
67
+ - 非交互或 `--json` 下必须明确失败
68
+ - `bridge start` 必须先完成:
69
+ - provider 解析
70
+ - runtime 配置校验
71
+ - SDK probe
72
+ - Copilot auth state 校验
73
+ - bridge 启动或复用
74
+ - healthcheck
75
+ - `bridge stop` 只停止当前受管 bridge 进程并清理 runtime state manifest,不修改 provider registry
76
+ - `bridge status` 返回 runtime state、provider 绑定关系、healthcheck 结果与异常原因
77
+
78
+ ## 生命周期规则
79
+
80
+ - `add --copilot` 只负责创建 provider 和可选安装 SDK,不负责启动 bridge
81
+ - `switch <copilot-provider>` 仍然是最常见的自动启动入口
82
+ - `bridge start` 与 `switch` 共享同一套 bridge 启动与复用逻辑
83
+ - 同一时刻只允许一个 bridge 实例处于受管运行状态
84
+ - 如果 bridge 已为同一 provider 运行且健康,则 `bridge start` 和 `switch` 都直接复用
85
+ - 如果已有 bridge 为另一 Copilot provider 运行:
86
+ - `bridge start <new-provider>` 先停止旧实例,再启动新实例
87
+ - `switch <new-provider>` 也遵守同样的单实例替换规则
88
+ - `bridge stop` 应该是幂等的:bridge 已停止时不报致命错误
89
+
90
+ ## 端口策略
91
+
92
+ - `--bridge-host` 默认 `127.0.0.1`
93
+ - `--bridge-port` 默认值必须是 5 位数端口
94
+ - Copilot provider 的默认端口不再使用 `4141`
95
+ - 如果 provider 指定端口已被占用:
96
+ - bridge 启动阶段必须自动检测冲突
97
+ - 自动选择新的可用 5 位数端口
98
+ - 自动更新 provider 的 runtime 配置和对应 `baseUrl`
99
+ - 自动保持 `config.toml` runtime projection 与 provider 配置一致
100
+ - 自动换端口后的新端口必须持久化,避免下一次启动再次撞到旧端口
101
+ - 端口自动切换只允许在 Copilot bridge provider 路径发生,不能影响 direct provider
102
+
103
+ ## 依赖策略
104
+
105
+ - `@github/copilot-sdk` 继续不进入 core CLI 默认依赖
106
+ - 命中 Copilot 路径时才 probe SDK
107
+ - SDK 自动安装仍然只允许发生在 `add --copilot`
108
+ - `bridge start`、`switch`、`status`、`doctor` 都不能隐式安装 SDK
109
+ - `bridge start` 在 SDK 缺失时必须明确失败,并提示回到 `add --copilot --install-copilot-sdk`
110
+ - SDK 运行前提仍然是用户本机已具备可用的 GitHub Copilot CLI / login 状态,`codex-switch` 不托管这部分登录数据
111
+
112
+ ## 数据边界
113
+
114
+ - `providers.json` 保存 Copilot provider、bridge host/port、`baseUrl` 与 shared secret
115
+ - `config.toml` 保存 Copilot provider 对应的 runtime `base_url` 与 `env_key`
116
+ - `auth.json` 只保存 Codex 到本地 bridge 的 shared secret
117
+ - runtime state manifest 保存当前 bridge 进程状态、provider 绑定、最后健康检查时间
118
+ - runtime state manifest 不进入 managed backup 事务
119
+ - GitHub/Copilot 上游认证状态仍然只由官方 SDK 管理,不进入受管 registry
120
+
121
+ ## 错误语义
122
+
123
+ `0.0.9` 继续稳定并收敛以下错误码:
124
+
125
+ - `COPILOT_SDK_MISSING`
126
+ - `COPILOT_SDK_INSTALL_FAILED`
127
+ - `COPILOT_SDK_INSTALL_REQUIRES_TTY`
128
+ - `COPILOT_SDK_UNSUPPORTED`
129
+ - `COPILOT_AUTH_REQUIRED`
130
+ - `COPILOT_PREMIUM_UNAVAILABLE`
131
+ - `BRIDGE_PORT_CONFLICT`
132
+ - `BRIDGE_START_FAILED`
133
+ - `BRIDGE_HEALTHCHECK_FAILED`
134
+ - `RUNTIME_PROVIDER_INVALID`
135
+ - `PROVIDER_BASE_URL_MISMATCH`
136
+
137
+ 新增要求:
138
+
139
+ - `bridge start`、`switch`、`bridge status` 对同一类 bridge 失败应尽量映射到同一类错误码
140
+ - 端口冲突在自动恢复成功时不应向上冒泡为致命失败
141
+ - 只有在无法找到可用端口或无法持久化新端口时,才允许保留 `BRIDGE_PORT_CONFLICT` 失败
142
+
143
+ ## 验收标准
144
+
145
+ - `add --copilot` 可以创建 Copilot provider,并写入 5 位数默认端口
146
+ - `switch` 到 Copilot provider 时完成 `probe -> auth check -> start or reuse bridge -> healthcheck -> config/auth write`
147
+ - `codexs bridge start [provider]` 可以手动启动或复用 bridge
148
+ - `codexs bridge stop [provider]` 可以手动停止当前 bridge,并清理 runtime state
149
+ - `codexs bridge status [provider]` 可以给出 bridge 是否运行、绑定哪个 provider、端口是多少、是否健康
150
+ - 当 bridge 目标端口被占用时,系统会自动切换到新的可用 5 位数端口,并持久化该变更
151
+ - direct provider 不经过 Copilot 特有的 runtime gate
152
+ - `status` / `doctor` 能诊断 SDK 缺失、认证缺失、bridge 不健康、stale runtime state、runtime base_url 漂移
153
+
154
+ ## 测试重点
155
+
156
+ - `bridge start/stop/status` 参数解析与帮助文案
157
+ - 省略 provider 时的 active provider 解析路径
158
+ - TTY 下的 Copilot provider 交互选择路径
159
+ - 非交互下目标 provider 不明确时的失败路径
160
+ - 单实例 bridge 的复用路径
161
+ - 单实例 bridge 的替换路径
162
+ - `switch` 与 `bridge start` 共享生命周期逻辑的回归
163
+ - 端口占用时的自动换端口与持久化
164
+ - stale runtime state 的诊断与清理
165
+ - `bridge stop` 幂等性
166
+ - direct provider 回归