cckit 0.3.2__tar.gz → 0.3.3__tar.gz

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 (155) hide show
  1. {cckit-0.3.2 → cckit-0.3.3}/Docs/02-architecture.md +1 -1
  2. {cckit-0.3.2 → cckit-0.3.3}/Docs/05-design-log.md +44 -0
  3. {cckit-0.3.2 → cckit-0.3.3}/Docs/06-platform-findings.md +22 -0
  4. {cckit-0.3.2 → cckit-0.3.3}/Docs/11-web-panel.md +256 -256
  5. {cckit-0.3.2 → cckit-0.3.3}/Docs/README.md +152 -134
  6. {cckit-0.3.2 → cckit-0.3.3}/Docs/cli-spec.md +22 -1
  7. {cckit-0.3.2 → cckit-0.3.3}/PKG-INFO +2 -2
  8. {cckit-0.3.2 → cckit-0.3.3}/README.md +1 -1
  9. {cckit-0.3.2 → cckit-0.3.3}/pyproject.toml +45 -45
  10. {cckit-0.3.2 → cckit-0.3.3}/src/cckit/__init__.py +8 -8
  11. {cckit-0.3.2 → cckit-0.3.3}/src/cckit/alt.py +11 -6
  12. {cckit-0.3.2 → cckit-0.3.3}/src/cckit/doctor.py +38 -0
  13. cckit-0.3.3/src/cckit/fsutil.py +39 -0
  14. {cckit-0.3.2 → cckit-0.3.3}/src/cckit/installer.py +82 -12
  15. {cckit-0.3.2 → cckit-0.3.3}/src/cckit/state.py +29 -0
  16. {cckit-0.3.2 → cckit-0.3.3}/src/cckit/web/app.py +712 -713
  17. {cckit-0.3.2 → cckit-0.3.3}/tests/test_alt.py +22 -1
  18. cckit-0.3.3/tests/test_fsutil.py +60 -0
  19. {cckit-0.3.2 → cckit-0.3.3}/tests/test_installer.py +177 -0
  20. {cckit-0.3.2 → cckit-0.3.3}/uv.lock +1 -1
  21. cckit-0.3.2/web/dist/assets/index-VQNfSAQl.js → cckit-0.3.3/web/dist/assets/index-BHbLqx9G.js +1 -1
  22. {cckit-0.3.2 → cckit-0.3.3}/web/dist/index.html +1 -1
  23. {cckit-0.3.2 → cckit-0.3.3}/web/package-lock.json +2 -2
  24. {cckit-0.3.2 → cckit-0.3.3}/web/package.json +1 -1
  25. {cckit-0.3.2 → cckit-0.3.3}/web/src/components/AppSidebar.tsx +222 -222
  26. {cckit-0.3.2 → cckit-0.3.3}/.gitignore +0 -0
  27. {cckit-0.3.2 → cckit-0.3.3}/Docs/.gitignore +0 -0
  28. {cckit-0.3.2 → cckit-0.3.3}/Docs/01-overview.md +0 -0
  29. {cckit-0.3.2 → cckit-0.3.3}/Docs/03-manifest-spec.md +0 -0
  30. {cckit-0.3.2 → cckit-0.3.3}/Docs/04-cli-spec.md +0 -0
  31. {cckit-0.3.2 → cckit-0.3.3}/Docs/07-security.md +0 -0
  32. {cckit-0.3.2 → cckit-0.3.3}/Docs/08-installation.md +0 -0
  33. {cckit-0.3.2 → cckit-0.3.3}/Docs/09-state-api.md +0 -0
  34. {cckit-0.3.2 → cckit-0.3.3}/Docs/10-kit-authoring.md +0 -0
  35. {cckit-0.3.2 → cckit-0.3.3}/Docs/examples/video-toolkit/cckit.yaml +0 -0
  36. {cckit-0.3.2 → cckit-0.3.3}/Docs/kit-development.md +0 -0
  37. {cckit-0.3.2 → cckit-0.3.3}/Docs/pics/architecture.md +0 -0
  38. {cckit-0.3.2 → cckit-0.3.3}/Docs/pics/directory-layout.md +0 -0
  39. {cckit-0.3.2 → cckit-0.3.3}/Docs/pics/four-state-model.md +0 -0
  40. {cckit-0.3.2 → cckit-0.3.3}/Docs/pics/install-flow.md +0 -0
  41. {cckit-0.3.2 → cckit-0.3.3}/Docs/pics/pain-points-solution.md +0 -0
  42. {cckit-0.3.2 → cckit-0.3.3}/Docs/pics/state-transitions.md +0 -0
  43. {cckit-0.3.2 → cckit-0.3.3}/Docs/schema/cckit.schema.json +0 -0
  44. {cckit-0.3.2 → cckit-0.3.3}/Docs/test/README.md +0 -0
  45. {cckit-0.3.2 → cckit-0.3.3}/Docs/test/hello-skill/SKILL.md +0 -0
  46. {cckit-0.3.2 → cckit-0.3.3}/Docs/test/test-skill-dependency/.gitignore +0 -0
  47. {cckit-0.3.2 → cckit-0.3.3}/Docs/test/test-skill-dependency/README.md +0 -0
  48. {cckit-0.3.2 → cckit-0.3.3}/Docs/test/test-skill-dependency/json-pretty/SKILL.md +0 -0
  49. {cckit-0.3.2 → cckit-0.3.3}/Docs/test/test-skill-dependency/json-pretty/pretty.config.json +0 -0
  50. {cckit-0.3.2 → cckit-0.3.3}/Docs/test/test-skill-dependency/json-pretty/scripts/pretty.js +0 -0
  51. {cckit-0.3.2 → cckit-0.3.3}/Docs/test/test-skill-dependency/package.json +0 -0
  52. {cckit-0.3.2 → cckit-0.3.3}/Docs/test/test-skill-dependency/plain-advisor/SKILL.md +0 -0
  53. {cckit-0.3.2 → cckit-0.3.3}/Docs/test/test-skill-dependency/requirements.txt +0 -0
  54. {cckit-0.3.2 → cckit-0.3.3}/Docs/test/test-skill-dependency/test-skill-dependency/SKILL.md +0 -0
  55. {cckit-0.3.2 → cckit-0.3.3}/Docs/test/test-skill-dependency/test-skill-dependency/config.json +0 -0
  56. {cckit-0.3.2 → cckit-0.3.3}/Docs/test/test-skill-dependency/test-skill-dependency/scripts/test.py +0 -0
  57. {cckit-0.3.2 → cckit-0.3.3}/Docs/test/test-skill-dependency-converted/.gitignore +0 -0
  58. {cckit-0.3.2 → cckit-0.3.3}/Docs/test/test-skill-dependency-converted/README.md +0 -0
  59. {cckit-0.3.2 → cckit-0.3.3}/Docs/test/test-skill-dependency-converted/cckit.yaml +0 -0
  60. {cckit-0.3.2 → cckit-0.3.3}/Docs/test/test-skill-dependency-converted/json-pretty/SKILL.md +0 -0
  61. {cckit-0.3.2 → cckit-0.3.3}/Docs/test/test-skill-dependency-converted/json-pretty/pretty.config.json +0 -0
  62. {cckit-0.3.2 → cckit-0.3.3}/Docs/test/test-skill-dependency-converted/json-pretty/scripts/pretty.js +0 -0
  63. {cckit-0.3.2 → cckit-0.3.3}/Docs/test/test-skill-dependency-converted/package.json +0 -0
  64. {cckit-0.3.2 → cckit-0.3.3}/Docs/test/test-skill-dependency-converted/plain-advisor/SKILL.md +0 -0
  65. {cckit-0.3.2 → cckit-0.3.3}/Docs/test/test-skill-dependency-converted/requirements.txt +0 -0
  66. {cckit-0.3.2 → cckit-0.3.3}/Docs/test/test-skill-dependency-converted/test-skill-dependency/SKILL.md +0 -0
  67. {cckit-0.3.2 → cckit-0.3.3}/Docs/test/test-skill-dependency-converted/test-skill-dependency/config.json +0 -0
  68. {cckit-0.3.2 → cckit-0.3.3}/Docs/test/test-skill-dependency-converted/test-skill-dependency/scripts/test.py +0 -0
  69. {cckit-0.3.2 → cckit-0.3.3}/Docs/test/test_notice.txt +0 -0
  70. {cckit-0.3.2 → cckit-0.3.3}/LICENSE +0 -0
  71. {cckit-0.3.2 → cckit-0.3.3}/hatch_build.py +0 -0
  72. {cckit-0.3.2 → cckit-0.3.3}/skills/kit-builder/cckit.yaml +0 -0
  73. {cckit-0.3.2 → cckit-0.3.3}/skills/kit-builder/kit-builder/SKILL.md +0 -0
  74. {cckit-0.3.2 → cckit-0.3.3}/skills/kit-builder/kit-builder/kit-authoring.md +0 -0
  75. {cckit-0.3.2 → cckit-0.3.3}/skills/kit-builder/kit-builder/manifest-spec.md +0 -0
  76. {cckit-0.3.2 → cckit-0.3.3}/skills/kit-builder/kit-builder/templates/SKILL.md +0 -0
  77. {cckit-0.3.2 → cckit-0.3.3}/skills/kit-builder/kit-builder/templates/cckit.yaml +0 -0
  78. {cckit-0.3.2 → cckit-0.3.3}/src/cckit/__main__.py +0 -0
  79. {cckit-0.3.2 → cckit-0.3.3}/src/cckit/cckit.schema.json +0 -0
  80. {cckit-0.3.2 → cckit-0.3.3}/src/cckit/cli.py +0 -0
  81. {cckit-0.3.2 → cckit-0.3.3}/src/cckit/config.py +0 -0
  82. {cckit-0.3.2 → cckit-0.3.3}/src/cckit/env.py +0 -0
  83. {cckit-0.3.2 → cckit-0.3.3}/src/cckit/errors.py +0 -0
  84. {cckit-0.3.2 → cckit-0.3.3}/src/cckit/exec.py +0 -0
  85. {cckit-0.3.2 → cckit-0.3.3}/src/cckit/link.py +0 -0
  86. {cckit-0.3.2 → cckit-0.3.3}/src/cckit/lint.py +0 -0
  87. {cckit-0.3.2 → cckit-0.3.3}/src/cckit/manifest.py +0 -0
  88. {cckit-0.3.2 → cckit-0.3.3}/src/cckit/projects.py +0 -0
  89. {cckit-0.3.2 → cckit-0.3.3}/src/cckit/registry.py +0 -0
  90. {cckit-0.3.2 → cckit-0.3.3}/src/cckit/schema.py +0 -0
  91. {cckit-0.3.2 → cckit-0.3.3}/src/cckit/web/__init__.py +0 -0
  92. {cckit-0.3.2 → cckit-0.3.3}/tests/conftest.py +0 -0
  93. {cckit-0.3.2 → cckit-0.3.3}/tests/helpers.py +0 -0
  94. {cckit-0.3.2 → cckit-0.3.3}/tests/test_budget.py +0 -0
  95. {cckit-0.3.2 → cckit-0.3.3}/tests/test_env.py +0 -0
  96. {cckit-0.3.2 → cckit-0.3.3}/tests/test_exec.py +0 -0
  97. {cckit-0.3.2 → cckit-0.3.3}/tests/test_link.py +0 -0
  98. {cckit-0.3.2 → cckit-0.3.3}/tests/test_lint.py +0 -0
  99. {cckit-0.3.2 → cckit-0.3.3}/tests/test_manifest.py +0 -0
  100. {cckit-0.3.2 → cckit-0.3.3}/tests/test_registry.py +0 -0
  101. {cckit-0.3.2 → cckit-0.3.3}/tests/test_state.py +0 -0
  102. {cckit-0.3.2 → cckit-0.3.3}/tests/test_web_alt.py +0 -0
  103. {cckit-0.3.2 → cckit-0.3.3}/tests/test_web_base.py +0 -0
  104. {cckit-0.3.2 → cckit-0.3.3}/tests/test_web_config.py +0 -0
  105. {cckit-0.3.2 → cckit-0.3.3}/tests/test_web_notice.py +0 -0
  106. {cckit-0.3.2 → cckit-0.3.3}/web/.gitignore +0 -0
  107. {cckit-0.3.2 → cckit-0.3.3}/web/components.json +0 -0
  108. {cckit-0.3.2 → cckit-0.3.3}/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  109. {cckit-0.3.2 → cckit-0.3.3}/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  110. {cckit-0.3.2 → cckit-0.3.3}/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  111. {cckit-0.3.2 → cckit-0.3.3}/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  112. {cckit-0.3.2 → cckit-0.3.3}/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  113. {cckit-0.3.2 → cckit-0.3.3}/web/dist/assets/index-DyKMbG6Y.css +0 -0
  114. {cckit-0.3.2 → cckit-0.3.3}/web/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  115. {cckit-0.3.2 → cckit-0.3.3}/web/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  116. {cckit-0.3.2 → cckit-0.3.3}/web/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  117. {cckit-0.3.2 → cckit-0.3.3}/web/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  118. {cckit-0.3.2 → cckit-0.3.3}/web/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  119. {cckit-0.3.2 → cckit-0.3.3}/web/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  120. {cckit-0.3.2 → cckit-0.3.3}/web/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  121. {cckit-0.3.2 → cckit-0.3.3}/web/index.html +0 -0
  122. {cckit-0.3.2 → cckit-0.3.3}/web/src/App.tsx +0 -0
  123. {cckit-0.3.2 → cckit-0.3.3}/web/src/components/AddKitDialog.tsx +0 -0
  124. {cckit-0.3.2 → cckit-0.3.3}/web/src/components/AddScopeDialog.tsx +0 -0
  125. {cckit-0.3.2 → cckit-0.3.3}/web/src/components/DoctorDialog.tsx +0 -0
  126. {cckit-0.3.2 → cckit-0.3.3}/web/src/components/KitList.tsx +0 -0
  127. {cckit-0.3.2 → cckit-0.3.3}/web/src/components/NoticeMenu.tsx +0 -0
  128. {cckit-0.3.2 → cckit-0.3.3}/web/src/components/ui/badge.tsx +0 -0
  129. {cckit-0.3.2 → cckit-0.3.3}/web/src/components/ui/button.tsx +0 -0
  130. {cckit-0.3.2 → cckit-0.3.3}/web/src/components/ui/card.tsx +0 -0
  131. {cckit-0.3.2 → cckit-0.3.3}/web/src/components/ui/dialog.tsx +0 -0
  132. {cckit-0.3.2 → cckit-0.3.3}/web/src/components/ui/dropdown-menu.tsx +0 -0
  133. {cckit-0.3.2 → cckit-0.3.3}/web/src/components/ui/input.tsx +0 -0
  134. {cckit-0.3.2 → cckit-0.3.3}/web/src/components/ui/popover.tsx +0 -0
  135. {cckit-0.3.2 → cckit-0.3.3}/web/src/components/ui/progress.tsx +0 -0
  136. {cckit-0.3.2 → cckit-0.3.3}/web/src/components/ui/separator.tsx +0 -0
  137. {cckit-0.3.2 → cckit-0.3.3}/web/src/components/ui/sheet.tsx +0 -0
  138. {cckit-0.3.2 → cckit-0.3.3}/web/src/components/ui/sidebar.tsx +0 -0
  139. {cckit-0.3.2 → cckit-0.3.3}/web/src/components/ui/skeleton.tsx +0 -0
  140. {cckit-0.3.2 → cckit-0.3.3}/web/src/components/ui/sonner.tsx +0 -0
  141. {cckit-0.3.2 → cckit-0.3.3}/web/src/components/ui/textarea.tsx +0 -0
  142. {cckit-0.3.2 → cckit-0.3.3}/web/src/components/ui/tooltip.tsx +0 -0
  143. {cckit-0.3.2 → cckit-0.3.3}/web/src/hooks/use-mobile.ts +0 -0
  144. {cckit-0.3.2 → cckit-0.3.3}/web/src/hooks/useSSE.ts +0 -0
  145. {cckit-0.3.2 → cckit-0.3.3}/web/src/index.css +0 -0
  146. {cckit-0.3.2 → cckit-0.3.3}/web/src/lib/api.ts +0 -0
  147. {cckit-0.3.2 → cckit-0.3.3}/web/src/lib/utils.ts +0 -0
  148. {cckit-0.3.2 → cckit-0.3.3}/web/src/main.tsx +0 -0
  149. {cckit-0.3.2 → cckit-0.3.3}/web/tsconfig.json +0 -0
  150. {cckit-0.3.2 → cckit-0.3.3}/web/tsconfig.node.json +0 -0
  151. {cckit-0.3.2 → cckit-0.3.3}/web/tsconfig.node.tsbuildinfo +0 -0
  152. {cckit-0.3.2 → cckit-0.3.3}/web/tsconfig.tsbuildinfo +0 -0
  153. {cckit-0.3.2 → cckit-0.3.3}/web/vite.config.d.ts +0 -0
  154. {cckit-0.3.2 → cckit-0.3.3}/web/vite.config.js +0 -0
  155. {cckit-0.3.2 → cckit-0.3.3}/web/vite.config.ts +0 -0
@@ -17,7 +17,7 @@
17
17
 
18
18
  ```
19
19
  ~/.cckit/ # 可被 CCKIT_HOME 覆盖
20
- store/<kit>/ # git clone 的真实内容,含 cckit.yaml
20
+ store/<kit>/ # kit 的真实文件树,含 cckit.yaml(不含 .git)
21
21
  <skill>/SKILL.md
22
22
  envs/<kit>__<skill>/ # 每 skill 一个独立环境
23
23
  registry.json # 装了什么、装在哪、什么版本
@@ -516,6 +516,49 @@ Web 上都没有填写的入口,粒度还和 `needs`(skill 级)不一致。
516
516
  后者不是 cckit 管的。UI 禁用之外,服务端也重新校验一遍——UI 禁用了不代表可以信任
517
517
  客户端传来的请求。
518
518
 
519
+ ## D-18 卸载:删除必须可验证,store 不含 `.git`
520
+
521
+ **提出的问题**(用户报告):从远程仓库装好后,远程更新了,再次 `cckit add` 被提示
522
+ "已安装(store 已存在),请先 `cckit remove`";`remove` 跑完看似成功,store 里却留下
523
+ 一个 `.git`,于是 `add` 永远装不上,而 `remove` 这时又说"未安装"——两头堵死。
524
+
525
+ **根因**:两处叠加,单独任一处都不会致命。
526
+
527
+ 1. `installer._clone` 把 clone 的 `.git` 一起搬进了 store。
528
+ 2. 删 store 用 `shutil.rmtree(ignore_errors=True)`,而 git for Windows 把
529
+ `.git/objects/**` 标为只读,`os.unlink` 抛 PermissionError 被这个 flag 吞掉:
530
+ **报告成功,目录还在**(实测见 [06](06-platform-findings.md) 4.1)。
531
+ registry 已先被清掉,于是这个残留再没有任何人能定位到它。
532
+
533
+ **最终方案**:四条一起上,少一条这个问题都会换个形式回来。
534
+
535
+ 1. **store 不含 `.git`**:`_clone` 落盘前剥离。来源与 sha 已记进 registry,git 历史
536
+ 对安装/卸载都没有用处。alt 流程一直如此(cli-spec 已写"总是剥离 `.git`"),
537
+ 标准流程现在对齐,`_clone` 与 `alt.materialize` 行为一致。
538
+ 2. **删目录统一走 `fsutil.rmtree`**:失败回调里清只读位后重试;`ignore_errors` 只
539
+ 决定"重试还失败时是否抛出",再也没有"静默跳过只读文件"这条路径。
540
+ 3. **remove 不许谎报成功**:删不掉就抛受检错误并**保留 registry**,让用户排掉占用
541
+ 后重跑;并且当 registry 无记录、store 却有同名目录时,`remove <kit>` 直接清掉它
542
+ (连同指向它的 link、按 `<kit>__*` 命名的 env),给出唯一出路。`doctor` 增加一条
543
+ 同名残留检查,但**只报告不自动修**——registry.json 若被人为删掉,自动清等于清空
544
+ store。
545
+ 4. **删 store 前按目标路径反查 link**(`state.links_into`)。registry 的
546
+ `known_scopes` + skill 名只管得住记录在案的 link;记录丢失或用户在别的项目根手工
547
+ 建的 link 会留成悬空 link —— 而悬空 link 同样占住 skill 名,下一次 `add` 照样被挡。
548
+
549
+ **为什么这样定**:
550
+
551
+ - **store 是 cckit 的私有缓存,不是工作副本。** "远程更新后重装" = remove + add,
552
+ 用户不需要在 store 里 `git pull`;保留 `.git` 只买到只读文件与磁盘占用两个成本。
553
+ - **静默失败比失败更贵。** 这次的代价不是"少删一个目录",而是状态自相矛盾:一个
554
+ 说装了、一个说没装,用户没有任何可自证的路径。宁可当场报错。
555
+ - **不做"删除前先备份"之类的补偿。** store 里的内容随时可由 registry 记录的
556
+ URL+sha 重新拉取,为它做备份机制是把简单问题复杂化。
557
+
558
+ **代价(必须记录)**:store 里不再能 `git log`(查装了什么版本看 `cckit list` 或
559
+ registry);`remove` 现在可能因文件被占用而失败并留下半清理状态,需要重跑一次——
560
+ 这是刻意的取舍,失败会明确说出来,而不是留在那里等人踩。
561
+
519
562
  ## 我的判断失误汇总
520
563
 
521
564
  集中列出,便于后续开发者校准对本文档其余部分的信任度:
@@ -529,6 +572,7 @@ Web 上都没有填写的入口,粒度还和 `needs`(skill 级)不一致。
529
572
  | 5 | 建议 subprocess 调 `mklink` | 多余(标准库已有) | D-10 |
530
573
  | 6 | 未抽出状态读写层 | 架构层遗漏,被 Web 需求问出来 | D-13 |
531
574
  | 7 | 未考虑并发写 | 遗漏,且是 Web 下最难定位的一类 bug | D-14 |
575
+ | 8 | 用 `shutil.rmtree(ignore_errors=True)` 删 store,并把 `.git` 一起搬进 store | 静默失败:命令报成功、目录还在,用户无路可走 | D-18 |
532
576
 
533
577
  前三条的共同点:**"我验证过"和"我搜过但没找到"都会伪装成确定性。**
534
578
  对策是[06](06-platform-findings.md)只记录实跑结论,并保留"待验证清单"。
@@ -203,6 +203,28 @@ os.open(O_CREAT|O_EXCL) 加锁 + os.replace → 8 条完整保留
203
203
  - **Windows 保留名**:`con` `prn` `aux` `nul` `com1..9` `lpt1..9` 不能作 skill 名。
204
204
  - **可执行位**:POSIX 下脚本需要 exec bit;若 kit 作者在 Windows 开发可能未设置。
205
205
 
206
+ ### 4.1 ⚠️ 删 `.git` 会静默失败(已实测,Windows)
207
+
208
+ git for Windows 把 `.git/objects/**` 的对象文件标成**只读**,`os.unlink` 抛
209
+ PermissionError;`shutil.rmtree(ignore_errors=True)` 会把这些异常吞掉 ——
210
+ **命令报告成功,目录却还在**。
211
+
212
+ 实测(`git init` → `git clone` → `shutil.rmtree(clone, ignore_errors=True)`):
213
+ 工作树文件全被删掉,只剩 `.git/` 骨架,里面是那批只读对象文件。
214
+ 即"看起来删干净了、其实留了一个 `.git`"。
215
+
216
+ 这就是 `cckit remove` 后 store 里留一个 `.git`、之后 `add` 永远报
217
+ "store 已存在"的成因 —— 而 registry 已经清掉,`remove` 又回一句"未安装",
218
+ 用户两头堵死。
219
+
220
+ 对策(三层,见 `fsutil.rmtree` / `installer.remove_kit`):
221
+
222
+ 1. 删目录统一走 `fsutil.rmtree`:失败回调里 `os.chmod(path, 0o700)` 后重试一次;
223
+ 2. `installer._clone` 落盘前剥离 `.git`(只读对象文件根本不进 store);
224
+ 3. 确实删不掉时**抛错并保留 registry**,绝不"报告成功却留下残留"。
225
+
226
+ `shutil.rmtree(..., ignore_errors=True)` 在本项目里已不再直接使用。
227
+
206
228
  ---
207
229
 
208
230
  ## 5. 待验证清单(实现前必须补)
@@ -1,256 +1,256 @@
1
- # Web 管理面板
2
-
3
- 配套 v0.3.2 的可选管理面板:平时展示 skill 列表,支持导入(add)、卸载(remove)、
4
- 四态开关切换、诊断(doctor)。**纯增量**——不装 `[web]` 额外依赖时,`cckit` 本体
5
- 与命令行工具完全不变。
6
-
7
- ## 定位
8
-
9
- - 目标是**管理界面**,不是运行界面:`exec` 这类"跑 skill"的能力在网页上不做
10
- (服务器上直接跑 `ccb`/Claude Code 即可)。
11
- - 部署形态是**独立面板 + iframe 融合**:它要和一个外部 dashboard(Vite+React,
12
- 非本项目维护)一起上 Linux 服务器,通过 `<iframe>` 嵌入对方页面,因此必须
13
- 独立开发、独立部署、独立端口,不依赖改对方代码。
14
-
15
- ## 技术方案
16
-
17
- | 决策 | 选择 | 理由 |
18
- |---|---|---|
19
- | 仓库 | 当前仓库(`web/` + `src/cckit/web/`) | Web 要动状态层(root 推广)与 installer(两阶段),本就是同一件事;版本天然锁步 |
20
- | 后端 | FastAPI + uvicorn,import `cckit.*` | 状态/安装/诊断逻辑只有一份,Web 不重实现;SSE 流式 |
21
- | 前端 | Vite + React 19 + TypeScript + Tailwind v4 + shadcn/ui | 纯客户端 SPA,无 SSR 需求;shadcn 4.x 用 base-ui 原语 |
22
- | 图标 | @phosphor-icons/react | shadcn 4.x 默认图标库 |
23
- | 数据层 | TanStack Query | 缓存 + 失效即"刷新机制" |
24
- | 提示 | sonner(shadcn 包装) | 四类 toast + 弹窗/toast 分工 |
25
- | 依赖隔离 | `[web]` 可选 extra | `uv tool install cckit` 仍是 3 依赖纯本体 |
26
- | 安全 | 后端默认 `127.0.0.1` | Web 能装 kit(跑作者代码)、能改状态,不放开监听 |
27
- | 子路径部署 | `--base` 运行时挂载 + `window.__CCKIT_BASE__` 注入 + Vite `base: "./"` | 嵌入 dashboard 子路径时避免与宿主 `/api` 冲突,无需重建前端 |
28
-
29
- ## 架构
30
-
31
- ```
32
- 浏览器 (外部 dashboard 内 iframe)
33
- │ http://<server>:<port>/
34
- ▼
35
- FastAPI (src/cckit/web/) ── 静态托管 web/dist/
36
- │ import ├── GET/POST /api/* (JSON)
37
- │ ┌─ cckit.state ├── POST /api/add/execute (SSE)
38
- │ ├─ cckit.installer └── POST /api/doctor (SSE)
39
- │ └─ cckit.doctor
40
- ▼
41
- ~/.cckit/ store + registry.json + projects.json + envs/ + envs.json
42
- <CC 配置目录>/skills + settings.json (状态派生源)
43
- ```
44
-
45
- **核心原则**:状态读写只走 `cckit.state`,安装只走 `cckit.installer`,诊断只走
46
- `cckit.doctor`。Web 后端是这些模块的薄前端,零逻辑重实现,杜绝「改两边」——
47
- 改核心逻辑一处,CLI 与 Web 自动同步;唯一要保证的是核心函数**只返回数据、不发声音**
48
- (不 `print`/`input`),由 CLI/Web 各自渲染。
49
-
50
- ## 后端 API
51
-
52
- | 端点 | 底层调用 | 说明 |
53
- |---|---|---|
54
- | `GET /api/scopes` | `state.list_scopes()` | 侧栏作用域(全局 + 历史项目 + 关注项目) |
55
- | `POST/DELETE /api/scopes` | `projects.add/remove` | 关注/取关项目,add 时建 `.claude` |
56
- | `GET /api/skills?scope=&root=` | `state.list_skills` + `state.budget` | 列表 + 预算一次返回;skill 带 `is_global_skill`/`global_state`/`override`/`envs`/`conf_files`;另含 `kit_env`(按 kit 名索引的 kit 级声明) |
57
- | `POST /api/skills/{name}/state` | `state.set_state` | 四态切换,返回 `{message}` |
58
- | `GET /api/config?scope=&kit=&skill=` | `state.env_requirements` + `state.conf_files` | 配置需求 + 当前值;`skill` 省略时只返回 kit 级(`kit_env`) |
59
- | `POST /api/config/env` | `state.set_user_env` | 保存/清除一个环境变量的值 |
60
- | `GET /api/config/conf?scope=&kit=&skill=&path=` | `state.read_conf_file` | 读一个可修改的配置文件 |
61
- | `PUT /api/config/conf` | `state.write_conf_file` | 写回(服务端校验路径,见下) |
62
- | `GET /api/info` | 读 `--notice` 指定的文件 | 管理员通知正文。**没配 `--notice` 时 404**,前端据此不渲染控件 |
63
- | `GET /api/kits` | `registry.load()` | kit 头部元信息 |
64
- | `POST /api/kits/{kit}/remove` | `installer.remove_kit` | 卸载 |
65
- | `POST /api/add/preview` | `installer.stage_install` | 克隆+校验+计划,存 `preview_id` |
66
- | `POST /api/add/execute` | `installer.execute_install` | 确认后执行,SSE 进度 |
67
- | `DELETE /api/add/preview/{id}` | `StagedInstall.cleanup` | 取消时清理临时目录 |
68
- | `POST /api/doctor` | `doctor.run(fix, on_event)` | SSE 进度 + findings |
69
- | `GET /api/add/alt/check` | `alt.check_sdk_available` + `alt.kit_builder_status` | alt 开关的前置条件检查 |
70
- | `POST /api/add/alt/fix` | `alt.apply_kit_builder_fix` | 修复 kit-builder(安装/切 enabled),重新校验前置条件防冲突/来源不一致 |
71
- | `POST /api/add/alt/start` | `alt.run_alt_conversion`(后台线程) | 三阶段转换,SSE `alt_progress`/`alt_done`/`alt_error`/`alt_cancelled` |
72
- | `POST /api/add/alt/cancel` | 取消标志 + `ClaudeSDKClient.interrupt` | 终止后台转换,等线程结束 + 清理 |
73
- | `POST /api/add/alt/complete` | `installer.stage_install` | 转换结果交给现有本地安装流程 |
74
-
75
- 错误契约:`CckitError`(含 `InstallError`)统一捕获 → `{error:{message}}` + 400;
76
- 参数校验失败 → 422。前端只读 `message`,不依赖状态码区分。
77
-
78
- `preview_id → StagedInstall` 存进程内字典,带 30 分钟过期回收 + 取消清理,避免
79
- 临时 clone 目录堆积。
80
-
81
- ## 关键改动(相对 v0.1)
82
-
83
- ### 状态层作用域推广
84
-
85
- `list_skills` / `get_state` / `set_state` / `budget` 增加 `root: Path | None` 参数,
86
- 缺省回落 `config.project_root()`(cwd)——CLI 行为不变。`registry._kit_in_scope` /
87
- `find_skill` 等同步线程 root。这让 Web 侧栏能操作**任意项目作用域**,而非只有 cwd。
88
-
89
- ### 关注项目清单 `projects.py`
90
-
91
- `~/.cckit/projects.json`(声明式存储):记录「用户想持续关注的项目目录」。与
92
- `registry.json` 分工——registry 存「装了什么」(观测不到),projects 存「想关注什么」
93
- (用户意图)。`add()` 顺带建 `.claude` 目录使其成为合法项目作用域。
94
-
95
- `state.list_scopes()` 返回三源并集(去重):全局 + registry 的
96
- `known_scopes`/`override_scopes` + 关注清单,每项带 `{path, source, has_skills}`。
97
-
98
- ### installer 拆两阶段
99
-
100
- `install()` 原是一条龙(clone→校验→print→`input()`→执行),混着 I/O。拆为:
101
-
102
- - `stage_install(...) -> StagedInstall`:clone → 校验 → lint → 计算计划,不落盘。
103
- lint 的 error **不在此中止**(消息进 `lint_msgs`),由调用方决定是否继续。
104
- - `execute_install(staged, ...) -> Iterator[ProgressEvent]`:移入 store → env →
105
- postinstall → registry → link,逐条 yield 进度;异常回滚 store/env/registry。
106
-
107
- CLI 的 `install()` 变为薄封装(stage → print 计划 → input 确认 → 迭代打印进度),
108
- 行为与拆分前一致;Web 用 stage/preview + SSE 消费 progress。这是**根治「改两边」**
109
- 的关键:核心函数只返回数据。
110
-
111
- ### doctor 流式回调
112
-
113
- `doctor.run(fix, on_event=None)` 增加可选回调,`_apply_fixes` 重建 env 前后推送
114
- 事件。向后兼容(不传回调行为不变)。
115
-
116
- ### 全局 skill 项目级覆盖(D-15 读回)
117
-
118
- `list_skills(scope="project")` 会把所有全局 kit 的 skill 也列出,`SkillState` 带
119
- `is_global_skill` / `global_state` / `override` 三个字段(详见
120
- [09](09-state-api.md))。项目作用域下能「按项目单独覆盖」全局 skill:
121
-
122
- - 覆盖 `off` / `name-only`:写项目 `skillOverrides`,不影响全局。
123
- - `enabled`:清覆盖 = 跟随全局。
124
- - `installed`(purge)不允许:项目里没有 link 可删。
125
-
126
- CLI 与 Web 共用这套语义:CLI `list --project` 显示「项目覆盖 已关闭」或「跟随全局
127
- 已启用」;Web 把全局 skill 单独折叠成一组,用独立的覆盖选择器(不复用四态),只展示
128
- 当前全局状态下允许的选项。
129
-
130
- ### 非标准仓库导入(alt)
131
-
132
- 「添加 Kit」弹窗新增「允许导入非标准仓库」开关(默认关闭)。开启时立即执行与 CLI 一致的
133
- 前置条件检查:未装 `alt` extra → 提示 `uv tool install 'cckit[alt]'`;`kit-builder`
134
- 缺失/冲突/状态不合适 → 分别给出安装、修改或仅关闭的处理,开关保持关闭;全部满足才保持开启。
135
-
136
- 标准仓库不触发转换,直接复用原有「计划 → 确认 → 安装」流程。非标准仓库则进入三阶段
137
- 进度面板(准备仓库 / 改造仓库 / 审计结果),后端在**独立后台线程**里跑 `alt` 流程,
138
- 线程内部用 `asyncio.run()` 执行 Claude Agent SDK,通过线程安全队列 → 现有 SSE 机制把
139
- 结构化事件(`event: alt_progress`,含 `stage` / `kind` / `message`)推给前端。前端
140
- **不依赖自由文本判断阶段状态**,只认结构化字段。
141
-
142
- 三阶段全部成功后「完成」才可点击;点击后把转换出的临时 Kit 交给现有 `stage_install`,
143
- 复用安装计划弹窗、lint 展示、确认与安装进度。「终止」不只断开 SSE:设置取消标志、
144
- 请求 `ClaudeSDKClient` 断开、等待后台线程结束、清理临时目录,并向前端发 `cancelled`。
145
- 转换、取消、出错、浏览器断开等路径都保证临时目录最终被清理,清理完成前不报告
146
- 「资源已全部释放」。
147
-
148
- ## 前端(`web/`)
149
-
150
- ```
151
- src/
152
- App.tsx # 编排:SidebarProvider + AppSidebar + SidebarInset + 弹窗开关
153
- lib/api.ts # 类型 + fetch 封装(SSE 端点返回原生 Response)
154
- lib/utils.ts # cn()
155
- hooks/useSSE.ts # 消费 SSE 流(data: {json} 帧解析)
156
- components/
157
- AppSidebar / KitList / AddKitDialog / DoctorDialog / AddScopeDialog / NoticeMenu
158
- ui/ # shadcn 组件(button/dialog/dropdown-menu/tooltip/input/textarea/
159
- # card/badge/progress/separator/popover/sonner/sidebar/sheet/skeleton)
160
- ```
161
-
162
- - **布局**:dashboard 式——官方 `Sidebar`(variant=inset)+ 右侧 `SidebarInset`。
163
- Sidebar 内:Header(CCKitKit + 版本)、4 个分组(作用域 / 添加关注项目 / Kit 管理 /
164
- 刷新)、Footer(清单预算,收起时变圆环);右侧顶部 `SidebarTrigger` 收起/展开。
165
- - **管理员通知**(`cckit web --notice TITLE FILE`,没配就完全不渲染):
166
- 在右侧 **`Skills` 那一行的最右端**。位置选这里的理由:这行 header 在 `SidebarInset` 里,
167
- 与 `<Sidebar>` 是兄弟节点,所以**收起侧栏不影响它**,而且它在滚动容器之外,始终可见。
168
- 控件用 `Dialog` + `DialogTrigger` + `Button`(都是已装的官方组件)拼成:按钮用
169
- **`default`(`primary`)变体**让它在 header 里足够显眼,左侧一个 `Megaphone` 图标,
170
- 右侧两行——第一行 `管理员通知`,第二行是 `TITLE`。图标与文字都用按钮自己的
171
- `primary-foreground`(标签行取 70% 那一档),不另指定灰色,免得压在彩色底上发糊。
172
- 按钮宽度是 `w-1/2`,即**内容区宽度的一半**(用百分比而非固定值:这样侧栏收起/展开时
173
- 它自动跟着变,不需要读侧栏状态);标题超出用 `truncate` 截断,完整标题在弹窗里。
174
- 点击弹窗以**纯文本**(`<pre>`)显示 `FILE` 内容。
175
- 标题由后端启动时注入、**首屏即渲染**;正文在打开弹窗时才请求,所以改了公告不用重启。
176
- - **列表**:项目 kit 可收起/展开/卸载(卸载用 Popover + destructive 确认);skill
177
- 四态选择器(`name-only` 附解释)、`env_ok=False` 警示、`managed=False` 置灰只读;
178
- 全局 skill 单独折叠一组、无卸载按钮、用覆盖选择器。
179
- 名字旁边有两类互不相同的提示图标:
180
- - 🔴 **红色 `Warning`** —— `env_ok === false`,运行环境(venv)缺失或损坏,去「诊断」修;
181
- - 🔑 **琥珀色 `Key`** —— `missing_env` 非空,有声明 `required: true` 的环境变量还没填。
182
- Tooltip 列出变量名并提示"点右侧齿轮填写"。**只是提醒不是报错**,所以用琥珀色与红色
183
- 区分开——点齿轮就能解决,不用离开这一屏。
184
- 两者都只在真有事时出现,不给每一行加噪声。
185
- - **配置管理**(齿轮图标 + 下拉,内容为空时显示「无」):
186
- - **skill 级**:在**状态下拉左侧**。下拉分「环境变量」与「配置文件」两段;
187
- 点 env 行弹对话框改值(输入框 + 取消/保存),点 conf 行弹对话框预览与编辑
188
- (Textarea + 取消/保存)。
189
- - **kit 级**:在**卸载按钮左侧**,复用同一形态,只有环境变量(没有 `conf_files`)。
190
- - 值在展开时才向后端取 —— 可能在别处改过,缓存会给出过期的默认值。
191
- - `installed` 态(无 link)与 `managed=False` 的 skill 一律禁用;服务端也拦
192
- (见 [07-security.md](07-security.md) 第 10 条)。
193
- - **添加 Kit**:两阶段弹窗(表单 → 计划确认,lint error 禁用安装 → SSE 日志)。
194
- - **诊断**:`--fix` 勾选 + SSE 进度 + findings 结果。
195
- - **刷新三层**:侧栏刷新按钮 + 写操作后 `invalidateQueries` 自动重拉 + 可选轮询。
196
- 所有开关改动提示「**新会话生效**」(对齐 09 的语义)。
197
- - **提示框**:后端错误契约统一拦截,`set_state` 的返回串走 error、成功走 success。
198
-
199
- ## 使用与部署
200
-
201
- **普通用户**(前端已随 wheel 打包,一条命令即用):
202
-
203
- ```bash
204
- uv tool install 'cckit[web]'
205
- cckit web # 打开 http://127.0.0.1:8000 即用
206
- ```
207
-
208
- **开发者**(源码仓库里改前端):
209
-
210
- ```bash
211
- # 开发调试
212
- uv run cckit web # 后端 :8000(不托管前端)
213
- cd web && npm run dev # 前端 :5173(vite dev 代理 /api)
214
-
215
- # 生产 / 部署
216
- cd web && npm install && npm run build # 产出 web/dist/,uv build 时打进 wheel
217
- uv run cckit web --host 0.0.0.0 --port 8000 # 自动定位前端,缺省也无需 --static-dir
218
- ```
219
-
220
- **安全**:`--host` 默认 `127.0.0.1`。上服务器被浏览器访问时用 `--host 0.0.0.0`,
221
- ⚠️ 放开即把「能装 kit(跑作者代码)、能改状态」的能力暴露给网段,只在可信内网用,
222
- 必要时在反向代理层加鉴权。
223
-
224
- **iframe 融合**:后端托管前端后,宿主页 `<iframe src="http://<server>:<port>/">`
225
- 即可。同源策略保证宿主页读不到 iframe 内部、也调不了 cckit 接口(无 CORS 问题);
226
- 不设 `frame-ancestors` CSP(Starlette 默认即可嵌)。HTTPS 宿主嵌 `http://<server>`
227
- 需在目标浏览器实测(Safari 较严)。
228
-
229
- **子路径部署(`--base`)**:当面板要挂到宿主 dashboard 的某个子路径(如 `/cckit/`)下,
230
- 且不想与宿主页自身的 `/api` 冲突时,启动时指定根路径前缀:
231
-
232
- ```bash
233
- cckit web --host 0.0.0.0 --base /cckit
234
- ```
235
-
236
- 此时后端把整站(所有 `/api/*`、`/assets`、SPA fallback)整体 mount 到 `/cckit` 下,
237
- 反向代理只需把 `/cckit/*` **原样转发**(无需 strip 前缀),宿主页
238
- `<iframe src="http://<server>:<port>/cckit/">` 即可。实现要点:
239
-
240
- - **运行时注入**:后端 serve 时把 `window.__CCKIT_BASE__` 注入 `index.html` 的 `<head>`,
241
- 前端据此给 `/api/*` 拼前缀(SSE 同走 `fetch`,一并覆盖)。根部署(base 为空)不注入、
242
- 行为与从前完全一致。
243
- - **资源相对化**:前端 Vite 用 `base: "./"` 构建,`./assets/...` 与字体 `url(...)` 自动
244
- 跟随当前子路径,因此**同一份产物**在根部署与任意子路径部署下都可用,`--base` 是纯运行
245
- 时配置,无需重新构建前端。
246
-
247
- ## 验证
248
-
249
- - `uv run pytest -q` 82 项全绿(状态层 root 推广、installer 拆分后 CLI 行为不回归)。
250
- - `cd web && npm run build` 通过(tsc 类型检查 + vite 打包)。
251
- - 端到端:装 wheel 到隔离 venv 后 `cckit web` 一键启动,静态页面、`/api/*`、静态资源
252
- 均可达,非法 API 路径返回 404 而非 index.html。
253
- - 纯净性:`uv build`(不先跑前端构建)产出的 wheel 不含前端,`[web]` 依赖也不随
254
- `uv tool install cckit` 安装;前端产物仅在前端 build 后才由 hatch 钩子打进 wheel。
255
- `skills/kit-builder`(alt 的自举工具)则**始终**随 wheel 分发到 `cckit/skills/kit-builder`,
256
- 与前端是否构建无关。
1
+ # Web 管理面板
2
+
3
+ 配套 v0.3.3 的可选管理面板:平时展示 skill 列表,支持导入(add)、卸载(remove)、
4
+ 四态开关切换、诊断(doctor)。**纯增量**——不装 `[web]` 额外依赖时,`cckit` 本体
5
+ 与命令行工具完全不变。
6
+
7
+ ## 定位
8
+
9
+ - 目标是**管理界面**,不是运行界面:`exec` 这类"跑 skill"的能力在网页上不做
10
+ (服务器上直接跑 `ccb`/Claude Code 即可)。
11
+ - 部署形态是**独立面板 + iframe 融合**:它要和一个外部 dashboard(Vite+React,
12
+ 非本项目维护)一起上 Linux 服务器,通过 `<iframe>` 嵌入对方页面,因此必须
13
+ 独立开发、独立部署、独立端口,不依赖改对方代码。
14
+
15
+ ## 技术方案
16
+
17
+ | 决策 | 选择 | 理由 |
18
+ |---|---|---|
19
+ | 仓库 | 当前仓库(`web/` + `src/cckit/web/`) | Web 要动状态层(root 推广)与 installer(两阶段),本就是同一件事;版本天然锁步 |
20
+ | 后端 | FastAPI + uvicorn,import `cckit.*` | 状态/安装/诊断逻辑只有一份,Web 不重实现;SSE 流式 |
21
+ | 前端 | Vite + React 19 + TypeScript + Tailwind v4 + shadcn/ui | 纯客户端 SPA,无 SSR 需求;shadcn 4.x 用 base-ui 原语 |
22
+ | 图标 | @phosphor-icons/react | shadcn 4.x 默认图标库 |
23
+ | 数据层 | TanStack Query | 缓存 + 失效即"刷新机制" |
24
+ | 提示 | sonner(shadcn 包装) | 四类 toast + 弹窗/toast 分工 |
25
+ | 依赖隔离 | `[web]` 可选 extra | `uv tool install cckit` 仍是 3 依赖纯本体 |
26
+ | 安全 | 后端默认 `127.0.0.1` | Web 能装 kit(跑作者代码)、能改状态,不放开监听 |
27
+ | 子路径部署 | `--base` 运行时挂载 + `window.__CCKIT_BASE__` 注入 + Vite `base: "./"` | 嵌入 dashboard 子路径时避免与宿主 `/api` 冲突,无需重建前端 |
28
+
29
+ ## 架构
30
+
31
+ ```
32
+ 浏览器 (外部 dashboard 内 iframe)
33
+ │ http://<server>:<port>/
34
+ ▼
35
+ FastAPI (src/cckit/web/) ── 静态托管 web/dist/
36
+ │ import ├── GET/POST /api/* (JSON)
37
+ │ ┌─ cckit.state ├── POST /api/add/execute (SSE)
38
+ │ ├─ cckit.installer └── POST /api/doctor (SSE)
39
+ │ └─ cckit.doctor
40
+ ▼
41
+ ~/.cckit/ store + registry.json + projects.json + envs/ + envs.json
42
+ <CC 配置目录>/skills + settings.json (状态派生源)
43
+ ```
44
+
45
+ **核心原则**:状态读写只走 `cckit.state`,安装只走 `cckit.installer`,诊断只走
46
+ `cckit.doctor`。Web 后端是这些模块的薄前端,零逻辑重实现,杜绝「改两边」——
47
+ 改核心逻辑一处,CLI 与 Web 自动同步;唯一要保证的是核心函数**只返回数据、不发声音**
48
+ (不 `print`/`input`),由 CLI/Web 各自渲染。
49
+
50
+ ## 后端 API
51
+
52
+ | 端点 | 底层调用 | 说明 |
53
+ |---|---|---|
54
+ | `GET /api/scopes` | `state.list_scopes()` | 侧栏作用域(全局 + 历史项目 + 关注项目) |
55
+ | `POST/DELETE /api/scopes` | `projects.add/remove` | 关注/取关项目,add 时建 `.claude` |
56
+ | `GET /api/skills?scope=&root=` | `state.list_skills` + `state.budget` | 列表 + 预算一次返回;skill 带 `is_global_skill`/`global_state`/`override`/`envs`/`conf_files`;另含 `kit_env`(按 kit 名索引的 kit 级声明) |
57
+ | `POST /api/skills/{name}/state` | `state.set_state` | 四态切换,返回 `{message}` |
58
+ | `GET /api/config?scope=&kit=&skill=` | `state.env_requirements` + `state.conf_files` | 配置需求 + 当前值;`skill` 省略时只返回 kit 级(`kit_env`) |
59
+ | `POST /api/config/env` | `state.set_user_env` | 保存/清除一个环境变量的值 |
60
+ | `GET /api/config/conf?scope=&kit=&skill=&path=` | `state.read_conf_file` | 读一个可修改的配置文件 |
61
+ | `PUT /api/config/conf` | `state.write_conf_file` | 写回(服务端校验路径,见下) |
62
+ | `GET /api/info` | 读 `--notice` 指定的文件 | 管理员通知正文。**没配 `--notice` 时 404**,前端据此不渲染控件 |
63
+ | `GET /api/kits` | `registry.load()` | kit 头部元信息 |
64
+ | `POST /api/kits/{kit}/remove` | `installer.remove_kit` | 卸载 |
65
+ | `POST /api/add/preview` | `installer.stage_install` | 克隆+校验+计划,存 `preview_id` |
66
+ | `POST /api/add/execute` | `installer.execute_install` | 确认后执行,SSE 进度 |
67
+ | `DELETE /api/add/preview/{id}` | `StagedInstall.cleanup` | 取消时清理临时目录 |
68
+ | `POST /api/doctor` | `doctor.run(fix, on_event)` | SSE 进度 + findings |
69
+ | `GET /api/add/alt/check` | `alt.check_sdk_available` + `alt.kit_builder_status` | alt 开关的前置条件检查 |
70
+ | `POST /api/add/alt/fix` | `alt.apply_kit_builder_fix` | 修复 kit-builder(安装/切 enabled),重新校验前置条件防冲突/来源不一致 |
71
+ | `POST /api/add/alt/start` | `alt.run_alt_conversion`(后台线程) | 三阶段转换,SSE `alt_progress`/`alt_done`/`alt_error`/`alt_cancelled` |
72
+ | `POST /api/add/alt/cancel` | 取消标志 + `ClaudeSDKClient.interrupt` | 终止后台转换,等线程结束 + 清理 |
73
+ | `POST /api/add/alt/complete` | `installer.stage_install` | 转换结果交给现有本地安装流程 |
74
+
75
+ 错误契约:`CckitError`(含 `InstallError`)统一捕获 → `{error:{message}}` + 400;
76
+ 参数校验失败 → 422。前端只读 `message`,不依赖状态码区分。
77
+
78
+ `preview_id → StagedInstall` 存进程内字典,带 30 分钟过期回收 + 取消清理,避免
79
+ 临时 clone 目录堆积。
80
+
81
+ ## 关键改动(相对 v0.1)
82
+
83
+ ### 状态层作用域推广
84
+
85
+ `list_skills` / `get_state` / `set_state` / `budget` 增加 `root: Path | None` 参数,
86
+ 缺省回落 `config.project_root()`(cwd)——CLI 行为不变。`registry._kit_in_scope` /
87
+ `find_skill` 等同步线程 root。这让 Web 侧栏能操作**任意项目作用域**,而非只有 cwd。
88
+
89
+ ### 关注项目清单 `projects.py`
90
+
91
+ `~/.cckit/projects.json`(声明式存储):记录「用户想持续关注的项目目录」。与
92
+ `registry.json` 分工——registry 存「装了什么」(观测不到),projects 存「想关注什么」
93
+ (用户意图)。`add()` 顺带建 `.claude` 目录使其成为合法项目作用域。
94
+
95
+ `state.list_scopes()` 返回三源并集(去重):全局 + registry 的
96
+ `known_scopes`/`override_scopes` + 关注清单,每项带 `{path, source, has_skills}`。
97
+
98
+ ### installer 拆两阶段
99
+
100
+ `install()` 原是一条龙(clone→校验→print→`input()`→执行),混着 I/O。拆为:
101
+
102
+ - `stage_install(...) -> StagedInstall`:clone → 校验 → lint → 计算计划,不落盘。
103
+ lint 的 error **不在此中止**(消息进 `lint_msgs`),由调用方决定是否继续。
104
+ - `execute_install(staged, ...) -> Iterator[ProgressEvent]`:移入 store → env →
105
+ postinstall → registry → link,逐条 yield 进度;异常回滚 store/env/registry。
106
+
107
+ CLI 的 `install()` 变为薄封装(stage → print 计划 → input 确认 → 迭代打印进度),
108
+ 行为与拆分前一致;Web 用 stage/preview + SSE 消费 progress。这是**根治「改两边」**
109
+ 的关键:核心函数只返回数据。
110
+
111
+ ### doctor 流式回调
112
+
113
+ `doctor.run(fix, on_event=None)` 增加可选回调,`_apply_fixes` 重建 env 前后推送
114
+ 事件。向后兼容(不传回调行为不变)。
115
+
116
+ ### 全局 skill 项目级覆盖(D-15 读回)
117
+
118
+ `list_skills(scope="project")` 会把所有全局 kit 的 skill 也列出,`SkillState` 带
119
+ `is_global_skill` / `global_state` / `override` 三个字段(详见
120
+ [09](09-state-api.md))。项目作用域下能「按项目单独覆盖」全局 skill:
121
+
122
+ - 覆盖 `off` / `name-only`:写项目 `skillOverrides`,不影响全局。
123
+ - `enabled`:清覆盖 = 跟随全局。
124
+ - `installed`(purge)不允许:项目里没有 link 可删。
125
+
126
+ CLI 与 Web 共用这套语义:CLI `list --project` 显示「项目覆盖 已关闭」或「跟随全局
127
+ 已启用」;Web 把全局 skill 单独折叠成一组,用独立的覆盖选择器(不复用四态),只展示
128
+ 当前全局状态下允许的选项。
129
+
130
+ ### 非标准仓库导入(alt)
131
+
132
+ 「添加 Kit」弹窗新增「允许导入非标准仓库」开关(默认关闭)。开启时立即执行与 CLI 一致的
133
+ 前置条件检查:未装 `alt` extra → 提示 `uv tool install 'cckit[alt]'`;`kit-builder`
134
+ 缺失/冲突/状态不合适 → 分别给出安装、修改或仅关闭的处理,开关保持关闭;全部满足才保持开启。
135
+
136
+ 标准仓库不触发转换,直接复用原有「计划 → 确认 → 安装」流程。非标准仓库则进入三阶段
137
+ 进度面板(准备仓库 / 改造仓库 / 审计结果),后端在**独立后台线程**里跑 `alt` 流程,
138
+ 线程内部用 `asyncio.run()` 执行 Claude Agent SDK,通过线程安全队列 → 现有 SSE 机制把
139
+ 结构化事件(`event: alt_progress`,含 `stage` / `kind` / `message`)推给前端。前端
140
+ **不依赖自由文本判断阶段状态**,只认结构化字段。
141
+
142
+ 三阶段全部成功后「完成」才可点击;点击后把转换出的临时 Kit 交给现有 `stage_install`,
143
+ 复用安装计划弹窗、lint 展示、确认与安装进度。「终止」不只断开 SSE:设置取消标志、
144
+ 请求 `ClaudeSDKClient` 断开、等待后台线程结束、清理临时目录,并向前端发 `cancelled`。
145
+ 转换、取消、出错、浏览器断开等路径都保证临时目录最终被清理,清理完成前不报告
146
+ 「资源已全部释放」。
147
+
148
+ ## 前端(`web/`)
149
+
150
+ ```
151
+ src/
152
+ App.tsx # 编排:SidebarProvider + AppSidebar + SidebarInset + 弹窗开关
153
+ lib/api.ts # 类型 + fetch 封装(SSE 端点返回原生 Response)
154
+ lib/utils.ts # cn()
155
+ hooks/useSSE.ts # 消费 SSE 流(data: {json} 帧解析)
156
+ components/
157
+ AppSidebar / KitList / AddKitDialog / DoctorDialog / AddScopeDialog / NoticeMenu
158
+ ui/ # shadcn 组件(button/dialog/dropdown-menu/tooltip/input/textarea/
159
+ # card/badge/progress/separator/popover/sonner/sidebar/sheet/skeleton)
160
+ ```
161
+
162
+ - **布局**:dashboard 式——官方 `Sidebar`(variant=inset)+ 右侧 `SidebarInset`。
163
+ Sidebar 内:Header(CCKitKit + 版本)、4 个分组(作用域 / 添加关注项目 / Kit 管理 /
164
+ 刷新)、Footer(清单预算,收起时变圆环);右侧顶部 `SidebarTrigger` 收起/展开。
165
+ - **管理员通知**(`cckit web --notice TITLE FILE`,没配就完全不渲染):
166
+ 在右侧 **`Skills` 那一行的最右端**。位置选这里的理由:这行 header 在 `SidebarInset` 里,
167
+ 与 `<Sidebar>` 是兄弟节点,所以**收起侧栏不影响它**,而且它在滚动容器之外,始终可见。
168
+ 控件用 `Dialog` + `DialogTrigger` + `Button`(都是已装的官方组件)拼成:按钮用
169
+ **`default`(`primary`)变体**让它在 header 里足够显眼,左侧一个 `Megaphone` 图标,
170
+ 右侧两行——第一行 `管理员通知`,第二行是 `TITLE`。图标与文字都用按钮自己的
171
+ `primary-foreground`(标签行取 70% 那一档),不另指定灰色,免得压在彩色底上发糊。
172
+ 按钮宽度是 `w-1/2`,即**内容区宽度的一半**(用百分比而非固定值:这样侧栏收起/展开时
173
+ 它自动跟着变,不需要读侧栏状态);标题超出用 `truncate` 截断,完整标题在弹窗里。
174
+ 点击弹窗以**纯文本**(`<pre>`)显示 `FILE` 内容。
175
+ 标题由后端启动时注入、**首屏即渲染**;正文在打开弹窗时才请求,所以改了公告不用重启。
176
+ - **列表**:项目 kit 可收起/展开/卸载(卸载用 Popover + destructive 确认);skill
177
+ 四态选择器(`name-only` 附解释)、`env_ok=False` 警示、`managed=False` 置灰只读;
178
+ 全局 skill 单独折叠一组、无卸载按钮、用覆盖选择器。
179
+ 名字旁边有两类互不相同的提示图标:
180
+ - 🔴 **红色 `Warning`** —— `env_ok === false`,运行环境(venv)缺失或损坏,去「诊断」修;
181
+ - 🔑 **琥珀色 `Key`** —— `missing_env` 非空,有声明 `required: true` 的环境变量还没填。
182
+ Tooltip 列出变量名并提示"点右侧齿轮填写"。**只是提醒不是报错**,所以用琥珀色与红色
183
+ 区分开——点齿轮就能解决,不用离开这一屏。
184
+ 两者都只在真有事时出现,不给每一行加噪声。
185
+ - **配置管理**(齿轮图标 + 下拉,内容为空时显示「无」):
186
+ - **skill 级**:在**状态下拉左侧**。下拉分「环境变量」与「配置文件」两段;
187
+ 点 env 行弹对话框改值(输入框 + 取消/保存),点 conf 行弹对话框预览与编辑
188
+ (Textarea + 取消/保存)。
189
+ - **kit 级**:在**卸载按钮左侧**,复用同一形态,只有环境变量(没有 `conf_files`)。
190
+ - 值在展开时才向后端取 —— 可能在别处改过,缓存会给出过期的默认值。
191
+ - `installed` 态(无 link)与 `managed=False` 的 skill 一律禁用;服务端也拦
192
+ (见 [07-security.md](07-security.md) 第 10 条)。
193
+ - **添加 Kit**:两阶段弹窗(表单 → 计划确认,lint error 禁用安装 → SSE 日志)。
194
+ - **诊断**:`--fix` 勾选 + SSE 进度 + findings 结果。
195
+ - **刷新三层**:侧栏刷新按钮 + 写操作后 `invalidateQueries` 自动重拉 + 可选轮询。
196
+ 所有开关改动提示「**新会话生效**」(对齐 09 的语义)。
197
+ - **提示框**:后端错误契约统一拦截,`set_state` 的返回串走 error、成功走 success。
198
+
199
+ ## 使用与部署
200
+
201
+ **普通用户**(前端已随 wheel 打包,一条命令即用):
202
+
203
+ ```bash
204
+ uv tool install 'cckit[web]'
205
+ cckit web # 打开 http://127.0.0.1:8000 即用
206
+ ```
207
+
208
+ **开发者**(源码仓库里改前端):
209
+
210
+ ```bash
211
+ # 开发调试
212
+ uv run cckit web # 后端 :8000(不托管前端)
213
+ cd web && npm run dev # 前端 :5173(vite dev 代理 /api)
214
+
215
+ # 生产 / 部署
216
+ cd web && npm install && npm run build # 产出 web/dist/,uv build 时打进 wheel
217
+ uv run cckit web --host 0.0.0.0 --port 8000 # 自动定位前端,缺省也无需 --static-dir
218
+ ```
219
+
220
+ **安全**:`--host` 默认 `127.0.0.1`。上服务器被浏览器访问时用 `--host 0.0.0.0`,
221
+ ⚠️ 放开即把「能装 kit(跑作者代码)、能改状态」的能力暴露给网段,只在可信内网用,
222
+ 必要时在反向代理层加鉴权。
223
+
224
+ **iframe 融合**:后端托管前端后,宿主页 `<iframe src="http://<server>:<port>/">`
225
+ 即可。同源策略保证宿主页读不到 iframe 内部、也调不了 cckit 接口(无 CORS 问题);
226
+ 不设 `frame-ancestors` CSP(Starlette 默认即可嵌)。HTTPS 宿主嵌 `http://<server>`
227
+ 需在目标浏览器实测(Safari 较严)。
228
+
229
+ **子路径部署(`--base`)**:当面板要挂到宿主 dashboard 的某个子路径(如 `/cckit/`)下,
230
+ 且不想与宿主页自身的 `/api` 冲突时,启动时指定根路径前缀:
231
+
232
+ ```bash
233
+ cckit web --host 0.0.0.0 --base /cckit
234
+ ```
235
+
236
+ 此时后端把整站(所有 `/api/*`、`/assets`、SPA fallback)整体 mount 到 `/cckit` 下,
237
+ 反向代理只需把 `/cckit/*` **原样转发**(无需 strip 前缀),宿主页
238
+ `<iframe src="http://<server>:<port>/cckit/">` 即可。实现要点:
239
+
240
+ - **运行时注入**:后端 serve 时把 `window.__CCKIT_BASE__` 注入 `index.html` 的 `<head>`,
241
+ 前端据此给 `/api/*` 拼前缀(SSE 同走 `fetch`,一并覆盖)。根部署(base 为空)不注入、
242
+ 行为与从前完全一致。
243
+ - **资源相对化**:前端 Vite 用 `base: "./"` 构建,`./assets/...` 与字体 `url(...)` 自动
244
+ 跟随当前子路径,因此**同一份产物**在根部署与任意子路径部署下都可用,`--base` 是纯运行
245
+ 时配置,无需重新构建前端。
246
+
247
+ ## 验证
248
+
249
+ - `uv run pytest -q` 82 项全绿(状态层 root 推广、installer 拆分后 CLI 行为不回归)。
250
+ - `cd web && npm run build` 通过(tsc 类型检查 + vite 打包)。
251
+ - 端到端:装 wheel 到隔离 venv 后 `cckit web` 一键启动,静态页面、`/api/*`、静态资源
252
+ 均可达,非法 API 路径返回 404 而非 index.html。
253
+ - 纯净性:`uv build`(不先跑前端构建)产出的 wheel 不含前端,`[web]` 依赖也不随
254
+ `uv tool install cckit` 安装;前端产物仅在前端 build 后才由 hatch 钩子打进 wheel。
255
+ `skills/kit-builder`(alt 的自举工具)则**始终**随 wheel 分发到 `cckit/skills/kit-builder`,
256
+ 与前端是否构建无关。