@minniexcode/codex-switch 0.0.3 → 0.0.5

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 (44) hide show
  1. package/README.AI.md +8 -3
  2. package/README.md +160 -91
  3. package/dist/app/add-provider.js +32 -1
  4. package/dist/app/edit-provider.js +137 -0
  5. package/dist/app/get-status.js +9 -2
  6. package/dist/app/import-providers.js +47 -2
  7. package/dist/app/list-backups.js +17 -0
  8. package/dist/app/list-config-profiles.js +29 -0
  9. package/dist/app/remove-provider.js +34 -2
  10. package/dist/app/rollback-backup.js +30 -0
  11. package/dist/app/run-doctor.js +22 -21
  12. package/dist/app/setup-codex.js +155 -0
  13. package/dist/app/show-config.js +34 -0
  14. package/dist/app/show-provider.js +22 -0
  15. package/dist/app/switch-provider.js +5 -2
  16. package/dist/cli/add-interactive.js +25 -31
  17. package/dist/cli/args.js +19 -5
  18. package/dist/cli/help.js +109 -14
  19. package/dist/cli/interactive.js +123 -8
  20. package/dist/cli/output.js +56 -1
  21. package/dist/cli/prompt.js +19 -2
  22. package/dist/cli.js +250 -13
  23. package/dist/domain/backups.js +103 -0
  24. package/dist/domain/config.js +471 -39
  25. package/dist/domain/errors.js +3 -3
  26. package/dist/domain/providers.js +10 -0
  27. package/dist/domain/setup.js +30 -0
  28. package/dist/infra/backup-repo.js +65 -6
  29. package/dist/infra/codex-cli.js +79 -2
  30. package/dist/infra/codex-discovery.js +10 -0
  31. package/dist/infra/codex-paths.js +14 -1
  32. package/dist/infra/config-repo.js +102 -9
  33. package/dist/infra/providers-repo.js +29 -0
  34. package/docs/Design/codex-switch-v0.0.4-design.md +874 -0
  35. package/docs/Design/codex-switch-v0.0.5-design.md +922 -0
  36. package/docs/PRD/codex-switch-prd-v0.0.5-to-v0.1.0.md +308 -0
  37. package/docs/PRD/codex-switch-prd-v0.1.0.md +343 -0
  38. package/docs/{codex-switch-prd.md → PRD/codex-switch-prd.md} +9 -5
  39. package/docs/cli-usage.md +580 -0
  40. package/docs/codex-switch-command-design.md +1 -1
  41. package/docs/codex-switch-product-overview.md +1 -1
  42. package/docs/codex-switch-product-research.md +2 -2
  43. package/docs/codex-switch-technical-architecture.md +1 -1
  44. package/package.json +1 -1
@@ -0,0 +1,922 @@
1
+ # codex-switch `0.0.5` 设计文档
2
+
3
+ ## 文档信息
4
+
5
+ - 文档类型:详细设计文档
6
+ - 适用版本:`0.0.5`
7
+ - 目标范围:`0.0.4 -> 0.0.5`
8
+ - 主对齐 PRD:[`../PRD/codex-switch-prd-v0.1.0.md`](../PRD/codex-switch-prd-v0.1.0.md)
9
+ - 远期边界参考:[`../PRD/codex-switch-prd-v0.0.5-to-v0.1.0.md`](../PRD/codex-switch-prd-v0.0.5-to-v0.1.0.md)
10
+ - 风格基线:[`codex-switch-v0.0.4-design.md`](./codex-switch-v0.0.4-design.md)
11
+
12
+ ## 1. 文档目标
13
+
14
+ 这份文档回答的是 `0.0.5` 应该怎样落地,而不是继续讨论长期愿景:
15
+
16
+ - `config.toml` 怎样从字符串级切换升级为结构化读取与局部受控写回
17
+ - provider 管理命令怎样与 linked profile sections 保持一致
18
+ - `config show` / `config list-profiles` 的命令契约和返回边界是什么
19
+ - 历史状态、不一致状态、adopt / repair 流程怎样进入现有 CLI
20
+ - 当前代码结构上具体改哪些模块、补哪些错误码、补哪些测试
21
+
22
+ 目标是让实现阶段不再重复拍板关键技术决策。
23
+
24
+ ## 2. 版本定位与设计原则
25
+
26
+ ### 2.1 当前基线
27
+
28
+ 当前 `0.0.4` 已具备:
29
+
30
+ - provider registry 管理:`list`、`show`、`add`、`edit`、`remove`
31
+ - 运行态切换:`current`、`switch`
32
+ - 导入导出:`import`、`import --merge`、`export`
33
+ - 初始化与诊断:`setup`、`status`、`doctor`
34
+ - 备份恢复:`backups list`、`rollback`
35
+ - 统一 JSON envelope
36
+ - 写操作统一走锁、备份、失败回滚
37
+
38
+ 当前实现短板主要在 `config.toml`:
39
+
40
+ - 仅支持浅层读取顶层 active `profile`
41
+ - profile section 仍靠轻量字符串匹配
42
+ - 不能安全维护注释、空行和未受管内容
43
+ - provider 写命令没有稳定的 provider-config 双写一致性模型
44
+
45
+ ### 2.2 `0.0.5` 的一句话定义
46
+
47
+ `0.0.5` 的核心不是再堆一批命令,而是建立 `config.toml` 的结构化读取、provider-linked profile 管理和一致性诊断能力。
48
+
49
+ ### 2.3 设计原则
50
+
51
+ `0.0.5` 继续沿用现有工程原则:
52
+
53
+ - `CLI First`
54
+ - `Local First`
55
+ - `Safe by Default`
56
+ - `AI Friendly`
57
+ - `Split State Model`
58
+ - `Lightweight Transactions`
59
+
60
+ 在此基础上新增四条版本原则:
61
+
62
+ - 不把 `config.toml` 提升为 full config editor
63
+ - 不新增独立 `repair` 命令
64
+ - 不做全量 TOML parse -> stringify 写回
65
+ - 所有 provider-config 双写仍必须纳入同一备份与回滚事务
66
+
67
+ ## 3. 范围与边界
68
+
69
+ ### 3.1 `0.0.5` 范围内
70
+
71
+ 本设计覆盖:
72
+
73
+ - `config.toml` 结构化读取
74
+ - `[profiles.<name>]` 的最小受管写入能力
75
+ - 顶层 active `profile` 与 provider-linked profile section 的一致性
76
+ - `config show`
77
+ - `config list-profiles`
78
+ - `add` / `edit` / `remove` / `setup` / `import --merge` 的 config-aware 升级
79
+ - `doctor` / `status` 的一致性信号升级
80
+ - 多候选 Codex 目录发现与交互选择
81
+
82
+ ### 3.2 明确不在 `0.0.5` 范围内
83
+
84
+ 下面这些内容不进入本设计:
85
+
86
+ - 通用 `config edit` 命令族
87
+ - `model` / `base_url` 之外的大 profile schema 首版规格
88
+ - 独立 `repair` 命令
89
+ - 第三方 auth / extension 集成
90
+ - 任意顶层 TOML 键的自由增删改
91
+ - 自动化的“全量历史 profile 收编”
92
+
93
+ ### 3.3 数据边界
94
+
95
+ `0.0.5` 继续坚持状态分层:
96
+
97
+ - `providers.json`:provider registry 的单一事实源
98
+ - `config.toml`:运行态配置投影,部分受管
99
+ - `auth.json`:认证态
100
+ - `backups/` + `latest.json`:恢复态
101
+
102
+ 不引入数据库,不引入新的长期状态仓库。
103
+
104
+ ## 4. `0.0.5` 功能总览
105
+
106
+ `0.0.5` 需要完成四件事:
107
+
108
+ - 引入结构化 TOML 读取与非破坏性局部 patch 写回能力
109
+ - 增加稳定的 config 读取命令,供人类、AI 和脚本消费
110
+ - 让 provider 写命令同步维护 linked profile sections
111
+ - 补齐历史状态、不一致状态、共享 profile 和安全删除规则
112
+
113
+ ## 5. 数据模型设计
114
+
115
+ ### 5.1 `ManagedProfileFields`
116
+
117
+ `ManagedProfileFields` 表示真正写入 `[profiles.<name>]` 的受管字段。
118
+
119
+ `0.0.5` 只正式锁定:
120
+
121
+ ```ts
122
+ type ManagedProfileFields = {
123
+ model: string;
124
+ baseUrl: string;
125
+ };
126
+ ```
127
+
128
+ 规则:
129
+
130
+ - `model` 和 `base_url` 是当前唯一正式受管字段
131
+ - 写命令创建 profile section 时,必须同时具备 `model` 和 `base_url`
132
+ - 未提供任一必需字段时,不允许创建新的受管 section
133
+ - `apiKey` 继续只保存在 `providers.json`
134
+ - `note`、`tags` 等 provider 管理字段不进入 `config.toml`
135
+
136
+ 字段归属直接锁定如下:
137
+
138
+ - `[profiles.<name>]`:`model`、`base_url`
139
+ - `providers.json`:`profile`、`apiKey`、可选 `note`、`tags`
140
+ - `auth.json`:当前激活 provider 对应的运行态认证内容
141
+
142
+ 设计原因:
143
+
144
+ - 你的使用场景是“中转站”,`model` 和 `base_url` 共同定义真正的上游路由
145
+ - 如果 `base_url` 只放在 `providers.json`,而 `config.toml` 的 profile section 不受控,切换后就可能出现 profile 名变了但请求仍落到旧 endpoint 的分裂状态
146
+ - 因此 `0.0.5` 必须把 `base_url` 与 `model` 一起视为 profile runtime projection 的正式字段
147
+
148
+ ### 5.2 `ManagedProfileView`
149
+
150
+ `ManagedProfileView` 是读取命令返回的稳定视图,不等同于持久化结构。
151
+
152
+ 建议内部和输出层统一围绕以下字段:
153
+
154
+ ```ts
155
+ type ManagedProfileView = {
156
+ name: string;
157
+ managed: boolean;
158
+ isActive: boolean;
159
+ linkedProviders: string[];
160
+ model: string | null;
161
+ baseUrl: string | null;
162
+ managedFields: string[];
163
+ source: "managed" | "unmanaged" | "orphaned-reference";
164
+ };
165
+ ```
166
+
167
+ 字段语义:
168
+
169
+ - `name`:profile 名
170
+ - `managed`:是否至少被一个 provider 引用
171
+ - `isActive`:是否为顶层 active profile
172
+ - `linkedProviders`:引用该 profile 的 provider 名列表
173
+ - `model`:可识别的受管 `model` 值;不存在或不受管时为 `null`
174
+ - `baseUrl`:可识别的受管 `base_url` 值;不存在或不受管时为 `null`
175
+ - `managedFields`:当前识别到并纳入正式受管的字段名数组;`0.0.5` 只可能为 `[]`、`["model"]`、`["base_url"]` 或 `["model", "base_url"]`
176
+ - `source`:
177
+ - `managed`:section 存在且被 provider 引用
178
+ - `unmanaged`:section 存在但没有 provider 引用
179
+ - `orphaned-reference`:provider 引用了该 profile,但 `config.toml` 中缺少对应 section
180
+
181
+ ### 5.3 `ConfigConsistencyIssue`
182
+
183
+ `ConfigConsistencyIssue` 是 `doctor` / `status` 的问题抽象。
184
+
185
+ 最少覆盖以下问题类型:
186
+
187
+ ```ts
188
+ type ConfigConsistencyIssue =
189
+ | { code: "ORPHANED_PROFILE_REFERENCE"; profile: string; providers: string[] }
190
+ | { code: "UNMANAGED_ACTIVE_PROFILE"; profile: string }
191
+ | { code: "SHARED_PROFILE_REFERENCE"; profile: string; providers: string[] }
192
+ | { code: "ORPHANED_PROFILE_SECTION"; profile: string }
193
+ | { code: "DESTRUCTIVE_REMOVE_BLOCKED"; profile: string; provider: string; activeProfile: string };
194
+ ```
195
+
196
+ 说明:
197
+
198
+ - `ORPHANED_PROFILE_REFERENCE`:`providers.json` 引用了不存在的 profile section
199
+ - `UNMANAGED_ACTIVE_PROFILE`:当前 active profile 存在,但没有 provider 映射
200
+ - `SHARED_PROFILE_REFERENCE`:多个 provider 指向同一 profile;本身不一定是错误,但必须被识别
201
+ - `ORPHANED_PROFILE_SECTION`:`config.toml` 存在 profile section,但没有 provider 引用
202
+ - `DESTRUCTIVE_REMOVE_BLOCKED`:删除 provider 会导致 active profile 悬空,必须先切换
203
+
204
+ ### 5.4 配置文档内部抽象
205
+
206
+ 为了支撑非破坏性写回,`0.0.5` 在 infra 层引入下列内部抽象:
207
+
208
+ ```ts
209
+ type ParsedConfigDocument = {
210
+ rawText: string;
211
+ lineEnding: "\n" | "\r\n";
212
+ activeProfile: string | null;
213
+ profiles: ProfileSectionRef[];
214
+ };
215
+
216
+ type ProfileSectionRef = {
217
+ name: string;
218
+ headerStart: number;
219
+ sectionStart: number;
220
+ sectionEnd: number;
221
+ modelValueRange: { start: number; end: number } | null;
222
+ baseUrlValueRange: { start: number; end: number } | null;
223
+ model: string | null;
224
+ baseUrl: string | null;
225
+ };
226
+
227
+ type ConfigPatchOperation =
228
+ | { kind: "replace-range"; start: number; end: number; text: string }
229
+ | { kind: "insert-at"; index: number; text: string }
230
+ | { kind: "delete-range"; start: number; end: number };
231
+
232
+ type ConfigMutationPlan = {
233
+ operations: ConfigPatchOperation[];
234
+ createdProfileSections: string[];
235
+ deletedProfileSections: string[];
236
+ updatedProfiles: string[];
237
+ switchedActiveProfile: boolean;
238
+ };
239
+ ```
240
+
241
+ 这些抽象只作为实现内部契约,不直接暴露给 CLI 输出。
242
+
243
+ ## 6. TOML 处理路线
244
+
245
+ ### 6.1 技术决策
246
+
247
+ `0.0.5` 直接锁定 TOML 技术路线:
248
+
249
+ - 采用 `@toml-tools/parser` 一类的 CST / 结构化 parser 路线
250
+ - 目标是获得 section / field 的稳定位置边界
251
+ - 最终对原始文本执行局部 patch 写回
252
+
253
+ 不采用:
254
+
255
+ - 全量 parse -> stringify
256
+ - 重新序列化整个 `config.toml`
257
+ - 继续只靠字符串正则拼接
258
+
259
+ ### 6.2 为什么不做全量 stringify
260
+
261
+ 全量 stringify 不符合当前产品目标,因为它会带来下面这些不可接受的副作用:
262
+
263
+ - 破坏注释
264
+ - 破坏空行
265
+ - 改写未受管内容的顺序
266
+ - 让用户手工维护的 config 漂移过大
267
+
268
+ `0.0.5` 的目标是受控管理 provider-linked sections,而不是接管整份配置文件。
269
+
270
+ ### 6.3 Patch 规则
271
+
272
+ patch 规则直接锁定:
273
+
274
+ - 所有 patch 操作基于原始文本坐标生成
275
+ - 应用时按文本区间从后往前执行
276
+ - 这样可以避免前面 patch 影响后面 patch 的 offset
277
+
278
+ 示例策略:
279
+
280
+ 1. 先生成完整 `ConfigMutationPlan`
281
+ 2. 将 `replace-range` / `delete-range` / `insert-at` 按起始偏移倒序排序
282
+ 3. 统一在一次写回中应用
283
+
284
+ ### 6.4 结构化读取边界
285
+
286
+ `0.0.5` 结构化识别的范围只包括:
287
+
288
+ - 顶层 `profile = "..."`
289
+ - `[profiles.<name>]`
290
+ - `model = "..."`
291
+ - `base_url = "..."`
292
+
293
+ 其余字段允许原样存在,但:
294
+
295
+ - 不在 `0.0.5` 的正式受管写入范围内
296
+ - 不要求被完整解析成稳定 schema
297
+ - 不允许在写入时被误删或重排
298
+
299
+ ## 7. 目录发现与 `setup` 候选策略
300
+
301
+ ### 7.1 候选集算法
302
+
303
+ `setup` 的目录发现采用保守候选集,不做全盘扫描。
304
+
305
+ 优先规则:
306
+
307
+ - 若显式传入 `--codex-dir`,只使用该目录
308
+ - 否则候选集来自:
309
+ - `CODEXS_CODEX_DIR`
310
+ - `dev-codex/local-sandbox`,但仅在 `NODE_ENV=development`
311
+ - `~/.codex`
312
+
313
+ 候选处理:
314
+
315
+ - 去重
316
+ - 过滤不存在路径
317
+ - 保留顺序,形成最终候选列表
318
+
319
+ ### 7.2 交互规则
320
+
321
+ TTY 模式:
322
+
323
+ - 单候选:自动继续
324
+ - 多候选:让用户选择现有候选或手动输入
325
+ - 无候选:允许手动输入
326
+
327
+ 非交互模式:
328
+
329
+ - 多候选:返回 `CODEX_DIR_AMBIGUOUS`
330
+ - 无候选:返回 `CODEX_DIR_NOT_FOUND`
331
+
332
+ ### 7.3 设计取舍
333
+
334
+ 这里明确不做:
335
+
336
+ - 扫描整个用户目录
337
+ - 递归搜索所有潜在 config 路径
338
+ - 推断多个工作区下的任意历史目录
339
+
340
+ 原因是这类扫描成本高、噪声大、可预测性差,不适合作为稳定 CLI 契约。
341
+
342
+ ## 8. 命令设计
343
+
344
+ ### 8.1 新增命令面
345
+
346
+ `0.0.5` 新增:
347
+
348
+ ```bash
349
+ codexs config show [profile] [--json] [--codex-dir <path>]
350
+ codexs config list-profiles [--json] [--codex-dir <path>]
351
+ ```
352
+
353
+ ### 8.2 现有命令新增 flags
354
+
355
+ `add <provider>` 新增:
356
+
357
+ - `--create-profile`
358
+ - `--model <name>`
359
+ - `--base-url <url>`
360
+
361
+ `edit <provider>` 新增:
362
+
363
+ - `--create-profile`
364
+ - `--model <name>`
365
+ - `--base-url <url>`
366
+ - 继续支持 `--profile`
367
+
368
+ `remove <provider>` 新增:
369
+
370
+ - `--switch-to <profile>`
371
+
372
+ ### 8.3 不新增命令
373
+
374
+ `0.0.5` 明确不新增独立 `repair` 命令。
375
+
376
+ repair / adopt 路径通过以下方式承接:
377
+
378
+ - `doctor` 暴露问题和建议动作
379
+ - `setup` 在交互模式下承接 adopt
380
+ - `import --merge` 在交互模式下承接 adopt / repair
381
+ - 现有写命令在交互模式下承接必要确认
382
+
383
+ ## 9. 读取命令契约
384
+
385
+ ### 9.1 `config show`
386
+
387
+ #### 用途
388
+
389
+ 返回结构化 config 视图,可选聚焦单个 profile。
390
+
391
+ #### 命令形态
392
+
393
+ ```bash
394
+ codexs config show [profile] [--json] [--codex-dir <path>]
395
+ ```
396
+
397
+ #### 返回边界
398
+
399
+ 默认返回 `config.toml` 中全部可识别 profiles,而不是只返回 managed profiles。
400
+
401
+ JSON 最小字段:
402
+
403
+ ```json
404
+ {
405
+ "activeProfile": "packycode",
406
+ "selectedProfile": null,
407
+ "profiles": [
408
+ {
409
+ "name": "packycode",
410
+ "managed": true,
411
+ "isActive": true,
412
+ "linkedProviders": ["packycode"],
413
+ "model": "gpt-5",
414
+ "baseUrl": "https://relay.example.com/v1",
415
+ "managedFields": ["model", "base_url"],
416
+ "source": "managed"
417
+ }
418
+ ]
419
+ }
420
+ ```
421
+
422
+ 若传入 `[profile]`:
423
+
424
+ - `selectedProfile` 返回目标 profile
425
+ - `profiles` 仍建议保持数组 shape,但只包含目标视图
426
+ - 若目标来自 orphaned reference,也允许返回 `source = "orphaned-reference"` 的单条视图
427
+
428
+ #### 失败语义
429
+
430
+ - 读取失败:`CONFIG_NOT_FOUND` 或 `CONFIG_PARSE_ERROR`
431
+ - 指定 profile 不可识别:`PROFILE_NOT_FOUND`
432
+
433
+ ### 9.2 `config list-profiles`
434
+
435
+ #### 用途
436
+
437
+ 返回 profile 的轻量列表视图。
438
+
439
+ #### 命令形态
440
+
441
+ ```bash
442
+ codexs config list-profiles [--json] [--codex-dir <path>]
443
+ ```
444
+
445
+ #### 返回边界
446
+
447
+ 也必须返回全部可识别 profiles,而不是只返回 managed profiles。
448
+
449
+ 最小字段:
450
+
451
+ - `name`
452
+ - `managed`
453
+ - `isActive`
454
+ - `linkedProviders`
455
+ - `model`
456
+ - `baseUrl`
457
+ - `source`
458
+
459
+ 与 `config show` 的差异:
460
+
461
+ - `list-profiles` 是轻量列表
462
+ - `show` 允许更完整的单 profile 语义和问题上下文
463
+
464
+ ## 10. 写命令一致性规则
465
+
466
+ ### 10.1 `add <provider>`
467
+
468
+ 规则锁定如下:
469
+
470
+ - 当目标 profile 已存在时,只建立 provider -> profile 映射
471
+ - 当目标 profile 缺失时,只有同时传入 `--create-profile --model <name> --base-url <url>` 才允许创建 section
472
+ - 只传 `--create-profile` 但缺少 `--model` 或 `--base-url`,返回 `MANAGED_PROFILE_FIELDS_MISSING`
473
+ - 不允许写出 provider 指向缺失 profile 的新状态
474
+
475
+ ### 10.2 `edit <provider>`
476
+
477
+ 规则锁定如下:
478
+
479
+ - 改绑到已有 profile:更新 provider 映射并重新计算 active / shared 关系
480
+ - 改绑到缺失 profile:只有 `--create-profile --model <name> --base-url <url>` 才允许
481
+ - 旧 section 不做隐式 rename
482
+ - 旧 section 不做隐式 copy
483
+ - 旧 section 不做隐式 `model` / `base_url` 迁移
484
+
485
+ 删除旧 section 的规则:
486
+
487
+ - 若旧 profile 仍被其他 provider 引用:保留
488
+ - 若旧 profile 无其他引用且不是 active profile:可删除
489
+ - 若旧 profile 无其他引用但仍是 active profile:必须先切换或交互确认后显式切换
490
+
491
+ ### 10.3 `remove <provider>`
492
+
493
+ 规则锁定如下:
494
+
495
+ - 若删除后 profile 仍被其他 provider 引用:保留 section
496
+ - 若删除后已无任何 provider 引用:允许删除 section
497
+ - 若会删掉当前 active profile 且这是最后一个引用:必须先 `switch` 或显式 `--switch-to`
498
+ - 不允许把 active profile 留成悬空状态
499
+
500
+ 这里新增明确错误码:
501
+
502
+ - `PROFILE_IN_USE`:对共享 profile 或 active profile 进行危险删除时阻止继续
503
+
504
+ ### 10.4 `switch <provider>`
505
+
506
+ `switch` 在 `0.0.5` 继续只负责:
507
+
508
+ - 修改顶层 active `profile`
509
+
510
+ 它不负责:
511
+
512
+ - 修复 profile section 内容
513
+ - 创建缺失 section
514
+ - 迁移 profile schema
515
+
516
+ ### 10.5 `setup`
517
+
518
+ `setup` 在 `0.0.5` 的要求:
519
+
520
+ - 支持多候选目录发现与 TTY 选择
521
+ - 支持 adopt 现有 unmanaged profiles
522
+ - 不要求一次性把所有历史 profile 全部变为 managed
523
+ - 不能制造新的 registry-config 不一致
524
+
525
+ 当发现 unmanaged profile 时:
526
+
527
+ - 交互模式可选择 adopt 或跳过
528
+ - 非交互模式不得静默 adopt;只有在输入已足够明确时才继续
529
+
530
+ ### 10.6 `import --merge`
531
+
532
+ `import --merge` 继续保持“导入侧覆盖本地同名 provider”的语义。
533
+
534
+ 新增一致性规则:
535
+
536
+ - 如果导入结果引用缺失 profile,进入与 `add` / `edit` 相同的 create / adopt 规则
537
+ - 非交互模式下,无法满足 create 条件则失败
538
+ - 交互模式下,可进入 adopt / repair 辅助流
539
+ - 最终写入结果不得留下新的 orphaned reference
540
+
541
+ ## 11. 写结果契约
542
+
543
+ `0.0.5` 要求以下写命令在 JSON `data` 中稳定返回新字段:
544
+
545
+ - `add`
546
+ - `edit`
547
+ - `remove`
548
+ - `setup`
549
+ - `import --merge`
550
+
551
+ 新增字段:
552
+
553
+ ```json
554
+ {
555
+ "createdProfileSections": [],
556
+ "deletedProfileSections": [],
557
+ "keptSharedProfiles": [],
558
+ "switchedActiveProfile": false,
559
+ "adoptedProfiles": [],
560
+ "repairedProfiles": []
561
+ }
562
+ ```
563
+
564
+ 约束:
565
+
566
+ - 未发生时返回空数组或 `false`
567
+ - 不用缺省省略字段
568
+ - 顶层 envelope 结构不变
569
+
570
+ ## 12. 错误语义
571
+
572
+ ### 12.1 `0.0.5` 需要新增的错误码
573
+
574
+ 设计明确要求实现中新增:
575
+
576
+ - `CONFIG_PARSE_ERROR`
577
+ - `PROFILE_IN_USE`
578
+ - `MANAGED_PROFILE_FIELDS_MISSING`
579
+
580
+ 继续使用已有:
581
+
582
+ - `CODEX_DIR_NOT_FOUND`
583
+ - `CODEX_DIR_AMBIGUOUS`
584
+ - `CONFIG_NOT_FOUND`
585
+ - `PROFILE_NOT_FOUND`
586
+ - `PROVIDERS_NOT_FOUND`
587
+ - `PROVIDERS_PARSE_ERROR`
588
+ - `BACKUP_FAILED`
589
+ - `ROLLBACK_FAILED`
590
+
591
+ ### 12.2 错误码语义
592
+
593
+ `CONFIG_PARSE_ERROR`:
594
+
595
+ - `config.toml` 存在,但结构化读取失败
596
+ - 应包含文件路径和 parser 原因
597
+
598
+ `PROFILE_IN_USE`:
599
+
600
+ - 删除 / 改绑操作会破坏共享 profile 或 active profile 安全约束
601
+ - 应包含 `profile`、`provider`、`activeProfile`、`linkedProviders`
602
+
603
+ `MANAGED_PROFILE_FIELDS_MISSING`:
604
+
605
+ - 需要创建新的受管 profile section,但缺少最小字段
606
+ - `0.0.5` 至少用于缺少 `model` 或 `base_url`
607
+
608
+ ### 12.3 明确不进入 `0.0.5` 的错误语义
609
+
610
+ `IMPORT_MERGE_CONFLICT` 不进入 `0.0.5`。
611
+
612
+ 原因:
613
+
614
+ - `import --merge` 继续采用导入侧覆盖策略
615
+ - 不引入逐条冲突解决语义
616
+
617
+ ## 13. 模块设计与代码落点
618
+
619
+ ### 13.1 总体结构
620
+
621
+ 保持当前四层结构不变:
622
+
623
+ - CLI 层
624
+ - Application 层
625
+ - Domain 层
626
+ - Infrastructure 层
627
+
628
+ ### 13.2 `src/domain/config.ts`
629
+
630
+ 当前 `src/domain/config.ts` 主要还是字符串 helper。
631
+
632
+ `0.0.5` 升级为 config 领域规则入口,负责:
633
+
634
+ - active profile 规则
635
+ - managed / unmanaged / orphaned 视图拼装
636
+ - 写操作前校验
637
+ - shared profile 与 destructive remove 规则判断
638
+
639
+ 建议新增:
640
+
641
+ - `buildManagedProfileViews(...)`
642
+ - `collectConfigConsistencyIssues(...)`
643
+ - `validateManagedProfileCreation(...)`
644
+ - `planProfileLifecycleOutcome(...)`
645
+
646
+ ### 13.3 `src/infra/config-repo.ts`
647
+
648
+ 当前 `src/infra/config-repo.ts` 主要负责读取 active profile、列出 section 名、改写顶层 profile。
649
+
650
+ `0.0.5` 升级为结构化读取 + patch 应用中心,至少暴露:
651
+
652
+ - `readStructuredConfig`
653
+ - `listStructuredProfiles`
654
+ - `planConfigMutation`
655
+ - `applyConfigMutation`
656
+ - `findCodexDirCandidates`
657
+
658
+ 职责包括:
659
+
660
+ - 读取原始 `config.toml`
661
+ - 通过 CST parser 建立 section / field 边界
662
+ - 生成 `ConfigMutationPlan`
663
+ - 应用局部 patch
664
+ - 保持注释、空行和未受管内容稳定
665
+
666
+ 说明:
667
+
668
+ - 当前仓库已有 `src/infra/codex-discovery.ts` 的目录发现逻辑
669
+ - `0.0.5` 设计上允许把候选目录规则下沉整合进 config-aware 路径解析,但不要求强行删除旧文件
670
+ - 实现时可以保留 `codex-discovery.ts` 作为薄封装,底层委托给 `config-repo` 或共用 helper
671
+
672
+ ### 13.4 `src/app/`
673
+
674
+ 新增:
675
+
676
+ - `src/app/show-config.ts`
677
+ - `src/app/list-config-profiles.ts`
678
+
679
+ 更新:
680
+
681
+ - `src/app/add-provider.ts`
682
+ - `src/app/edit-provider.ts`
683
+ - `src/app/remove-provider.ts`
684
+ - `src/app/setup-codex.ts`
685
+ - `src/app/import-providers.ts`
686
+ - `src/app/get-status.ts`
687
+ - `src/app/run-doctor.ts`
688
+
689
+ 应用层需要承担:
690
+
691
+ - 组合 provider repo 与 config repo
692
+ - 组装双写事务输入
693
+ - 生成写结果契约字段
694
+ - 把 doctor / status 的 issue 转换为稳定输出结构
695
+
696
+ ### 13.5 `src/cli.ts`
697
+
698
+ 需要新增:
699
+
700
+ - `config show` 分派
701
+ - `config list-profiles` 分派
702
+
703
+ 同时更新现有:
704
+
705
+ - `add` 参数接收 `--create-profile`、`--model`、`--base-url`
706
+ - `edit` 参数接收 `--create-profile`、`--model`、`--base-url`
707
+ - `remove` 参数接收 `--switch-to`
708
+ - `setup` 接入多候选目录交互
709
+
710
+ ### 13.6 `src/cli/args.ts`
711
+
712
+ 需要把新子命令归一化为稳定 command key,方式与现有 `backups list` 类似。
713
+
714
+ 建议新增 command key:
715
+
716
+ - `config-show`
717
+ - `config-list-profiles`
718
+
719
+ ### 13.7 `src/cli/help.ts` / `src/cli/output.ts` / `src/cli/interactive.ts`
720
+
721
+ 需要增加:
722
+
723
+ - 新命令帮助文案
724
+ - `config show` / `config list-profiles` 的文本渲染
725
+ - `setup` 多候选目录选择交互
726
+ - adopt / repair 辅助交互的最小提示
727
+
728
+ ## 14. 关键流程时序
729
+
730
+ ### 14.1 `config show` 只读流程
731
+
732
+ ```text
733
+ argv
734
+ -> parseArgs
735
+ -> executeCommand("config-show")
736
+ -> app/show-config
737
+ -> infra/config-repo.readStructuredConfig
738
+ -> infra/providers-repo.readProvidersFileIfExists
739
+ -> domain/config.buildManagedProfileViews
740
+ -> output
741
+ ```
742
+
743
+ ### 14.2 `add` 创建缺失 profile 的双写流程
744
+
745
+ ```text
746
+ argv
747
+ -> parseArgs
748
+ -> executeCommand("add")
749
+ -> app/add-provider
750
+ -> read providers.json + structured config
751
+ -> validate create-profile + model/base_url preconditions
752
+ -> domain/config.planProfileLifecycleOutcome
753
+ -> infra/config-repo.planConfigMutation
754
+ -> app/run-mutation
755
+ -> write providers.json + apply config patch
756
+ -> success result with createdProfileSections
757
+ ```
758
+
759
+ ### 14.3 `edit --profile` 重绑定流程
760
+
761
+ ```text
762
+ argv
763
+ -> parseArgs
764
+ -> executeCommand("edit")
765
+ -> app/edit-provider
766
+ -> load provider + structured config
767
+ -> resolve new profile target
768
+ -> create missing profile only when --create-profile --model --base-url is present
769
+ -> update provider mapping
770
+ -> keep or delete old section based on shared/active rules
771
+ -> single mutation transaction
772
+ -> success result with created/deleted/kept fields
773
+ ```
774
+
775
+ ### 14.4 `remove --switch-to` 安全删除流程
776
+
777
+ ```text
778
+ argv
779
+ -> parseArgs
780
+ -> executeCommand("remove")
781
+ -> app/remove-provider
782
+ -> load provider + structured config
783
+ -> detect whether target profile is last reference and active
784
+ -> require explicit switch target when destructive
785
+ -> switch active profile first in mutation plan when needed
786
+ -> delete provider mapping
787
+ -> delete profile section only if no remaining references
788
+ -> success result with switchedActiveProfile + deletedProfileSections
789
+ ```
790
+
791
+ ### 14.5 `setup` 多候选目录 + adopt 流程
792
+
793
+ ```text
794
+ argv
795
+ -> parseArgs
796
+ -> executeCommand("setup")
797
+ -> findCodexDirCandidates
798
+ -> TTY choose candidate or manual path
799
+ -> read structured config
800
+ -> collect unmanaged profiles and active profile
801
+ -> interactive adopt decision when applicable
802
+ -> build provider drafts
803
+ -> single mutation write
804
+ -> run doctor
805
+ -> output adopt/repair summary
806
+ ```
807
+
808
+ ## 15. 兼容、迁移与诊断
809
+
810
+ ### 15.1 历史状态识别
811
+
812
+ `0.0.5` 需要识别至少四类历史状态:
813
+
814
+ - `providers.json` 可用,但 `config.toml` 只有手工维护 profile
815
+ - provider 引用了缺失 section
816
+ - `config.toml` 存在没有任何 provider 引用的历史 section
817
+ - 当前 active profile 指向 unmanaged profile
818
+
819
+ ### 15.2 收敛路线
820
+
821
+ 在没有独立 `repair` 命令的前提下,收敛路线明确为:
822
+
823
+ - `doctor` 识别问题并给出问题码
824
+ - `status` 给出浅层信号,不做静默修复
825
+ - `setup` / `import --merge` 在交互模式下承接 adopt / repair
826
+ - 非交互模式遇到不可自动收敛状态时失败,并返回明确原因
827
+
828
+ ### 15.3 `doctor` 与 `status` 的责任差异
829
+
830
+ `status`:
831
+
832
+ - 给出当前 active profile、是否映射、是否存在浅层漂移信号
833
+ - 输出面向日常查看
834
+
835
+ `doctor`:
836
+
837
+ - 给出结构化 `ConfigConsistencyIssue[]`
838
+ - 明确问题码、上下文和建议动作
839
+ - 输出面向修复和自动化诊断
840
+
841
+ ## 16. 测试设计
842
+
843
+ ### 16.1 总体原则
844
+
845
+ 测试继续采用当前 plain Node specs 模式:
846
+
847
+ - 不引入 Jest / Vitest
848
+ - fixture 继续放在 `tests/` / `dev-codex/` 现有模式中
849
+
850
+ ### 16.2 CLI 测试
851
+
852
+ 最少覆盖:
853
+
854
+ - `config show` 文本输出
855
+ - `config show --json` 输出
856
+ - `config list-profiles` 文本输出
857
+ - `config list-profiles --json` 输出
858
+ - `setup` 单候选目录
859
+ - `setup` 多候选目录 + TTY 选择
860
+ - `setup` 无候选目录 + TTY 手动输入
861
+ - `setup` 多候选目录 + 非交互失败 `CODEX_DIR_AMBIGUOUS`
862
+ - `add --create-profile --model --base-url`
863
+ - `edit --profile <missing> --create-profile --model --base-url`
864
+ - `remove --switch-to`
865
+
866
+ ### 16.3 Application 测试
867
+
868
+ 最少覆盖:
869
+
870
+ - provider + config 双写成功
871
+ - provider + config 双写失败整体回滚
872
+ - `import --merge` 后 linked section 一致性
873
+ - `setup` adopt unmanaged profile
874
+
875
+ ### 16.4 Domain / Infra 测试
876
+
877
+ 最少覆盖:
878
+
879
+ - structured config 读取
880
+ - patch 计划生成
881
+ - patch 应用后注释、空行、未受管内容保持
882
+ - managed / unmanaged / orphaned 视图计算
883
+ - `doctor` / `status` issue 计算
884
+
885
+ ### 16.5 Fixture 设计
886
+
887
+ 最少新增:
888
+
889
+ - 带注释和空行的 `config.toml`
890
+ - 共享 profile fixture
891
+ - orphaned reference fixture
892
+ - unmanaged active profile fixture
893
+ - 多候选 Codex 目录 fixture
894
+
895
+ ## 17. Deferred 到 `0.1.0`
896
+
897
+ 下面这些内容作为 `0.0.5 -> 0.1.0` 后续项单列,不混入当前实现:
898
+
899
+ - 更大的 profile schema
900
+ - 真正的 `config edit` 命令族
901
+ - 更强的 repair 自动化
902
+ - extensions / auth integration
903
+
904
+ ## 18. 验收标准
905
+
906
+ `0.0.5` 设计落地后,至少应满足:
907
+
908
+ - `config show` 在文本和 `--json` 模式下返回稳定结构
909
+ - `config list-profiles` 返回全部可识别 profiles,并通过 `managed` / `source` 区分来源
910
+ - `add` / `edit` / `remove` 同步维护 linked profile sections
911
+ - 共享 profile 不会因单个 provider 操作被误删
912
+ - active profile 不会因删除或重绑定而悬空
913
+ - `setup` 的目录发现行为在 TTY / 非交互下可预测
914
+ - 结构化 TOML 写回后注释、空行和未受管内容保持稳定
915
+ - 双写失败时 `providers.json` 与 `config.toml` 能整体回滚
916
+ - `doctor` / `status` 能识别 orphaned reference、unmanaged active profile、shared profile 和 orphaned section
917
+
918
+ ## 19. 结论
919
+
920
+ `0.0.5` 的本质,是把 `codex-switch` 从“能管理 provider registry 并做浅层 profile 切换”的工具,推进到“对 `config.toml` 有稳定结构化认知、能维护 provider-linked profile、一致性可诊断、双写可回滚”的下一阶段。
921
+
922
+ 这一步不要求引入更大的命令体系,也不要求把 `config.toml` 变成通用编辑器;它要求的是把当前最容易产生漂移和隐式破坏的那部分能力,收敛成一套明确、可实现、可测试的实现规格。