@jspg-ai/coding-bb 0.0.1

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 (183) hide show
  1. package/README.md +41 -0
  2. package/cbb/dev-standards/rules/cbb-ai-behavior.md +104 -0
  3. package/cbb/dev-standards/rules/cbb-coding-rule.md +50 -0
  4. package/cbb/dev-standards/rules/cbb-priority.md +56 -0
  5. package/cbb/dev-standards/skills/architecture-specs/SKILL.md +129 -0
  6. package/cbb/dev-standards/skills/coding-specs/SKILL.md +376 -0
  7. package/cbb/dev-standards/skills/coding-specs/references/concurrency.md +53 -0
  8. package/cbb/dev-standards/skills/coding-specs/references/config-center.md +16 -0
  9. package/cbb/dev-standards/skills/coding-specs/references/distributed.md +42 -0
  10. package/cbb/dev-standards/skills/coding-specs/references/es-coding.md +60 -0
  11. package/cbb/dev-standards/skills/coding-specs/references/scheduled-task.md +25 -0
  12. package/cbb/dev-standards/skills/coding-specs/references/security.md +11 -0
  13. package/cbb/dev-standards/skills/coding-specs/references/unit-testing.md +43 -0
  14. package/cbb/dev-standards/skills/es-design-specs/SKILL.md +104 -0
  15. package/cbb/dev-standards/skills/mysql-design-specs/SKILL.md +89 -0
  16. package/cbb/lib/install/claude-code.js +20 -0
  17. package/cbb/lib/install/cleanup.js +64 -0
  18. package/cbb/lib/install/init.js +1875 -0
  19. package/cbb/lib/install/qoder.js +20 -0
  20. package/cbb/lib/install/workspaces.js +232 -0
  21. package/cbb/lib/openspec/index.js +554 -0
  22. package/cbb/lib/superpowers/index.js +265 -0
  23. package/cbb/lib/utils/check-update.js +147 -0
  24. package/cbb/lib/utils/checkbox.js +383 -0
  25. package/cbb/lib/utils/gitignore.js +69 -0
  26. package/cbb/lib/utils/output.js +64 -0
  27. package/cbb/lib/utils/settings.js +119 -0
  28. package/cbb/lib/utils/version.js +135 -0
  29. package/cbb/lib/wiki/api-client.js +358 -0
  30. package/cbb/lib/wiki/cli.js +427 -0
  31. package/cbb/lib/wiki/convert.js +464 -0
  32. package/cbb/lib/wiki/index.js +218 -0
  33. package/cbb/lib/wiki/mermaid-guard.js +103 -0
  34. package/cbb/lib/wiki/split.js +131 -0
  35. package/cbb/tools/cbb-decompile-jar/SKILL.md +220 -0
  36. package/cbb/tools/cbb-decompile-jar/scripts/decompile.py +863 -0
  37. package/cbb/tools/cbb-design-to-wiki/SKILL.md +180 -0
  38. package/cbb/tools/cbb-mvn-guide/SKILL.md +281 -0
  39. package/cbb/tools/cbb-mvn-guide/scripts/mvn_jdk_manager.py +378 -0
  40. package/cbb/tools/cbb-mvn-guide/scripts/mvn_pom_jdk_reader.py +197 -0
  41. package/cbb/tools/cbb-plantuml-authoring/SKILL.md +448 -0
  42. package/cbb/tools/cbb-plantuml-authoring/references/drawing-templates.md +479 -0
  43. package/cbb/tools/cbb-wiki-ops/SKILL.md +134 -0
  44. package/cbb/tools/cbb-wiki-ops/scripts/check-auth.js +44 -0
  45. package/cbb/worktrees/_shared/scripts/find-target-worktree.js +96 -0
  46. package/cbb/worktrees/_shared/scripts/find-workspace-root.js +67 -0
  47. package/cbb/worktrees/_shared/scripts/push-core.js +136 -0
  48. package/cbb/worktrees/_shared/scripts/silence-popup.js +23 -0
  49. package/cbb/worktrees/commands/close.md +64 -0
  50. package/cbb/worktrees/commands/extend.md +65 -0
  51. package/cbb/worktrees/commands/init.md +55 -0
  52. package/cbb/worktrees/commands/push.md +42 -0
  53. package/cbb/worktrees/skills/openspec-close-worktree/SKILL.md +449 -0
  54. package/cbb/worktrees/skills/openspec-close-worktree/scripts/check-env.js +76 -0
  55. package/cbb/worktrees/skills/openspec-close-worktree/scripts/check-unarchived.js +48 -0
  56. package/cbb/worktrees/skills/openspec-close-worktree/scripts/delete-branches.js +136 -0
  57. package/cbb/worktrees/skills/openspec-close-worktree/scripts/discover-apps.js +76 -0
  58. package/cbb/worktrees/skills/openspec-close-worktree/scripts/find-target-worktree.js +96 -0
  59. package/cbb/worktrees/skills/openspec-close-worktree/scripts/find-workspace-root.js +67 -0
  60. package/cbb/worktrees/skills/openspec-close-worktree/scripts/remove-worktrees.js +124 -0
  61. package/cbb/worktrees/skills/openspec-close-worktree/scripts/safety-check.js +174 -0
  62. package/cbb/worktrees/skills/openspec-close-worktree/scripts/silence-popup.js +23 -0
  63. package/cbb/worktrees/skills/openspec-extend-worktree/SKILL.md +390 -0
  64. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/check-env.js +95 -0
  65. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/check-repos.js +98 -0
  66. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/create-branches-and-worktrees.js +135 -0
  67. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/find-target-worktree.js +96 -0
  68. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/find-workspace-root.js +67 -0
  69. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/install-ai.js +150 -0
  70. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/list-available-apps.js +88 -0
  71. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/push-core.js +136 -0
  72. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/silence-popup.js +23 -0
  73. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/sync-repos.js +93 -0
  74. package/cbb/worktrees/skills/openspec-init-worktree/SKILL.md +539 -0
  75. package/cbb/worktrees/skills/openspec-init-worktree/scripts/auto-open.js +136 -0
  76. package/cbb/worktrees/skills/openspec-init-worktree/scripts/check-env-deep.js +87 -0
  77. package/cbb/worktrees/skills/openspec-init-worktree/scripts/check-env.js +90 -0
  78. package/cbb/worktrees/skills/openspec-init-worktree/scripts/check-repos.js +171 -0
  79. package/cbb/worktrees/skills/openspec-init-worktree/scripts/create-branches.js +103 -0
  80. package/cbb/worktrees/skills/openspec-init-worktree/scripts/create-worktrees.js +154 -0
  81. package/cbb/worktrees/skills/openspec-init-worktree/scripts/find-workspace-root.js +67 -0
  82. package/cbb/worktrees/skills/openspec-init-worktree/scripts/generate-app-options.js +93 -0
  83. package/cbb/worktrees/skills/openspec-init-worktree/scripts/install-ai.js +177 -0
  84. package/cbb/worktrees/skills/openspec-init-worktree/scripts/parse-config.js +67 -0
  85. package/cbb/worktrees/skills/openspec-init-worktree/scripts/push-branches.js +101 -0
  86. package/cbb/worktrees/skills/openspec-init-worktree/scripts/push-core.js +136 -0
  87. package/cbb/worktrees/skills/openspec-init-worktree/scripts/silence-popup.js +23 -0
  88. package/cbb/worktrees/skills/openspec-init-worktree/scripts/sync-repos.js +95 -0
  89. package/cbb/worktrees/skills/openspec-init-worktree/scripts/update-gitignore.js +134 -0
  90. package/cbb/worktrees/skills/openspec-push-worktrees/SKILL.md +307 -0
  91. package/cbb/worktrees/skills/openspec-push-worktrees/scripts/commit-worktrees.js +269 -0
  92. package/cbb/worktrees/skills/openspec-push-worktrees/scripts/find-target-worktree.js +96 -0
  93. package/cbb/worktrees/skills/openspec-push-worktrees/scripts/find-workspace-root.js +67 -0
  94. package/cbb/worktrees/skills/openspec-push-worktrees/scripts/push-core.js +136 -0
  95. package/cbb/worktrees/skills/openspec-push-worktrees/scripts/push-worktrees.js +159 -0
  96. package/cbb/worktrees/skills/openspec-push-worktrees/scripts/silence-popup.js +23 -0
  97. package/config/cbb.yaml +6 -0
  98. package/config/config.sample.json +16 -0
  99. package/config/openspec/config.yaml +28 -0
  100. package/config/openspec/schemas/spec-driven/schema.yaml +247 -0
  101. package/config/openspec/schemas/spec-driven/templates/design.md +475 -0
  102. package/config/openspec/schemas/spec-driven/templates/proposal.md +58 -0
  103. package/config/openspec/schemas/spec-driven/templates/spec.md +8 -0
  104. package/config/openspec/schemas/spec-driven/templates/tasks.md +9 -0
  105. package/config/workspaces.json +15 -0
  106. package/openspec/.version +6 -0
  107. package/openspec/commands/apply.md +182 -0
  108. package/openspec/commands/archive.md +223 -0
  109. package/openspec/commands/bulk-archive.md +334 -0
  110. package/openspec/commands/continue.md +112 -0
  111. package/openspec/commands/explore.md +206 -0
  112. package/openspec/commands/ff.md +111 -0
  113. package/openspec/commands/new.md +70 -0
  114. package/openspec/commands/onboard.md +555 -0
  115. package/openspec/commands/propose.md +157 -0
  116. package/openspec/commands/sync.md +256 -0
  117. package/openspec/commands/update.md +85 -0
  118. package/openspec/commands/verify.md +169 -0
  119. package/openspec/skills/openspec-apply-change/SKILL.md +187 -0
  120. package/openspec/skills/openspec-archive-change/SKILL.md +181 -0
  121. package/openspec/skills/openspec-bulk-archive-change/SKILL.md +338 -0
  122. package/openspec/skills/openspec-continue-change/SKILL.md +117 -0
  123. package/openspec/skills/openspec-explore/SKILL.md +342 -0
  124. package/openspec/skills/openspec-ff-change/SKILL.md +116 -0
  125. package/openspec/skills/openspec-new-change/SKILL.md +76 -0
  126. package/openspec/skills/openspec-onboard/SKILL.md +560 -0
  127. package/openspec/skills/openspec-propose/SKILL.md +162 -0
  128. package/openspec/skills/openspec-sync-specs/SKILL.md +261 -0
  129. package/openspec/skills/openspec-update-change/SKILL.md +90 -0
  130. package/openspec/skills/openspec-verify-change/SKILL.md +174 -0
  131. package/package.json +46 -0
  132. package/superpowers/.version +6 -0
  133. package/superpowers/skills/brainstorming/SKILL.md +250 -0
  134. package/superpowers/skills/brainstorming/scripts/frame-template.html +213 -0
  135. package/superpowers/skills/brainstorming/scripts/helper.js +167 -0
  136. package/superpowers/skills/brainstorming/scripts/server.cjs +723 -0
  137. package/superpowers/skills/brainstorming/scripts/start-server.sh +209 -0
  138. package/superpowers/skills/brainstorming/scripts/stop-server.sh +120 -0
  139. package/superpowers/skills/brainstorming/spec-document-reviewer-prompt.md +49 -0
  140. package/superpowers/skills/brainstorming/visual-companion.md +299 -0
  141. package/superpowers/skills/dispatching-parallel-agents/SKILL.md +167 -0
  142. package/superpowers/skills/executing-plans/SKILL.md +64 -0
  143. package/superpowers/skills/finishing-a-development-branch/SKILL.md +225 -0
  144. package/superpowers/skills/receiving-code-review/SKILL.md +205 -0
  145. package/superpowers/skills/requesting-code-review/SKILL.md +95 -0
  146. package/superpowers/skills/requesting-code-review/code-reviewer.md +181 -0
  147. package/superpowers/skills/subagent-driven-development/SKILL.md +568 -0
  148. package/superpowers/skills/subagent-driven-development/implementer-prompt.md +154 -0
  149. package/superpowers/skills/subagent-driven-development/re-review-prompt.md +115 -0
  150. package/superpowers/skills/subagent-driven-development/scripts/review-package +46 -0
  151. package/superpowers/skills/subagent-driven-development/scripts/sdd-workspace +40 -0
  152. package/superpowers/skills/subagent-driven-development/scripts/task-brief +41 -0
  153. package/superpowers/skills/subagent-driven-development/task-reviewer-prompt.md +207 -0
  154. package/superpowers/skills/systematic-debugging/CREATION-LOG.md +119 -0
  155. package/superpowers/skills/systematic-debugging/SKILL.md +283 -0
  156. package/superpowers/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
  157. package/superpowers/skills/systematic-debugging/condition-based-waiting.md +115 -0
  158. package/superpowers/skills/systematic-debugging/defense-in-depth.md +122 -0
  159. package/superpowers/skills/systematic-debugging/find-polluter.sh +72 -0
  160. package/superpowers/skills/systematic-debugging/root-cause-tracing.md +169 -0
  161. package/superpowers/skills/systematic-debugging/test-academic.md +14 -0
  162. package/superpowers/skills/systematic-debugging/test-pressure-1.md +58 -0
  163. package/superpowers/skills/systematic-debugging/test-pressure-2.md +68 -0
  164. package/superpowers/skills/systematic-debugging/test-pressure-3.md +69 -0
  165. package/superpowers/skills/test-driven-development/SKILL.md +320 -0
  166. package/superpowers/skills/test-driven-development/writing-good-tests.md +198 -0
  167. package/superpowers/skills/using-git-worktrees/SKILL.md +167 -0
  168. package/superpowers/skills/using-superpowers/SKILL.md +63 -0
  169. package/superpowers/skills/using-superpowers/references/antigravity-tools.md +23 -0
  170. package/superpowers/skills/using-superpowers/references/codex-tools.md +108 -0
  171. package/superpowers/skills/using-superpowers/references/gemini-tools.md +63 -0
  172. package/superpowers/skills/using-superpowers/references/hermes-tools.md +56 -0
  173. package/superpowers/skills/using-superpowers/references/pi-tools.md +16 -0
  174. package/superpowers/skills/verification-before-completion/SKILL.md +120 -0
  175. package/superpowers/skills/writing-plans/SKILL.md +171 -0
  176. package/superpowers/skills/writing-plans/plan-document-reviewer-prompt.md +49 -0
  177. package/superpowers/skills/writing-skills/SKILL.md +679 -0
  178. package/superpowers/skills/writing-skills/anthropic-best-practices.md +1150 -0
  179. package/superpowers/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
  180. package/superpowers/skills/writing-skills/graphviz-conventions.dot +172 -0
  181. package/superpowers/skills/writing-skills/persuasion-principles.md +187 -0
  182. package/superpowers/skills/writing-skills/render-graphs.js +169 -0
  183. package/superpowers/skills/writing-skills/testing-skills-with-subagents.md +384 -0
@@ -0,0 +1,1875 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * OpenSpec Shared Standards 安装脚本
5
+ *
6
+ * 将AI 编码规范与工作流套件安装到目标项目中。
7
+ * 支持工具:Claude Code (.claude/rules/)、Qoder (.qoder/rules/)
8
+ *
9
+ * 使用方式:
10
+ * npm install -g @jspg-ai/coding-bb # 首次使用
11
+ * npm install -g @jspg-ai/coding-bb
12
+ * cd your-project && cbb init
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const os = require('os');
17
+ const path = require('path');
18
+ const readline = require('readline');
19
+ const { execSync } = require('child_process');
20
+ const yaml = require('js-yaml');
21
+
22
+ const SHARED_STANDARDS_DIR = path.join(__dirname, '..', '..', '..');
23
+
24
+ // 目标项目根目录:默认取当前 cwd,init() / initWorkspace() 可在调用前覆盖。
25
+ // 注意:使用 let 而非 const 是为了让 cbb setup 能在拉好仓库后把目标目录
26
+ // 切到仓库根,再复用 init() 的完整安装流程,而无需把 TARGET_DIR 透传到所有 helper。
27
+ let TARGET_DIR = process.cwd();
28
+
29
+ // 清单文件:记录本包管理的所有路径,用于区分本包文件和用户自建文件
30
+ const MANIFEST_DIR = '.cbb';
31
+ const MANIFEST_FILE = path.join(MANIFEST_DIR, '.managed-by-cbb');
32
+ const LAST_TOOLS_FILE = path.join(MANIFEST_DIR, '.last-tools');
33
+
34
+ // 适配器
35
+ const adapters = [
36
+ require('./claude-code'),
37
+ require('./qoder'),
38
+ ];
39
+
40
+ // superpowers 部署白名单:仅装 openspec 工作流硬依赖 + 方法论基底(源仓库仍保留全量 14 个,
41
+ // 供 cbb update --upstream-superpowers 维护;砍掉的与 openspec 工作流重叠或属低频场景)
42
+ const SUPERPOWERS_WHITELIST = [
43
+ 'writing-plans', // openspec apply:生成 plan.md(schema.yaml 引用)
44
+ 'subagent-driven-development', // openspec apply:执行 plan.md(schema.yaml 引用)
45
+ 'executing-plans', // writing-plans 的无子代理 fallback 执行器
46
+ 'finishing-a-development-branch', // subagent-driven-development 收尾
47
+ 'requesting-code-review', // openspec verify:MANDATORY 代码审查
48
+ 'test-driven-development', // TDD 计划结构 + TDD Evidence 方法论基底
49
+ ];
50
+
51
+ // worktree 扩展 skills 部署到沙箱的白名单:init/extend 只在 workspace 根执行
52
+ // (由 installWorkspaceConfig 部署),沙箱内只需 push(推送)与 close(关闭)
53
+ const SANDBOX_WORKTREE_SKILLS = ['openspec-push-worktrees', 'openspec-close-worktree'];
54
+
55
+ // 装到沙箱的 tools skills:cbb-wiki-ops 装用户级(与项目无关),Java 工具不装
56
+ const SANDBOX_TOOLS_SKILLS = ['cbb-design-to-wiki'];
57
+
58
+ // settings.json hook 工具
59
+ const settingsManager = require('../utils/settings');
60
+
61
+ // 清单驱动的文件清理决策(保护用户文档 CLAUDE.md / AGENTS.md 等)
62
+ const { pathsToDelete, protectedKeptPaths } = require('./cleanup');
63
+
64
+ // .gitignore 条目决策(纯函数:检测/写入/卸载移除)
65
+ const gitignore = require('../utils/gitignore');
66
+
67
+ // 业务空间配置(cbb setup 命令专用)
68
+ const workspaces = require('./workspaces');
69
+
70
+ // 业务空间选择 UI(cbb setup 命令专用,inquirer 风格 checkbox)
71
+ const checkbox = require('../utils/checkbox');
72
+
73
+ // 版本工具(cbb setup 升级确认 + check-update 共用)
74
+ const version = require('../utils/version');
75
+
76
+ // Superpowers skills 管理(内置集成 + cbb update --upstream-superpowers)
77
+ const superpowers = require('../superpowers');
78
+
79
+ // OpenSpec 工作流管理(内置集成 + cbb update --upstream-openspec)
80
+ const openspec = require('../openspec');
81
+
82
+ // ── 颜色与输出辅助 ──────────────────────────────────────
83
+
84
+ const { COLOR, COLOR_ENABLED, c, log, section, field, LOG_STYLES } = require('../utils/output');
85
+
86
+ // 检测是否已安装(清单文件存在即视为已安装)
87
+ function detectExistingInstall() {
88
+ return fs.existsSync(path.join(TARGET_DIR, MANIFEST_FILE));
89
+ }
90
+
91
+ // ── 清单管理 ──────────────────────────────────────────────
92
+
93
+ /** 读取清单,返回 Set<path> */
94
+ function readManifest() {
95
+ const manifestPath = path.join(TARGET_DIR, MANIFEST_FILE);
96
+ if (!fs.existsSync(manifestPath)) return new Set();
97
+ const data = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
98
+ return new Set(data.paths || []);
99
+ }
100
+
101
+ /** 写入清单 */
102
+ function writeManifest(paths, selectedAdapters) {
103
+ const manifestDir = path.join(TARGET_DIR, MANIFEST_DIR);
104
+ fs.mkdirSync(manifestDir, { recursive: true });
105
+ const manifestPath = path.join(TARGET_DIR, MANIFEST_FILE);
106
+ const pkg = require(path.join(SHARED_STANDARDS_DIR, 'package.json'));
107
+
108
+ // 读取现有清单,保留未选中适配器的版本
109
+ let existingAdapterVersions = {};
110
+ let existingGlobalVersion = null;
111
+ if (fs.existsSync(manifestPath)) {
112
+ try {
113
+ const existing = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
114
+ if (existing.adapterVersions && typeof existing.adapterVersions === 'object') {
115
+ existingAdapterVersions = existing.adapterVersions;
116
+ }
117
+ existingGlobalVersion = existing.version || null;
118
+ } catch (_) { /* ignore parse errors */ }
119
+ }
120
+
121
+ // 首次迁移:旧清单无 adapterVersions 时,用旧全局 version 为未选中的适配器补齐
122
+ if (Object.keys(existingAdapterVersions).length === 0 && existingGlobalVersion) {
123
+ adapters.forEach(a => {
124
+ if (!selectedAdapters.some(sa => sa.name === a.name)) {
125
+ existingAdapterVersions[a.name] = existingGlobalVersion;
126
+ }
127
+ });
128
+ }
129
+
130
+ // 合并:选中的适配器写入新版本,未选中的保持不变
131
+ const adapterVersions = { ...existingAdapterVersions };
132
+ selectedAdapters.forEach(adapter => {
133
+ adapterVersions[adapter.name] = pkg.version;
134
+ });
135
+
136
+ const data = {
137
+ version: pkg.version,
138
+ adapterVersions,
139
+ paths: [...paths].sort()
140
+ };
141
+ fs.writeFileSync(manifestPath, JSON.stringify(data, null, 2) + '\n');
142
+ }
143
+
144
+ /** 读取上次选择的工具列表,返回 Set<adapterName> */
145
+ function readLastTools() {
146
+ const filePath = path.join(TARGET_DIR, LAST_TOOLS_FILE);
147
+ if (!fs.existsSync(filePath)) return null;
148
+ try {
149
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
150
+ return new Set(data.tools || []);
151
+ } catch (_) {
152
+ return null;
153
+ }
154
+ }
155
+
156
+ /** 保存本次选择的工具列表 */
157
+ function saveLastTools(selectedAdapters) {
158
+ const dirPath = path.join(TARGET_DIR, MANIFEST_DIR);
159
+ fs.mkdirSync(dirPath, { recursive: true });
160
+ const filePath = path.join(TARGET_DIR, LAST_TOOLS_FILE);
161
+ const data = { tools: selectedAdapters.map(a => a.name) };
162
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
163
+ }
164
+
165
+ /** 删除文件或目录 */
166
+ function removePath(targetPath) {
167
+ if (!fs.existsSync(targetPath)) return;
168
+ if (fs.statSync(targetPath).isDirectory()) {
169
+ fs.rmSync(targetPath, { recursive: true, force: true });
170
+ } else {
171
+ fs.unlinkSync(targetPath);
172
+ }
173
+ }
174
+
175
+ /**
176
+ * 安全删除目标路径(文件或目录)
177
+ * 在清单中 → 直接删除(本包旧版本)
178
+ * 不在清单中 → 先备份再删除(用户自建)
179
+ */
180
+ function safeRemove(targetPath, managedPaths) {
181
+ if (!fs.existsSync(targetPath)) return;
182
+ const relativePath = path.relative(TARGET_DIR, targetPath).replace(/\\/g, '/');
183
+
184
+ if (managedPaths.has(relativePath)) {
185
+ // 本包旧版本 → 直接删除
186
+ removePath(targetPath);
187
+ } else {
188
+ // 用户自建 → 先备份
189
+ const backupPath = generateBackupPath(targetPath);
190
+ if (fs.statSync(targetPath).isDirectory()) {
191
+ fs.cpSync(targetPath, backupPath, { recursive: true });
192
+ } else {
193
+ fs.copyFileSync(targetPath, backupPath);
194
+ }
195
+ log(`备份 ${relativePath} → ${path.basename(backupPath)}`, 'success');
196
+ removePath(targetPath);
197
+ }
198
+ }
199
+
200
+ // ── 交互式选择工具 ─────────────────────────────────────────
201
+
202
+ function ask(question) {
203
+ const rl = readline.createInterface({
204
+ input: process.stdin,
205
+ output: process.stdout,
206
+ });
207
+ return new Promise(resolve => {
208
+ rl.question(question, answer => {
209
+ rl.close();
210
+ resolve(answer.trim());
211
+ });
212
+ });
213
+ }
214
+
215
+ async function selectTools(isUpgrade) {
216
+ const action = isUpgrade ? '升级' : '安装';
217
+
218
+ // 读取上次选择的工具:有记录则预选上次选择,首次安装则全部不选
219
+ const lastTools = readLastTools();
220
+ const preSelected = lastTools
221
+ ? adapters.map((a, i) => lastTools.has(a.name) ? i : -1).filter(i => i >= 0)
222
+ : [];
223
+
224
+ // checkbox 多选:
225
+ // - 不切换 → 确认当前选择
226
+ // - 取消某项 → 只装剩下的
227
+ // - 全部不选 → 提示后回退全部
228
+ const selectedIdx = await checkbox.promptCheckbox({
229
+ header: `🔧 选择要${action}的 AI 工具(空格切换,回车确认)`,
230
+ items: adapters.map(a => ({
231
+ label: c('cyan', a.displayName),
232
+ })),
233
+ preSelected,
234
+ });
235
+
236
+ let result;
237
+ if (selectedIdx.size === 0) {
238
+ log(`未选择任何工具,默认全部${action}`, 'warn');
239
+ result = adapters;
240
+ } else {
241
+ // 转成有序适配器数组(按原始顺序)
242
+ result = [...selectedIdx]
243
+ .sort((a, b) => a - b)
244
+ .map(i => adapters[i]);
245
+ }
246
+
247
+ // 保存本次选择,供下次运行时预选
248
+ saveLastTools(result);
249
+ return result;
250
+ }
251
+
252
+ // ── 分发核心规则 ──────────────────────────────────────────────
253
+
254
+ function deployCoreRules(adapter, isUpgrade, managedPaths) {
255
+ const rulesSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'dev-standards', 'rules');
256
+ const rulesDest = path.join(TARGET_DIR, adapter.rulesDir);
257
+ const action = isUpgrade ? '更新' : '安装';
258
+
259
+ const coreRules = ['cbb-coding-rule.md', 'cbb-priority.md', 'cbb-ai-behavior.md'];
260
+
261
+ // 确保目标目录存在
262
+ fs.mkdirSync(rulesDest, { recursive: true });
263
+
264
+ // 复制核心规则文件
265
+ coreRules.forEach(f => {
266
+ const srcPath = path.join(rulesSrc, f);
267
+ const destPath = path.join(rulesDest, f);
268
+ if (fs.existsSync(srcPath)) {
269
+ // 同名文件存在时直接覆盖
270
+ removePath(destPath);
271
+
272
+ fs.writeFileSync(destPath, fs.readFileSync(srcPath, 'utf-8'));
273
+ }
274
+ });
275
+
276
+ }
277
+
278
+ // ── YAML 配置合并 ─────────────────────────────────────────
279
+
280
+ function mergeConfig(ourYaml, userYaml) {
281
+ const ours = yaml.load(ourYaml);
282
+ const user = yaml.load(userYaml) || {};
283
+
284
+ // schema: 本包覆盖
285
+ // context: 本包覆盖(单字符串字段)
286
+ // rules: 字段级合并,冲突以本包为准
287
+ const merged = { ...ours };
288
+ if (user.rules) {
289
+ merged.rules = {
290
+ ...user.rules,
291
+ ...ours.rules,
292
+ };
293
+ }
294
+
295
+ return yaml.dump(merged, { lineWidth: -1 });
296
+ }
297
+
298
+ // ── 备份文件命名 ──────────────────────────────────────────
299
+
300
+ /**
301
+ * 生成备份文件路径
302
+ * 格式:config.yaml.YYYYMMDD_NN
303
+ * - 同一日期下序号递增
304
+ * - 序号超过 99 时覆盖最新的备份文件
305
+ * - 新日期从 01 开始
306
+ */
307
+ function generateBackupPath(filePath) {
308
+ const dir = path.dirname(filePath);
309
+ const baseName = path.basename(filePath);
310
+ const today = new Date().toISOString().slice(0, 10).replace(/-/g, ''); // YYYYMMDD
311
+ const prefix = `${baseName}.${today}_`;
312
+
313
+ // 扫描目录,找出今天的备份文件
314
+ let maxSeq = 0;
315
+ if (fs.existsSync(dir)) {
316
+ const files = fs.readdirSync(dir);
317
+ const pattern = new RegExp(`^${prefix}(\\d{2})$`);
318
+ for (const f of files) {
319
+ const match = f.match(pattern);
320
+ if (match) {
321
+ const seq = parseInt(match[1], 10);
322
+ if (seq > maxSeq) maxSeq = seq;
323
+ }
324
+ }
325
+ }
326
+
327
+ // 计算下一个序号
328
+ let nextSeq = maxSeq + 1;
329
+ if (nextSeq > 99) {
330
+ // 超过 99 则覆盖最新的备份文件
331
+ nextSeq = 99;
332
+ }
333
+
334
+ const seqStr = String(nextSeq).padStart(2, '0');
335
+ return path.join(dir, `${prefix}${seqStr}`);
336
+ }
337
+
338
+ // ── 部署 Skills ──────────────────────────────────────────────
339
+
340
+ function deploySkills(adapter, isUpgrade, managedPaths, srcDir, filter) {
341
+ const skillsDir = adapter.skillsDir;
342
+ if (!skillsDir || !fs.existsSync(srcDir)) return { count: 0, names: [] };
343
+
344
+ const skillsDest = path.join(TARGET_DIR, skillsDir);
345
+
346
+ // 不再删除整个目录,只处理同名的 skill 子目录
347
+ fs.mkdirSync(skillsDest, { recursive: true });
348
+
349
+ // 复制技能目录
350
+ const skillDirs = fs.readdirSync(srcDir)
351
+ .filter(f => fs.statSync(path.join(srcDir, f)).isDirectory())
352
+ .filter(f => !filter || filter.includes(f));
353
+
354
+ skillDirs.forEach(skillName => {
355
+ const destDir = path.join(skillsDest, skillName);
356
+ removePath(destDir);
357
+ fs.cpSync(path.join(srcDir, skillName), destDir, { recursive: true });
358
+ });
359
+
360
+ return { count: skillDirs.length, names: skillDirs };
361
+ }
362
+
363
+ // ── 部署 Commands ──────────────────────────────────────────────
364
+
365
+ function deployCommands(adapter, isUpgrade, managedPaths, srcDir, filter) {
366
+ const commandsDir = adapter.commandsDir;
367
+ if (!commandsDir || !fs.existsSync(srcDir)) return { count: 0, names: [] };
368
+
369
+ const commandsDest = path.join(TARGET_DIR, commandsDir);
370
+
371
+ fs.mkdirSync(commandsDest, { recursive: true });
372
+
373
+ // 读取所有 .md 文件(command 是扁平文件,不像 skill 是子目录)
374
+ const commandFiles = fs.readdirSync(srcDir)
375
+ .filter(f => f.endsWith('.md') && fs.statSync(path.join(srcDir, f)).isFile())
376
+ .filter(f => !filter || filter.includes(f));
377
+
378
+ commandFiles.forEach(f => {
379
+ const srcPath = path.join(srcDir, f);
380
+ const destPath = path.join(commandsDest, f);
381
+ removePath(destPath);
382
+ fs.copyFileSync(srcPath, destPath);
383
+ });
384
+
385
+ return { count: commandFiles.length, names: commandFiles };
386
+ }
387
+
388
+ // ── 设置 OpenSpec 工作流 ──────────────────────────────────
389
+
390
+ function setupOpenSpec(isUpgrade, managedPaths) {
391
+ const schemasSrcs = [
392
+ path.join(SHARED_STANDARDS_DIR, 'config', 'openspec', 'schemas'),
393
+ ];
394
+ const schemasDest = path.join(TARGET_DIR, 'openspec', 'schemas');
395
+
396
+ // 安装 schema:本包提供的直接覆盖,非本包的不动
397
+ // 收集所有源目录的 schema 名称
398
+ const ourSchemaNames = new Set();
399
+ schemasSrcs.forEach(src => {
400
+ if (fs.existsSync(src)) {
401
+ fs.readdirSync(src).filter(f => fs.statSync(path.join(src, f)).isDirectory())
402
+ .forEach(f => ourSchemaNames.add(f));
403
+ }
404
+ });
405
+
406
+ if (ourSchemaNames.size > 0) {
407
+ fs.mkdirSync(schemasDest, { recursive: true });
408
+
409
+ // 处理目标目录中已有的 schema
410
+ if (fs.existsSync(schemasDest)) {
411
+ const existingNames = fs.readdirSync(schemasDest)
412
+ .filter(f => fs.statSync(path.join(schemasDest, f)).isDirectory());
413
+
414
+ existingNames.forEach(name => {
415
+ const dest = path.join(schemasDest, name);
416
+ const relativePath = `openspec/schemas/${name}`;
417
+
418
+ if (ourSchemaNames.has(name)) {
419
+ // 本包 schema → 直接删除后覆盖(无论是否在清单中)
420
+ removePath(dest);
421
+ } else if (managedPaths.has(relativePath)) {
422
+ // 清单中有但本包不再提供 → 清理旧版本
423
+ removePath(dest);
424
+ log(`移除已废弃的 openspec/schemas/${name}/`, 'success');
425
+ }
426
+ // 非本包且不在清单中 → 不动(用户自建)
427
+ });
428
+ }
429
+
430
+ // 复制本包 schema(从所有源目录)
431
+ schemasSrcs.forEach(src => {
432
+ if (!fs.existsSync(src)) return;
433
+ fs.readdirSync(src).filter(f => fs.statSync(path.join(src, f)).isDirectory())
434
+ .forEach(name => {
435
+ const srcPath = path.join(src, name);
436
+ const dest = path.join(schemasDest, name);
437
+ fs.cpSync(srcPath, dest, { recursive: true });
438
+ log(`安装 openspec/schemas/${name}/`, 'success');
439
+ });
440
+ });
441
+ }
442
+
443
+ // 处理用户项目的 openspec/config.yaml
444
+ const configSrc = path.join(SHARED_STANDARDS_DIR, 'config', 'openspec', 'config.yaml');
445
+ const configDest = path.join(TARGET_DIR, 'openspec', 'config.yaml');
446
+
447
+ if (!fs.existsSync(configSrc)) return;
448
+
449
+ fs.mkdirSync(path.dirname(configDest), { recursive: true });
450
+ const ourContent = fs.readFileSync(configSrc, 'utf-8');
451
+
452
+ if (fs.existsSync(configDest)) {
453
+ // 合并:rules 字段级合并,其余以本包为准
454
+ const userContent = fs.readFileSync(configDest, 'utf-8');
455
+ const merged = mergeConfig(ourContent, userContent);
456
+ fs.writeFileSync(configDest, merged);
457
+ log('合并 openspec/config.yaml', 'success');
458
+ } else {
459
+ fs.writeFileSync(configDest, ourContent);
460
+ log('创建 openspec/config.yaml', 'success');
461
+ }
462
+
463
+ // 输出已安装的 schema 和可用命令
464
+ if (fs.existsSync(schemasDest)) {
465
+ const defaultSchema = yaml.load(fs.readFileSync(configDest, 'utf-8')).schema;
466
+ const installedSchemas = fs.readdirSync(schemasDest).filter(name => {
467
+ return fs.statSync(path.join(schemasDest, name)).isDirectory();
468
+ });
469
+
470
+ log(`已安装工作流:${installedSchemas.join(', ')}(默认: ${defaultSchema})`);
471
+ }
472
+ }
473
+
474
+ // ── .gitignore 管理 ───────────────────────────────────────
475
+
476
+ function updateGitignore() {
477
+ const gitignorePath = path.join(TARGET_DIR, '.gitignore');
478
+
479
+ let content = '';
480
+ if (fs.existsSync(gitignorePath)) {
481
+ content = fs.readFileSync(gitignorePath, 'utf-8');
482
+ }
483
+
484
+ // 已含某目录(带/不带尾斜杠均认可)则跳过;新增无尾斜杠形式(.claude / .qoder)
485
+ const toAdd = gitignore.entriesToAdd(content);
486
+ if (toAdd.length === 0) return; // 全部已存在
487
+
488
+ // 新增条目用空行与现有内容分隔,另起一行开始
489
+ fs.writeFileSync(gitignorePath, gitignore.appendEntries(content, toAdd));
490
+ log(`添加 ${toAdd.join(', ')} 到 .gitignore`, 'success');
491
+ }
492
+
493
+ /** 从 .gitignore 中移除本包添加的条目(新旧版本写入的形式均覆盖) */
494
+ function cleanupGitignore() {
495
+ const gitignorePath = path.join(TARGET_DIR, '.gitignore');
496
+
497
+ if (!fs.existsSync(gitignorePath)) return;
498
+
499
+ const content = fs.readFileSync(gitignorePath, 'utf-8');
500
+ const lines = content.split(/\r?\n/);
501
+ const filtered = gitignore.filterManaged(lines);
502
+
503
+ if (filtered.length === lines.length) return; // 无需变更
504
+
505
+ // 避免文件末尾堆积空行
506
+ const newContent = filtered.join('\n').replace(/\n{3,}/g, '\n\n');
507
+ fs.writeFileSync(gitignorePath, newContent.endsWith('\n') ? newContent : newContent + '\n');
508
+ log(`从 .gitignore 移除 ${gitignore.GITIGNORE_RULES.map(r => r.add).join(', ')}`, 'success');
509
+ }
510
+
511
+ // ── OpenSpec CLI 自动安装/升级 ──────────────────────────────
512
+
513
+ const OPENSPEC_PKG = '@fission-ai/openspec';
514
+ const OPENSPEC_REGISTRY = 'https://registry.npmmirror.com';
515
+
516
+ /**
517
+ * 确保 OpenSpec CLI 已安装且为最新版本。
518
+ * 未安装或版本非最新时自动执行 npm install -g,无需用户确认。
519
+ */
520
+ async function ensureOpenSpecCli() {
521
+ // 1. 获取当前版本(未安装时 currentVersion = null)
522
+ let currentVersion = null;
523
+ try {
524
+ currentVersion = execSync('openspec --version', { stdio: 'pipe', timeout: 10000 })
525
+ .toString().trim().replace(/^v/, '');
526
+ } catch (_) {}
527
+
528
+ // 2. 查询最新版本(网络失败则跳过检测,视为无法判断)
529
+ let latestVersion = null;
530
+ try {
531
+ latestVersion = execSync(
532
+ `npm view ${OPENSPEC_PKG} version --registry ${OPENSPEC_REGISTRY}`,
533
+ { stdio: 'pipe', timeout: 15000 }
534
+ ).toString().trim();
535
+ } catch (_) {}
536
+
537
+ // 3. 判断是否需要安装/升级
538
+ const needInstall = !currentVersion;
539
+ const needUpgrade = currentVersion && latestVersion
540
+ && version.compareVersions(latestVersion, currentVersion) > 0;
541
+
542
+ if (!needInstall && !needUpgrade) {
543
+ log(`OpenSpec CLI: v${currentVersion} (最新)`, 'success');
544
+ return;
545
+ }
546
+
547
+ // 4. 执行安装/升级
548
+ const action = needInstall ? '安装' : '升级';
549
+ const detail = needInstall
550
+ ? `→ v${latestVersion || 'latest'}`
551
+ : `v${currentVersion} → v${latestVersion}`;
552
+ log(`${action} OpenSpec CLI (${detail})...`, 'info');
553
+
554
+ try {
555
+ execSync(
556
+ `npm install -g ${OPENSPEC_PKG}@latest --registry ${OPENSPEC_REGISTRY}`,
557
+ { stdio: 'pipe', timeout: 120000 }
558
+ );
559
+ log(`OpenSpec CLI ${action}完成`, 'success');
560
+ } catch (err) {
561
+ log(`OpenSpec CLI ${action}失败: ${err.message}`, 'error');
562
+ log(`请手动执行: npm install -g ${OPENSPEC_PKG}@latest --registry ${OPENSPEC_REGISTRY}`, 'warn');
563
+ }
564
+ }
565
+
566
+ /**
567
+ * 检测用户级 superpowers 安装(插件或 npx skills add -g),
568
+ * 发现重复时交互确认是否卸载;-y 模式仅警告不自动操作。
569
+ */
570
+ async function detectUserLevelSuperpowers(selectedAdapters, yes = false) {
571
+ const homeDir = os.homedir();
572
+ const conflicts = []; // { adapter, source, uninstallHint }
573
+ // 动态拼接所有技能名,避免使用集合名(npx skills remove 无法匹配集合实体)
574
+ const skillNames = superpowers.EXPECTED_SKILLS.join(' ');
575
+
576
+ for (const adapter of (selectedAdapters || [])) {
577
+ if (!adapter.skillsDir) continue;
578
+
579
+ // 检查 1: Claude Code 插件注册表(claude skills install superpowers)
580
+ if (adapter.name === 'claude-code') {
581
+ const pluginsFile = path.join(homeDir, '.claude', 'plugins', 'installed_plugins.json');
582
+ if (fs.existsSync(pluginsFile)) {
583
+ try {
584
+ const data = JSON.parse(fs.readFileSync(pluginsFile, 'utf-8'));
585
+ if (Object.keys(data.plugins || {}).some(n => n.startsWith('superpowers'))) {
586
+ conflicts.push({
587
+ adapter,
588
+ source: `~/.claude/plugins/ (插件注册)`,
589
+ uninstallHint: `npx skills remove ${skillNames} -g -y`,
590
+ });
591
+ }
592
+ } catch (_) { /* ignore */ }
593
+ }
594
+ }
595
+
596
+ // 检查 2: 用户级 skills 目录中的标志性 skill(取白名单中的技能名)
597
+ // 来源可能是 npx skills add obra/superpowers -g(Claude Code / Qoder 通用)
598
+ const markerSkills = SUPERPOWERS_WHITELIST.slice(0, 2);
599
+ const userSkillsDir = path.join(homeDir, adapter.skillsDir);
600
+ if (fs.existsSync(userSkillsDir)) {
601
+ for (const marker of markerSkills) {
602
+ const markerPath = path.join(userSkillsDir, marker);
603
+ try {
604
+ if (fs.existsSync(markerPath) && fs.statSync(markerPath).isDirectory()) {
605
+ // 避免与插件检测重复(同一适配器只报一次)
606
+ if (!conflicts.some(c => c.adapter.name === adapter.name)) {
607
+ conflicts.push({
608
+ adapter,
609
+ source: `${adapter.skillsDir}/ (npx skills add -g)`,
610
+ uninstallHint: `npx skills remove ${skillNames} -g -y`,
611
+ });
612
+ }
613
+ break;
614
+ }
615
+ } catch (_) { /* ignore */ }
616
+ }
617
+ }
618
+ }
619
+
620
+ if (conflicts.length === 0) return;
621
+
622
+ console.log('');
623
+ log('检测到用户级 superpowers 安装,会与项目级 skills 重复加载:', 'warn');
624
+ conflicts.forEach(c => {
625
+ log(`${c.adapter.displayName}: ${c.source}`, 'warn');
626
+ });
627
+
628
+ // -y 模式:仅警告,不操作用户环境
629
+ if (yes) {
630
+ log('--yes 模式下不自动操作用户环境,请手动卸载:', 'info');
631
+ conflicts.forEach(c => log(`${c.uninstallHint}`, 'info'));
632
+ return;
633
+ }
634
+
635
+ // 交互模式:询问是否卸载
636
+ console.log('');
637
+ console.log(' 建议卸载用户级 superpowers,由本包统一管理,避免版本不一致和 skill 重复。');
638
+ const answer = await ask('是否现在卸载用户级 superpowers?(y/N): ');
639
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
640
+ log('已跳过,请稍后手动卸载:', 'info');
641
+ conflicts.forEach(c => log(`${c.uninstallHint}`, 'info'));
642
+ return;
643
+ }
644
+
645
+ // 执行卸载
646
+ for (const c of conflicts) {
647
+ log(`执行: ${c.uninstallHint}`, 'info');
648
+ try {
649
+ execSync(c.uninstallHint, { stdio: 'inherit', timeout: 30000 });
650
+ // 卸载后检查用户级 skills 目录是否还有残留
651
+ const userSkillsDir = path.join(os.homedir(), c.adapter.skillsDir);
652
+ const markerSkills = SUPERPOWERS_WHITELIST.slice(0, 2);
653
+ const remaining = markerSkills.filter(m => {
654
+ try { return fs.existsSync(path.join(userSkillsDir, m)); } catch (_) { return false; }
655
+ });
656
+ if (remaining.length > 0) {
657
+ log(`${c.adapter.displayName}: 卸载命令已执行,但仍有残留 skill 目录`, 'warn');
658
+ log(`请手动删除: ${userSkillsDir}`, 'info');
659
+ } else {
660
+ log(`${c.adapter.displayName} 用户级 superpowers 已卸载`, 'success');
661
+ }
662
+ } catch (err) {
663
+ log(`卸载命令执行失败: ${err.message}`, 'error');
664
+ log(`请手动执行: ${c.uninstallHint}`, 'warn');
665
+ }
666
+ }
667
+ }
668
+
669
+ // ── 主流程 ────────────────────────────────────────────────
670
+
671
+ async function init(targetDir, options = {}) {
672
+ const { yes = false } = options;
673
+
674
+ // 允许调用方指定目标目录(cbb setup 在拉好仓库后切到仓库根调用)。
675
+ // 不传则保持模块级 TARGET_DIR(即调用 init 时的 cwd)。
676
+ if (targetDir) TARGET_DIR = targetDir;
677
+
678
+ // 检测是否已安装
679
+ const isUpgrade = detectExistingInstall();
680
+ const action = isUpgrade ? '升级' : '安装';
681
+ const managedPaths = readManifest();
682
+ const newPaths = new Set();
683
+
684
+ console.log(`\n 🚀 ${action} @jspg-ai/coding-bb...\n`);
685
+
686
+ // 先选适配器:后续的前置依赖检测仅针对所选适配器,避免误导(如选了 Qoder 却提示安装 Claude Code 的 Superpowers)
687
+ // 非交互模式判断:显式 -y/--yes 或 stdin 非 TTY(pipe/ignore)均视为非交互
688
+ const isNonInteractive = yes || !process.stdin.isTTY;
689
+ if (yes && process.stdin.isTTY) {
690
+ log('--yes 非交互模式,已默认选中所有适配器', 'info');
691
+ }
692
+ const selectedAdapters = isNonInteractive ? adapters : await selectTools(isUpgrade);
693
+
694
+ // 前置依赖检测(放在选适配器之后,只检测与所选工具相关的依赖)
695
+ await ensureOpenSpecCli();
696
+
697
+ // 分发核心规则到各工具目录
698
+ console.log(`\n 📦 ${action}核心编码规范...`);
699
+ selectedAdapters.forEach(adapter => {
700
+ deployCoreRules(adapter, isUpgrade, managedPaths);
701
+ newPaths.add(`${adapter.rulesDir}/cbb-coding-rule.md`);
702
+ newPaths.add(`${adapter.rulesDir}/cbb-priority.md`);
703
+ newPaths.add(`${adapter.rulesDir}/cbb-ai-behavior.md`);
704
+ });
705
+ log(`核心规则 → ${selectedAdapters.map(a => a.rulesDir).join(', ')} (2 个文件)`, 'success');
706
+
707
+ // 安装 Skills(支持 skills 的适配器)
708
+ const skillsAdapters = selectedAdapters.filter(a => a.skillsDir);
709
+ if (skillsAdapters.length > 0) {
710
+ console.log(`\n 🔧 ${action}按需加载 Skills...`);
711
+ const standardsSkillsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'dev-standards', 'skills');
712
+ const openspecExtendsSkillsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'worktrees', 'skills');
713
+ const extendsSkillsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'tools');
714
+
715
+ const skillsDests = skillsAdapters.map(a => a.skillsDir);
716
+ let stdCount = 0, osCount = 0, extCount = 0;
717
+ skillsAdapters.forEach(adapter => {
718
+ // 通用编码规范 Skills(cbb/dev-standards/skills)
719
+ const stdResult = deploySkills(adapter, isUpgrade, managedPaths, standardsSkillsSrc);
720
+ if (stdResult.count > 0) {
721
+ stdCount = stdResult.count;
722
+ stdResult.names.forEach(name => newPaths.add(`${adapter.skillsDir}/${name}`));
723
+ }
724
+ // 官方 OpenSpec Skills(openspec/skills/)不部署——工作流入口统一为 /opsx:* 斜杠命令
725
+ //(openspec/commands/ 在下方 Commands 段部署),避免双形态重复
726
+ // worktree 扩展 Skills:沙箱内只需 push / close(init/extend 由 workspace 根的子集安装)
727
+ const osExtResult = deploySkills(adapter, isUpgrade, managedPaths, openspecExtendsSkillsSrc, SANDBOX_WORKTREE_SKILLS);
728
+ if (osExtResult.count > 0) {
729
+ osCount = osExtResult.count;
730
+ osExtResult.names.forEach(name => newPaths.add(`${adapter.skillsDir}/${name}`));
731
+ }
732
+ // 工具 Skills:仅 cbb-design-to-wiki(设计文档发布属 openspec 设计阶段动作)
733
+ const extResult = deploySkills(adapter, isUpgrade, managedPaths, extendsSkillsSrc, SANDBOX_TOOLS_SKILLS);
734
+ if (extResult.count > 0) {
735
+ extCount = extResult.count;
736
+ extResult.names.forEach(name => newPaths.add(`${adapter.skillsDir}/${name}`));
737
+ }
738
+ });
739
+ if (stdCount > 0) log(`规范 Skills → ${skillsDests.join(', ')} (${stdCount} 个)`, 'success');
740
+ if (osCount > 0) log(`worktree Skills → ${skillsDests.join(', ')} (${osCount} 个)`, 'success');
741
+ if (extCount > 0) log(`扩展 Skills → ${skillsDests.join(', ')} (${extCount} 个)`, 'success');
742
+
743
+ // 部署 Superpowers Skills(白名单:openspec 工作流依赖 + 方法论基底)
744
+ const superpowersSkillsSrc = superpowers.SUPERPOWERS_SKILLS_DIR;
745
+ let spCount = 0;
746
+ if (fs.existsSync(superpowersSkillsSrc)) {
747
+ skillsAdapters.forEach(adapter => {
748
+ const spResult = deploySkills(adapter, isUpgrade, managedPaths, superpowersSkillsSrc, SUPERPOWERS_WHITELIST);
749
+ if (spResult.count > 0) {
750
+ spCount = spResult.count;
751
+ spResult.names.forEach(name => newPaths.add(`${adapter.skillsDir}/${name}`));
752
+ }
753
+ });
754
+ if (spCount > 0) log(`Superpowers Skills → ${skillsDests.join(', ')} (${spCount}/${superpowers.getSkillCount()} 个,白名单)`, 'success');
755
+ } else {
756
+ log('superpowers/skills/ 目录不存在,跳过 Superpowers Skills 部署', 'warn');
757
+ log('可运行 cbb update --upstream-superpowers 下载最新 skills', 'warn');
758
+ }
759
+
760
+ // 检测用户级 superpowers 安装,避免与项目级重复导致 skill 冲突
761
+ await detectUserLevelSuperpowers(selectedAdapters, yes);
762
+ }
763
+
764
+ // 安装 Commands(官方 12 个 + worktree 扩展;worktree 命令仅装 close/push,
765
+ // init/extend 只在 workspace 根使用,由 installWorkspaceConfig 部署)
766
+ const commandsAdapters = selectedAdapters.filter(a => a.commandsDir);
767
+ if (commandsAdapters.length > 0) {
768
+ console.log(`\n ⚡ ${action} Commands...`);
769
+ const commandsBaseSrc = path.join(SHARED_STANDARDS_DIR, 'openspec', 'commands');
770
+ const commandsExtendsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'worktrees', 'commands');
771
+ const cmdFilter = ['close.md', 'push.md'];
772
+
773
+ const cmdDests = commandsAdapters.map(a => a.commandsDir);
774
+ let cmdCount = 0;
775
+ commandsAdapters.forEach(adapter => {
776
+ let adapterCmdCount = 0;
777
+ // 官方 Commands(openspec/commands/)— 始终部署
778
+ const baseResult = deployCommands(adapter, isUpgrade, managedPaths, commandsBaseSrc);
779
+ if (baseResult.count > 0) {
780
+ adapterCmdCount += baseResult.count;
781
+ baseResult.names.forEach(name => newPaths.add(`${adapter.commandsDir}/${name}`));
782
+ }
783
+ // worktree 扩展 Commands(cbb/worktrees/commands/)
784
+ const extResult = deployCommands(adapter, isUpgrade, managedPaths, commandsExtendsSrc, cmdFilter);
785
+ if (extResult.count > 0) {
786
+ adapterCmdCount += extResult.count;
787
+ extResult.names.forEach(name => newPaths.add(`${adapter.commandsDir}/${name}`));
788
+ }
789
+ cmdCount = Math.max(cmdCount, adapterCmdCount);
790
+ });
791
+ if (cmdCount > 0) log(`Commands → ${cmdDests.join(', ')} (${cmdCount} 个)`, 'success');
792
+ }
793
+
794
+ // 设置 OpenSpec 工作流
795
+ console.log(`\n 📋 设置 OpenSpec 工作流...`);
796
+ const schemasSrcs = [
797
+ path.join(SHARED_STANDARDS_DIR, 'config', 'openspec', 'schemas'),
798
+ ];
799
+ schemasSrcs.forEach(src => {
800
+ if (fs.existsSync(src)) {
801
+ fs.readdirSync(src).filter(f => fs.statSync(path.join(src, f)).isDirectory())
802
+ .forEach(name => newPaths.add(`openspec/schemas/${name}`));
803
+ }
804
+ });
805
+ setupOpenSpec(isUpgrade, managedPaths);
806
+ newPaths.add('openspec/config.yaml');
807
+
808
+ // 写入自动版本检查 hook
809
+ const hookAdapters = selectedAdapters.filter(a => a.settingsFile && a.hookEvents);
810
+ if (hookAdapters.length > 0) {
811
+ console.log(`\n 🔔 配置自动版本检查...`);
812
+ const command = settingsManager.getCheckUpdateCommand();
813
+ hookAdapters.forEach(adapter => {
814
+ // 用户级 settings 用绝对路径,不写入项目清单
815
+ const settingsPath = adapter.settingsFileIsUserLevel
816
+ ? adapter.settingsFile
817
+ : path.join(TARGET_DIR, adapter.settingsFile);
818
+ settingsManager.addHooks(settingsPath, command, adapter.hookEvents);
819
+ // 清理旧版本遗留的 SessionStart hook(现已仅安装 UserPromptSubmit)
820
+ if (settingsManager.removeLegacyHooks(settingsPath)) {
821
+ log(`清理 ${adapter.displayName} 遗留的 SessionStart hook`, 'success');
822
+ }
823
+ if (!adapter.settingsFileIsUserLevel) newPaths.add(adapter.settingsFile);
824
+ });
825
+ log(`版本检查 hook → ${hookAdapters.map(a => a.displayName).join(', ')} (${hookAdapters[0].hookEvents.join(', ')})`, 'success');
826
+ }
827
+
828
+ // 升级时清理旧路径(在旧清单中但不在新路径集中)。
829
+ // 注意:CLAUDE.md / AGENTS.md 等用户文档受保护,即使旧清单里有也不删除。
830
+ if (isUpgrade) {
831
+ const oldPaths = pathsToDelete(managedPaths, newPaths);
832
+ if (oldPaths.length > 0) {
833
+ section('🧹', `清理上一版本残留(${oldPaths.length} 个)`);
834
+ // 先全部删除
835
+ oldPaths.forEach(oldPath => {
836
+ const targetPath = path.join(TARGET_DIR, oldPath);
837
+ if (fs.existsSync(targetPath)) removePath(targetPath);
838
+ });
839
+ // 再展示列表:超过 5 个则折叠为汇总,避免输出过长
840
+ const MAX_DISPLAY = 5;
841
+ oldPaths.slice(0, MAX_DISPLAY).forEach(p => log(` ${p}`, 'dim'));
842
+ if (oldPaths.length > MAX_DISPLAY) {
843
+ log(` ... 还有 ${oldPaths.length - MAX_DISPLAY} 个`, 'dim');
844
+ }
845
+ }
846
+
847
+ const kept = protectedKeptPaths(managedPaths, newPaths);
848
+ if (kept.length > 0) {
849
+ section('🛡️', '保留用户文档');
850
+ kept.forEach(k => {
851
+ log(` ${k}(本包已不再管理,文件可能含用户内容,未删除)`, 'warn');
852
+ });
853
+ }
854
+
855
+ // 清理旧版本遗留的 skills/node_modules/(v1.0.4-zjc.14 起 koffi 改为从全局位置加载)
856
+ skillsAdapters.forEach(adapter => {
857
+ const legacyNodeModules = path.join(TARGET_DIR, adapter.skillsDir, 'node_modules');
858
+ if (fs.existsSync(legacyNodeModules)) {
859
+ removePath(legacyNodeModules);
860
+ log(`清理遗留 ${adapter.skillsDir}/node_modules/`, 'success');
861
+ }
862
+ });
863
+ }
864
+
865
+ // 写入清单
866
+ writeManifest(newPaths, selectedAdapters);
867
+
868
+ // 更新 .gitignore
869
+ updateGitignore();
870
+
871
+ // 完成摘要
872
+ console.log(`\n ✅ ${action}完成!\n`);
873
+
874
+ if (yes) {
875
+ console.log(c('dim', ' (使用 --yes 非交互模式,已默认选中所有适配器)'));
876
+ }
877
+
878
+ // 适配器映射(单行)
879
+ const adapterSummary = selectedAdapters.map(a => `${a.displayName} → ${a.rulesDir}/`).join(' | ');
880
+ console.log(` ${adapterSummary}`);
881
+
882
+ // OpenSpec 摘要(单行)
883
+ const schemasDest = path.join(TARGET_DIR, 'openspec', 'schemas');
884
+ let schemaCount = 0;
885
+ let defaultSchema = '';
886
+ if (fs.existsSync(schemasDest)) {
887
+ const configPath = path.join(TARGET_DIR, 'openspec', 'config.yaml');
888
+ if (fs.existsSync(configPath)) {
889
+ defaultSchema = yaml.load(fs.readFileSync(configPath, 'utf-8')).schema || '';
890
+ }
891
+ schemaCount = fs.readdirSync(schemasDest).filter(name =>
892
+ fs.statSync(path.join(schemasDest, name)).isDirectory()
893
+ ).length;
894
+ }
895
+ console.log(` OpenSpec → openspec/config.yaml + ${schemaCount} 个工作流${defaultSchema ? ` (${defaultSchema} 默认)` : ''}`);
896
+
897
+ // 附加组件(单行)
898
+ const hasSkills = selectedAdapters.some(a => a.skillsDir);
899
+ const hasCommands = selectedAdapters.some(a => a.commandsDir);
900
+ const hasHooks = selectedAdapters.some(a => a.settingsFile && a.hookEvents);
901
+ const extras = [];
902
+ if (hasSkills) extras.push('Skills');
903
+ if (hasCommands) extras.push('Commands');
904
+ if (hasHooks) extras.push('版本检查 hook');
905
+ if (extras.length > 0) {
906
+ console.log(` ${extras.join(' / ')} 已配置`);
907
+ }
908
+ console.log('');
909
+ }
910
+
911
+ /**
912
+ * 为 workspace 根目录安装 AI 配置子集(由 cbb setup 调用)。
913
+ *
914
+ * 只装 worktree 生命周期管理(init/extend/close/push)+ openspec 运行时配置,
915
+ * 不装 base 工作流、编码规范、superpowers、版本检查 hook--这些属于 sandbox worktree
916
+ * 内的开发配置,由 worktree 内 `cbb init --yes` 完整模式安装。在 workspace 根目录
917
+ * (主分支)装 base 工作流会诱导绕过 worktree 直接在主分支做需求开发。
918
+ */
919
+ async function installWorkspaceConfig(targetDir, options = {}) {
920
+ const { yes = false } = options;
921
+ if (targetDir) TARGET_DIR = targetDir;
922
+
923
+ const isUpgrade = detectExistingInstall();
924
+ const action = isUpgrade ? '升级' : '安装';
925
+ const managedPaths = readManifest();
926
+ const newPaths = new Set();
927
+
928
+ console.log(`\n 🚀 ${action} workspace AI 配置(worktree 管理子集)...\n`);
929
+
930
+ const isNonInteractive = yes || !process.stdin.isTTY;
931
+ if (yes && process.stdin.isTTY) {
932
+ log('--yes 非交互模式,已默认选中所有适配器', 'info');
933
+ }
934
+ const selectedAdapters = isNonInteractive ? adapters : await selectTools(isUpgrade);
935
+ await ensureOpenSpecCli();
936
+
937
+ // 只装 worktree 生命周期管理 Skills(init/extend/close/push)
938
+ const skillsAdapters = selectedAdapters.filter(a => a.skillsDir);
939
+ if (skillsAdapters.length > 0) {
940
+ console.log(`\n 🔧 ${action} worktree 管理 Skills...`);
941
+ const openspecExtendsSkillsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'worktrees', 'skills');
942
+ const osExtFilter = ['openspec-init-worktree', 'openspec-extend-worktree', 'openspec-close-worktree', 'openspec-push-worktrees'];
943
+ const skillsDests = skillsAdapters.map(a => a.skillsDir);
944
+ let count = 0;
945
+ skillsAdapters.forEach(adapter => {
946
+ const r = deploySkills(adapter, isUpgrade, managedPaths, openspecExtendsSkillsSrc, osExtFilter);
947
+ if (r.count > 0) {
948
+ count += r.count;
949
+ r.names.forEach(name => newPaths.add(`${adapter.skillsDir}/${name}`));
950
+ }
951
+ });
952
+ if (count > 0) log(`worktree 管理 Skills → ${skillsDests.join(', ')} (${count} 个)`, 'success');
953
+ }
954
+
955
+ // 只装 worktree 管理 Commands(init/extend/close/push)
956
+ const commandsAdapters = selectedAdapters.filter(a => a.commandsDir);
957
+ if (commandsAdapters.length > 0) {
958
+ console.log(`\n ⚡ ${action} worktree 管理 Commands...`);
959
+ const commandsExtendsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'worktrees', 'commands');
960
+ const cmdFilter = ['init.md', 'extend.md', 'close.md', 'push.md'];
961
+ const cmdDests = commandsAdapters.map(a => a.commandsDir);
962
+ let count = 0;
963
+ commandsAdapters.forEach(adapter => {
964
+ const r = deployCommands(adapter, isUpgrade, managedPaths, commandsExtendsSrc, cmdFilter);
965
+ if (r.count > 0) {
966
+ count += r.count;
967
+ r.names.forEach(name => newPaths.add(`${adapter.commandsDir}/${name}`));
968
+ }
969
+ });
970
+ if (count > 0) log(`worktree 管理 Commands → ${cmdDests.join(', ')} (${count} 个)`, 'success');
971
+ }
972
+
973
+ // 设置 OpenSpec 工作流(config.yaml + schemas)
974
+ console.log(`\n 📋 设置 OpenSpec 工作流...`);
975
+ const schemasSrcs = [
976
+ path.join(SHARED_STANDARDS_DIR, 'config', 'openspec', 'schemas'),
977
+ ];
978
+ schemasSrcs.forEach(src => {
979
+ if (fs.existsSync(src)) {
980
+ fs.readdirSync(src).filter(f2 => fs.statSync(path.join(src, f2)).isDirectory())
981
+ .forEach(name => newPaths.add(`openspec/schemas/${name}`));
982
+ }
983
+ });
984
+ setupOpenSpec(isUpgrade, managedPaths);
985
+ newPaths.add('openspec/config.yaml');
986
+
987
+ // 升级时清理旧路径(workspace 曾装过的 base 工作流等不属于此子集,删除)
988
+ if (isUpgrade) {
989
+ const oldPaths = pathsToDelete(managedPaths, newPaths);
990
+ if (oldPaths.length > 0) {
991
+ section('🧹', `清理上一版本残留(${oldPaths.length} 个)`);
992
+ oldPaths.forEach(oldPath => {
993
+ const targetPath = path.join(TARGET_DIR, oldPath);
994
+ if (fs.existsSync(targetPath)) removePath(targetPath);
995
+ });
996
+ const MAX_DISPLAY = 5;
997
+ oldPaths.slice(0, MAX_DISPLAY).forEach(p => log(` ${p}`, 'dim'));
998
+ if (oldPaths.length > MAX_DISPLAY) log(` ... 还有 ${oldPaths.length - MAX_DISPLAY} 个`, 'dim');
999
+ }
1000
+ const kept = protectedKeptPaths(managedPaths, newPaths);
1001
+ if (kept.length > 0) {
1002
+ section('🛡️', '保留用户文档');
1003
+ kept.forEach(k => log(` ${k}(本包已不再管理,文件可能含用户内容,未删除)`, 'warn'));
1004
+ }
1005
+ (skillsAdapters || []).forEach(adapter => {
1006
+ const legacyNodeModules = path.join(TARGET_DIR, adapter.skillsDir, 'node_modules');
1007
+ if (fs.existsSync(legacyNodeModules)) {
1008
+ removePath(legacyNodeModules);
1009
+ log(`清理遗留 ${adapter.skillsDir}/node_modules/`, 'success');
1010
+ }
1011
+ });
1012
+ }
1013
+
1014
+ writeManifest(newPaths, selectedAdapters);
1015
+ updateGitignore();
1016
+
1017
+ console.log(`\n ✅ ${action}完成!\n`);
1018
+ const adapterSummary = selectedAdapters.map(a => `${a.displayName} → ${a.skillsDir}/`).join(' | ');
1019
+ console.log(` ${adapterSummary}`);
1020
+ console.log(' workspace 子集:worktree 管理 Skills/Commands + openspec 配置(不含 base 工作流/编码规范/superpowers)');
1021
+ console.log('');
1022
+ }
1023
+
1024
+ // ── cbb setup 命令 ──────────────────────────────────────
1025
+
1026
+ /**
1027
+ * 若 targetDir 已安装(.cbb/.managed-by-cbb 存在),输出当前版本号
1028
+ * 并尝试从 registry 查询最新版本,让用户确认是否继续。
1029
+ * - 用户输入 n / no → 返回 false(调用方决定是跳过还是退出)
1030
+ * - 未安装 / 查询失败 / 用户确认 → 返回 true
1031
+ */
1032
+ async function confirmUpgradeIfInstalled(targetDir) {
1033
+ const installed = version.readInstalledVersion(targetDir);
1034
+ if (!installed) return true; // 未安装:不提示,直接继续
1035
+
1036
+ // 根据当前包版本(而非已安装版本)决定查询哪个 tag:
1037
+ // 反查当前版本所属的 dist-tag(如 zjc / beta / latest),不依赖版本号格式
1038
+ const pkg = require(path.join(SHARED_STANDARDS_DIR, 'package.json'));
1039
+ let tag = 'latest';
1040
+ try {
1041
+ tag = await version.findTagForVersion(pkg.version) || 'latest';
1042
+ } catch (_) {
1043
+ // 网络失败静默降级:使用 latest
1044
+ }
1045
+
1046
+ let latest = null;
1047
+ try {
1048
+ latest = await version.fetchLatest(tag);
1049
+ } catch (_) {
1050
+ // 网络失败静默降级:latest 保持 null
1051
+ }
1052
+
1053
+ section('📦', '检测到已安装版本');
1054
+ field('当前', `v${installed}`, 'cyan');
1055
+ if (latest && version.compareVersions(latest, installed) > 0) {
1056
+ field('最新', `v${latest}`, 'green');
1057
+ console.log('');
1058
+ } else if (latest) {
1059
+ field('最新', `v${latest}`, 'gray');
1060
+ console.log(c('gray', ' (已是最新,无需升级)'));
1061
+ console.log('');
1062
+ // 已是最新,但仍询问一次确认,避免用户误以为没生效
1063
+ latest = null;
1064
+ } else {
1065
+ field('最新', c('yellow', '无法检查(网络异常)'));
1066
+ console.log('');
1067
+ }
1068
+
1069
+ const prompt = latest
1070
+ ? `是否升级到 v${latest}?(Y/n): `
1071
+ : '是否重新安装?(Y/n): ';
1072
+ const answer = await ask(prompt);
1073
+ if (answer && answer.toLowerCase() === 'n') {
1074
+ log('已取消安装/升级', 'warn');
1075
+ return false;
1076
+ }
1077
+ return true;
1078
+ }
1079
+
1080
+ /**
1081
+ * 检查业务空间根目录下是否有 CONFIG.json(init-worktree 技能依赖)。
1082
+ * 不存在时,将包内样例文件复制为 CONFIG.json,提示用户编辑为实际配置。
1083
+ */
1084
+ function ensureWorkspaceConfig(targetDir, workspaceName) {
1085
+ const configPath = path.join(targetDir, 'CONFIG.json');
1086
+ if (fs.existsSync(configPath)) {
1087
+ log(`工作空间「${workspaceName}」的 CONFIG.json 已存在,跳过初始化`, 'success');
1088
+ return;
1089
+ }
1090
+
1091
+ // 复制样例文件作为 CONFIG.json
1092
+ const samplePath = path.join(SHARED_STANDARDS_DIR, workspaces.SAMPLE_CONFIG);
1093
+ fs.copyFileSync(samplePath, configPath);
1094
+ log(`工作空间「${workspaceName}」未找到 CONFIG.json,已从样例文件复制`, 'warn');
1095
+ log('请编辑 CONFIG.json,将示例 apps 替换为实际关联应用', 'warn');
1096
+ }
1097
+
1098
+ /**
1099
+ * 读取 CONFIG.json 中的应用列表,将关联应用代码 clone/fetch 到 .code/ 目录。
1100
+ * 首次 clone(耗时较长),后续 fetch --all(秒级更新)。
1101
+ * 这是基准代码,init-worktree 技能后续只做轻量 fetch 检查。
1102
+ */
1103
+ function syncAppRepos(targetDir) {
1104
+ const configPath = path.join(targetDir, 'CONFIG.json');
1105
+ if (!fs.existsSync(configPath)) return;
1106
+
1107
+ let config;
1108
+ try {
1109
+ config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
1110
+ } catch (e) {
1111
+ log('CONFIG.json 解析失败,跳过应用代码拉取', 'warn');
1112
+ return;
1113
+ }
1114
+
1115
+ const apps = config.apps;
1116
+ if (!Array.isArray(apps) || apps.length === 0) {
1117
+ log('CONFIG.json 中 apps 为空,跳过 .code/ 拉取', 'dim');
1118
+ return;
1119
+ }
1120
+
1121
+ const codeDir = path.join(targetDir, '.code');
1122
+ fs.mkdirSync(codeDir, { recursive: true });
1123
+
1124
+ let ok = 0, fail = 0;
1125
+ apps.forEach(app => {
1126
+ if (!app.name || !app.repo) {
1127
+ log(`应用 "${app.name || '(未知)'}" 缺少 name 或 repo,跳过`, 'warn');
1128
+ fail++;
1129
+ return;
1130
+ }
1131
+
1132
+ const appDir = path.join(codeDir, app.name);
1133
+ if (fs.existsSync(path.join(appDir, '.git'))) {
1134
+ // 已存在:fetch 更新
1135
+ try {
1136
+ execSync('git fetch --all -q', { cwd: appDir, stdio: 'pipe', timeout: 60000 });
1137
+ log(`${app.name}: 代码已更新(fetch --all)`, 'success');
1138
+ ok++;
1139
+ } catch (e) {
1140
+ log(`${app.name}: fetch 失败(离线或无权限,使用本地已有代码)`, 'warn');
1141
+ ok++; // 不算失败,用本地代码
1142
+ }
1143
+ } else {
1144
+ // 不存在:clone
1145
+ try {
1146
+ execSync(`git clone -q "${app.repo}" "${appDir}"`, { stdio: 'pipe', timeout: 180000 });
1147
+ log(`${app.name}: 代码已拉取(clone)`, 'success');
1148
+ ok++;
1149
+ } catch (e) {
1150
+ log(`${app.name}: clone 失败 — ${app.repo}`, 'error');
1151
+ fail++;
1152
+ }
1153
+ }
1154
+ });
1155
+
1156
+ if (fail > 0) {
1157
+ log(`应用代码拉取完成(成功 ${ok},失败 ${fail})`, 'warn');
1158
+ } else {
1159
+ log(`应用代码已就绪(${ok} 个应用 → .code/)`, 'success');
1160
+ }
1161
+ }
1162
+
1163
+ /**
1164
+ * 读取远程默认分支名(如 "main" / "master" / "develop")。
1165
+ * 用 symbolic-ref refs/remotes/origin/HEAD 探测;取不到返回 null。
1166
+ * 参照 cbb/worktrees/skills/openspec-init-worktree/scripts/create-branches.js:47。
1167
+ */
1168
+ function getRemoteDefaultBranch(cwd) {
1169
+ try {
1170
+ const out = execSync('git symbolic-ref --quiet refs/remotes/origin/HEAD', {
1171
+ cwd,
1172
+ encoding: 'utf-8',
1173
+ stdio: ['ignore', 'pipe', 'pipe'],
1174
+ timeout: 10000,
1175
+ });
1176
+ const ref = out.trim(); // 例: "refs/remotes/origin/main"
1177
+ const prefix = 'refs/remotes/origin/';
1178
+ return ref.startsWith(prefix) ? ref.slice(prefix.length) : null;
1179
+ } catch (_) {
1180
+ return null;
1181
+ }
1182
+ }
1183
+
1184
+ /**
1185
+ * 在「子目录已存在且匹配」分支同步工作空间仓库到远程默认分支的最新。
1186
+ * 流程:fetch → 检测默认分支 → 必要时切换分支 → fast-forward merge。
1187
+ * 任一步失败都抛出,由调用方走错误路径中止整个 cbb setup。
1188
+ */
1189
+ function syncWorkspaceRepo(targetDir) {
1190
+ // 1. fetch
1191
+ try {
1192
+ execSync('git fetch --all -q', {
1193
+ cwd: targetDir,
1194
+ stdio: 'pipe',
1195
+ timeout: 60000,
1196
+ });
1197
+ } catch (e) {
1198
+ log(`git fetch 失败:${e.message}`, 'error');
1199
+ log(`请检查网络或 ${targetDir}/ 的 remote 凭证后重试`, 'error');
1200
+ throw e;
1201
+ }
1202
+
1203
+ // 2. 检测远程默认分支(不假设是 master / main)
1204
+ const defaultBranch = getRemoteDefaultBranch(targetDir);
1205
+ if (!defaultBranch) {
1206
+ log(`无法探测 ${targetDir}/ 的远程默认分支(refs/remotes/origin/HEAD 未设置)`, 'error');
1207
+ log(`请在该目录执行 \`git remote set-head origin --auto\` 后重试`, 'error');
1208
+ throw new Error('DEFAULT_BRANCH_UNKNOWN');
1209
+ }
1210
+
1211
+ // 3. 必要时切换到默认分支
1212
+ const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {
1213
+ cwd: targetDir,
1214
+ encoding: 'utf-8',
1215
+ stdio: ['ignore', 'pipe', 'pipe'],
1216
+ timeout: 10000,
1217
+ }).trim();
1218
+ if (currentBranch !== defaultBranch) {
1219
+ log(`本地当前分支 ${currentBranch} ≠ 远程默认分支 ${defaultBranch},正在切换...`, 'warn');
1220
+ try {
1221
+ execSync(`git checkout -q "${defaultBranch}"`, {
1222
+ cwd: targetDir,
1223
+ stdio: 'pipe',
1224
+ timeout: 30000,
1225
+ });
1226
+ } catch (e) {
1227
+ log(`git checkout ${defaultBranch} 失败:${e.message}`, 'error');
1228
+ log(`请手动切换分支后重试`, 'error');
1229
+ throw e;
1230
+ }
1231
+ }
1232
+
1233
+ // 4. fast-forward merge 到 origin/<默认分支>
1234
+ try {
1235
+ execSync(`git merge --ff-only -q "origin/${defaultBranch}"`, {
1236
+ cwd: targetDir,
1237
+ stdio: 'pipe',
1238
+ timeout: 60000,
1239
+ });
1240
+ log(`已同步 ${path.basename(targetDir)}/ 到 origin/${defaultBranch} 最新(fast-forward)`, 'success');
1241
+ } catch (e) {
1242
+ log(`git merge --ff-only origin/${defaultBranch} 失败:${e.message}`, 'error');
1243
+ log(`本地 ${targetDir}/ 与远程分叉或有未提交改动,请手动处理后重试`, 'error');
1244
+ throw e;
1245
+ }
1246
+ }
1247
+
1248
+ /**
1249
+ * 处理单个业务空间:展示详情 → 计算子目录 → 检测/克隆 → CONFIG.json → 拉取应用代码 → 升级询问 → init()。
1250
+ * 失败时不抛出也不退出,返回结果对象供调用方汇总。
1251
+ * @returns {Promise<{ok: boolean, error?: string, message?: string, subdir?: string, skipped?: boolean}>}
1252
+ */
1253
+ async function initSingleWorkspace(w) {
1254
+ // 1. 展示所选业务空间(单行摘要)
1255
+ console.log(`${c('cyan', '开始配置')} ${c('yellow', w.businessLine)}/${c('bold', w.name)}...`);
1256
+
1257
+ // 2. 计算仓库子目录名(来自 repo URL 的最后一段)
1258
+ const subdirName = workspaces.extractRepoBasename(w.repo);
1259
+ if (!subdirName) {
1260
+ log(`无法从仓库地址 "${w.repo}" 提取目录名`, 'error');
1261
+ return { ok: false, error: 'EXTRACT_FAILED' };
1262
+ }
1263
+
1264
+ const parentDir = process.cwd();
1265
+ const targetDir = path.join(parentDir, subdirName);
1266
+
1267
+ // 3. 检查子目录:复用 / 报错 / 克隆
1268
+ let wasCloned = false;
1269
+ if (fs.existsSync(targetDir)) {
1270
+ const detection = workspaces.detectRepo(targetDir, w.repo);
1271
+ if (detection.state === 'matched') {
1272
+ log(`子目录 ${c('cyan', subdirName + '/')} 已存在且匹配,正在同步远程最新...`, 'info');
1273
+ try {
1274
+ syncWorkspaceRepo(targetDir);
1275
+ } catch (e) {
1276
+ return { ok: false, error: 'PULL_FAILED', message: e.message };
1277
+ }
1278
+ } else {
1279
+ const remoteList = detection.remotes.length
1280
+ ? detection.remotes.map(r => ` ${c('gray', r.name + ':')} ${r.url}`).join('\n')
1281
+ : ` ${c('gray', '(该目录尚未配置 remote)')}`;
1282
+ log(
1283
+ `子目录 ${c('cyan', subdirName + '/')} 已存在但不是所选业务空间 "${w.name}" 的仓库:`,
1284
+ 'error',
1285
+ );
1286
+ console.log(remoteList);
1287
+ console.log(` ${c('gray', '期望 remote:')} ${w.repo}`);
1288
+ console.log('');
1289
+ log('请删除该子目录后重试,或在另一个父目录中运行本命令。', 'warn');
1290
+ return { ok: false, error: 'DIR_MISMATCH' };
1291
+ }
1292
+ } else {
1293
+ console.log(`⏳ 正在克隆仓库到 ${c('cyan', subdirName + '/')} ...`);
1294
+ try {
1295
+ // 在 parentDir 下克隆,git 会自动创建 subdirName 子目录
1296
+ execSync(`git clone "${w.repo}"`, {
1297
+ cwd: parentDir,
1298
+ stdio: 'pipe',
1299
+ timeout: 180000,
1300
+ });
1301
+ } catch (e) {
1302
+ log(`git clone 失败:${e.message}`, 'error');
1303
+ log('请检查:1) 仓库地址是否正确 2) 是否已配置 SSH key 或 GitLab 访问凭证', 'warn');
1304
+ return { ok: false, error: 'CLONE_FAILED', message: e.message };
1305
+ }
1306
+ wasCloned = true;
1307
+ log(`已克隆到 ${c('cyan', subdirName + '/')}`, 'success');
1308
+ }
1309
+
1310
+ // 4. 检查 CONFIG.json(init-worktree 技能依赖),不存在则复制样例文件
1311
+ ensureWorkspaceConfig(targetDir, w.name);
1312
+
1313
+ // 5. 校验 CONFIG.json 格式,不合法则中断并撤销本空间初始化
1314
+ const configPath = path.join(targetDir, 'CONFIG.json');
1315
+ const configValidation = workspaces.validateWorkspaceConfig(configPath);
1316
+ if (!configValidation.valid) {
1317
+ log(`CONFIG.json 校验失败:${configValidation.message}`, 'error');
1318
+ // 撤销本次克隆,使工作空间回到初始状态,用户配好 CONFIG.json 后可重新执行
1319
+ if (wasCloned && fs.existsSync(targetDir)) {
1320
+ removePath(targetDir);
1321
+ log(`已撤销本次克隆(${subdirName}/),请配好 CONFIG.json 后重新执行 cbb setup`, 'warn');
1322
+ } else {
1323
+ log('请修正 CONFIG.json 后重新执行 cbb setup', 'warn');
1324
+ }
1325
+ const samplePath = path.join(SHARED_STANDARDS_DIR, workspaces.SAMPLE_CONFIG);
1326
+ log(`参考格式:${samplePath}`, 'info');
1327
+ return { ok: false, error: 'CONFIG_INVALID', message: configValidation.message };
1328
+ }
1329
+ log('CONFIG.json 校验通过', 'success');
1330
+
1331
+ // 6. 拉取关联应用代码到 .code/(基准代码,首次 clone,后续 fetch 更新)
1332
+ syncAppRepos(targetDir);
1333
+
1334
+ // 7. 若已安装,提示当前版本并询问是否继续;用户拒绝则跳过本 workspace
1335
+ const shouldInstall = await confirmUpgradeIfInstalled(targetDir);
1336
+ if (!shouldInstall) {
1337
+ return { ok: false, error: 'USER_CANCELLED', skipped: true };
1338
+ }
1339
+
1340
+ // 7. 安装 workspace AI 配置子集(worktree 管理技能/命令 + openspec 配置,不含 base 工作流)
1341
+ try {
1342
+ await installWorkspaceConfig(targetDir, { yes: true });
1343
+ return { ok: true, subdir: subdirName };
1344
+ } catch (e) {
1345
+ log(`初始化 ${subdirName} 失败:${e.message}`, 'error');
1346
+ return { ok: false, error: 'INIT_FAILED', message: e.message };
1347
+ }
1348
+ }
1349
+
1350
+ /** 把 initSingleWorkspace 的 error 码翻译成可读的中文描述 */
1351
+ function describeBatchError(r) {
1352
+ switch (r.error) {
1353
+ case 'EXTRACT_FAILED': return '无法从仓库地址提取目录名';
1354
+ case 'DIR_MISMATCH': return '子目录已存在但不是该业务空间的仓库';
1355
+ case 'CLONE_FAILED': return `git clone 失败:${r.message || ''}`.trim();
1356
+ case 'PULL_FAILED': return `工作空间仓库同步失败:${r.message || ''}`.trim();
1357
+ case 'CONFIG_INVALID': return `CONFIG.json 不合法:${r.message || ''}`.trim();
1358
+ case 'USER_CANCELLED': return '用户取消升级';
1359
+ case 'INIT_FAILED': return `初始化失败:${r.message || ''}`.trim();
1360
+ default: return r.error || '未知错误';
1361
+ }
1362
+ }
1363
+
1364
+ /** 批量处理结束后输出汇总报告(成功项绿色、失败项红色) */
1365
+ function printBatchSummary(results) {
1366
+ const ok = results.filter(r => r.ok);
1367
+ const failed = results.filter(r => !r.ok);
1368
+
1369
+ section('📊', `处理汇总(成功 ${ok.length}/${results.length})`);
1370
+ ok.forEach(r => {
1371
+ log(`${c('cyan', (r.subdir || '?') + '/')} ${r.w.name}(${r.w.businessLine})`, 'success');
1372
+ });
1373
+ failed.forEach(r => {
1374
+ log(`${r.w.name}(${r.w.businessLine}):${describeBatchError(r)}`, 'error');
1375
+ });
1376
+ if (failed.length > 0) {
1377
+ console.log('');
1378
+ log(`${failed.length} 个业务空间处理失败,请按上述提示排查`, 'warn');
1379
+ }
1380
+ }
1381
+
1382
+ /**
1383
+ * 初始化业务空间工作目录(cbb setup 命令)。
1384
+ *
1385
+ * 流程:安装用户级组件(幂等)→ 检测当前目录 → 加载配置 → checkbox 多选
1386
+ * → 确认目标目录 → 依次处理每个选中的 workspace。
1387
+ * 单个 workspace 失败不影响后续。
1388
+ */
1389
+ async function setupWorkspace() {
1390
+ // 首次运行:安装用户级组件(hook / cbb-wiki-ops,幂等,已装则跳过)
1391
+ await setupUserLevelComponents();
1392
+
1393
+ // 0. 检测当前目录是否为 git 仓库
1394
+ const parentDir = process.cwd();
1395
+ try {
1396
+ execSync('git rev-parse --is-inside-work-tree', {
1397
+ cwd: parentDir,
1398
+ stdio: 'pipe',
1399
+ timeout: 5000,
1400
+ });
1401
+ log('当前目录是 Git 仓库,不支持在此执行 cbb setup。', 'error');
1402
+ log('请在非 Git 仓库的目录下运行(例如新建空文件夹后执行)。', 'warn');
1403
+ process.exit(1);
1404
+ } catch (_) {
1405
+ // 非 git 仓库,继续
1406
+ }
1407
+
1408
+ // 1. 加载业务空间配置
1409
+ let list;
1410
+ try {
1411
+ list = workspaces.loadWorkspaces();
1412
+ } catch (e) {
1413
+ log(e.message, 'error');
1414
+ process.exit(1);
1415
+ }
1416
+
1417
+ // 2. checkbox 多选(inquirer 风格:↑↓ 移动、空格 切换、a 全选、Enter 确认)
1418
+ const selectedIdx = await checkbox.promptCheckbox({
1419
+ header: '📦 选择要初始化的业务空间(多选用空格切换)',
1420
+ items: list.map(w => ({
1421
+ label: `${c('yellow', '[' + w.businessLine + ']')} ${w.name}`,
1422
+ hint: w.repo,
1423
+ })),
1424
+ });
1425
+
1426
+ if (selectedIdx.size === 0) {
1427
+ log('未选择任何业务空间,已退出', 'warn');
1428
+ process.exit(0);
1429
+ }
1430
+
1431
+ // 转成有序数组(按原始顺序)
1432
+ const selected = [...selectedIdx]
1433
+ .sort((a, b) => a - b)
1434
+ .map(i => list[i]);
1435
+
1436
+ // 3. 批量展示所选业务空间
1437
+ section('✓', `已选择 ${selected.length} 个业务空间`);
1438
+ selected.forEach((w, i) => {
1439
+ console.log(` ${c('cyan', String(i + 1) + '.')} ${c('yellow', '[' + w.businessLine + ']')} ${w.name}`);
1440
+ console.log(` ${c('gray', w.repo)}`);
1441
+ });
1442
+ console.log('');
1443
+
1444
+ // 4. 展示目标目录并让用户确认
1445
+ section('📁', '业务空间将创建在以下目录');
1446
+ console.log(` ${c('cyan', parentDir)}`);
1447
+ console.log('');
1448
+ const answer = await ask(`${c('yellow', '?')} 是否将上述业务空间创建在此目录?(Y/n) `);
1449
+ if (answer.toLowerCase() === 'n' || answer.toLowerCase() === 'no') {
1450
+ log('已取消,未创建任何业务空间。', 'warn');
1451
+ process.exit(0);
1452
+ }
1453
+ console.log('');
1454
+
1455
+ // 5. 依次处理每个 workspace;任一失败不影响后续
1456
+ const results = [];
1457
+ for (let i = 0; i < selected.length; i++) {
1458
+ const w = selected[i];
1459
+ section('📦', `[${i + 1}/${selected.length}] ${w.name}`);
1460
+ console.log('');
1461
+ const result = await initSingleWorkspace(w);
1462
+ results.push({ w, ...result });
1463
+ console.log('');
1464
+ }
1465
+
1466
+ // 5. 汇总报告(已移除)
1467
+ }
1468
+
1469
+ // ── 用户级组件 ────────────────────────────────────────────
1470
+
1471
+ /**
1472
+ * 安装用户级组件(与项目无关的内容,一次安装全局可用,幂等)。
1473
+ *
1474
+ * - 清单:~/.cbb/.user-components 记录本包管理的用户级路径
1475
+ * - 内容:check-update hook(settings 由 settingsManager 幂等管理)+ 工具 skills(cbb-wiki-ops)
1476
+ */
1477
+ const USER_MANIFEST_FILE = path.join(os.homedir(), '.cbb', '.user-components');
1478
+ // 装到用户级的工具 skills(与具体项目无关,任何会话都可能用到)
1479
+ const USER_LEVEL_SKILLS = ['cbb-wiki-ops'];
1480
+
1481
+ async function setupUserLevelComponents() {
1482
+ const home = os.homedir();
1483
+ const srcDir = path.join(SHARED_STANDARDS_DIR, 'cbb', 'tools');
1484
+ if (!fs.existsSync(srcDir)) return;
1485
+
1486
+ const managed = new Set();
1487
+ const manifestDir = path.dirname(USER_MANIFEST_FILE);
1488
+ fs.mkdirSync(manifestDir, { recursive: true });
1489
+ if (fs.existsSync(USER_MANIFEST_FILE)) {
1490
+ try {
1491
+ const data = JSON.parse(fs.readFileSync(USER_MANIFEST_FILE, 'utf-8'));
1492
+ (data.paths || []).forEach(p => managed.add(p));
1493
+ } catch (_) { /* ignore parse errors */ }
1494
+ }
1495
+
1496
+ section('🏠', '检查用户级组件(~/.claude/、~/.qoder/)');
1497
+
1498
+ const skillAdapters = adapters.filter(a => a.skillsDir);
1499
+ skillAdapters.forEach(adapter => {
1500
+ USER_LEVEL_SKILLS.forEach(skillName => {
1501
+ const src = path.join(srcDir, skillName);
1502
+ if (!fs.existsSync(src)) return;
1503
+ const dest = path.join(home, adapter.skillsDir, skillName);
1504
+ removePath(dest);
1505
+ fs.cpSync(src, dest, { recursive: true });
1506
+ managed.add(`${adapter.skillsDir}/${skillName}`);
1507
+ });
1508
+ });
1509
+ log(`工具 Skills → ${skillAdapters.map(a => `~/${a.skillsDir}/`).join(', ')} (${USER_LEVEL_SKILLS.length} 个)`, 'success');
1510
+
1511
+ // check-update hook(settingsManager 幂等:已有 _managedBy: cbb 标记则不重复写入)
1512
+ const hookAdapters = adapters.filter(a => a.settingsFile && a.hookEvents);
1513
+ if (hookAdapters.length > 0) {
1514
+ const command = settingsManager.getCheckUpdateCommand();
1515
+ hookAdapters.forEach(adapter => {
1516
+ settingsManager.addHooks(adapter.settingsFile, command, adapter.hookEvents);
1517
+ settingsManager.removeLegacyHooks(adapter.settingsFile);
1518
+ });
1519
+ log(`版本检查 hook → ${hookAdapters.map(a => a.displayName).join(', ')}`, 'success');
1520
+ }
1521
+
1522
+ const pkg = require(path.join(SHARED_STANDARDS_DIR, 'package.json'));
1523
+ fs.writeFileSync(USER_MANIFEST_FILE, JSON.stringify({
1524
+ version: pkg.version,
1525
+ paths: [...managed],
1526
+ }, null, 2));
1527
+ }
1528
+
1529
+ /**
1530
+ * cbb update:按现有安装模式升级当前目录内容 + 用户级组件。
1531
+ *
1532
+ * 安装模式判定:清单含 rules 路径 → 完整模式(worktree 沙箱/单项目),
1533
+ * 否则为 workspace 子集模式(空间根,只装 worktree 管理 + openspec 配置)。
1534
+ */
1535
+ async function updateInstalled() {
1536
+ await setupUserLevelComponents();
1537
+
1538
+ const manifest = readManifest();
1539
+ if (manifest.size === 0) {
1540
+ log('当前目录未检测到安装记录。初始化业务空间请用: cbb setup', 'error');
1541
+ process.exit(1);
1542
+ }
1543
+
1544
+ const isFullInstall = [...manifest].some(p =>
1545
+ adapters.some(a => p.startsWith(`${a.rulesDir}/`))
1546
+ );
1547
+
1548
+ if (isFullInstall) {
1549
+ log('检测到完整模式安装,升级开发配置(rules + skills + commands + 工作流)...', 'info');
1550
+ await init(undefined, {});
1551
+ } else {
1552
+ log('检测到 workspace 子集模式,升级 worktree 管理 + openspec 配置...', 'info');
1553
+ await installWorkspaceConfig(undefined, {});
1554
+ }
1555
+ }
1556
+
1557
+ // ── 卸载 ──────────────────────────────────────────────────
1558
+
1559
+ /**
1560
+ * 智能卸载 config.yaml:
1561
+ * - 反向合并:移除本包注入的字段,保留用户自有字段
1562
+ * - 纯本包内容(无用户字段)→ 删除文件
1563
+ */
1564
+ function uninstallConfigYaml(managedPaths) {
1565
+ const configDest = path.join(TARGET_DIR, 'openspec', 'config.yaml');
1566
+ const configSrc = path.join(SHARED_STANDARDS_DIR, 'config', 'openspec', 'config.yaml');
1567
+
1568
+ if (!fs.existsSync(configDest) || !fs.existsSync(configSrc)) return false;
1569
+ if (!managedPaths.has('openspec/config.yaml')) return false;
1570
+
1571
+ const ours = yaml.load(fs.readFileSync(configSrc, 'utf-8'));
1572
+ const current = yaml.load(fs.readFileSync(configDest, 'utf-8'));
1573
+
1574
+ if (!current || typeof current !== 'object') return false;
1575
+
1576
+ // 移除本包覆盖的标量字段(schema, context 等)
1577
+ const oursKeys = Object.keys(ours);
1578
+ oursKeys.forEach(key => {
1579
+ if (key === 'rules') return; // rules 单独处理
1580
+ if (current[key] !== undefined) {
1581
+ delete current[key];
1582
+ }
1583
+ });
1584
+
1585
+ // 移除本包注入的 rules 条目
1586
+ if (ours.rules && typeof ours.rules === 'object') {
1587
+ if (current.rules && typeof current.rules === 'object') {
1588
+ Object.keys(ours.rules).forEach(key => {
1589
+ delete current.rules[key];
1590
+ });
1591
+ // rules 对象变空则移除
1592
+ if (Object.keys(current.rules).length === 0) {
1593
+ delete current.rules;
1594
+ }
1595
+ }
1596
+ }
1597
+
1598
+ // 检查是否有用户内容剩余
1599
+ if (Object.keys(current).length === 0) {
1600
+ return false; // 无用户内容,标记删除
1601
+ }
1602
+
1603
+ // 保留用户字段
1604
+ const remaining = yaml.dump(current, { lineWidth: -1 });
1605
+ fs.writeFileSync(configDest, remaining);
1606
+ log('config.yaml: 移除本包字段,保留用户配置', 'success');
1607
+ return true; // 已处理,跳过通用删除
1608
+ }
1609
+
1610
+ /**
1611
+ * 卸载用户级组件(cbb-wiki-ops 等工具 skills + 版本检查 hook)。
1612
+ * 按 ~/.cbb/.user-components 清单精确删除,不影响用户自装内容;
1613
+ * ~/.cbb 目录不动(可能含用户自己的 wiki-config.json)。
1614
+ */
1615
+ async function uninstallUserLevelComponents() {
1616
+ if (!fs.existsSync(USER_MANIFEST_FILE)) {
1617
+ log('未检测到用户级组件安装记录', 'warn');
1618
+ return;
1619
+ }
1620
+ let paths = [];
1621
+ try {
1622
+ paths = JSON.parse(fs.readFileSync(USER_MANIFEST_FILE, 'utf-8')).paths || [];
1623
+ } catch (_) { /* ignore parse errors */ }
1624
+
1625
+ let removed = 0;
1626
+ paths.forEach(relPath => {
1627
+ const target = path.join(os.homedir(), relPath);
1628
+ if (fs.existsSync(target)) {
1629
+ removePath(target);
1630
+ log(`移除 ~/${relPath}`, 'success');
1631
+ removed++;
1632
+ }
1633
+ });
1634
+
1635
+ // 移除版本检查 hook(保留用户其余配置)
1636
+ adapters.forEach(adapter => {
1637
+ if (!adapter.settingsFile || !adapter.hookEvents) return;
1638
+ if (settingsManager.removeHooks(adapter.settingsFile, adapter.hookEvents)) {
1639
+ log(`${adapter.settingsFile}: 移除版本检查 hook,保留用户配置`, 'success');
1640
+ }
1641
+ });
1642
+
1643
+ removePath(USER_MANIFEST_FILE);
1644
+ log(`用户级组件已卸载(${removed} 个文件/目录)`, 'success');
1645
+ }
1646
+
1647
+ async function uninstall() {
1648
+ const manifestPath = path.join(TARGET_DIR, MANIFEST_FILE);
1649
+ const manifestDir = path.join(TARGET_DIR, MANIFEST_DIR);
1650
+
1651
+ if (!fs.existsSync(manifestPath)) {
1652
+ log('未检测到安装记录,无需卸载', 'warn');
1653
+ return;
1654
+ }
1655
+
1656
+ const managedPaths = readManifest();
1657
+
1658
+ // 确认卸载
1659
+ console.log('\n🗑 卸载 @jspg-ai/coding-bb...\n');
1660
+ const answer = await ask('确认卸载?此操作将移除本包管理的所有文件 (y/N): ');
1661
+ if (answer.toLowerCase() !== 'y') {
1662
+ log('已取消卸载');
1663
+ return;
1664
+ }
1665
+
1666
+ // 是否同时卸载用户级组件(工具 skills + 版本检查 hook,作用于 ~/.claude 等用户目录)
1667
+ let removeUserLevel = false;
1668
+ if (fs.existsSync(USER_MANIFEST_FILE)) {
1669
+ const answerUser = await ask('是否同时卸载用户级组件(~/.claude、~/.qoder 下的工具 skills + 版本检查 hook)?(y/N): ');
1670
+ removeUserLevel = answerUser.toLowerCase() === 'y' || answerUser.toLowerCase() === 'yes';
1671
+ }
1672
+
1673
+ // 智能处理 config.yaml(保留用户内容)
1674
+ const configHandled = uninstallConfigYaml(managedPaths);
1675
+
1676
+ // 构建跳过集合(已被智能处理的路径)
1677
+ const skippedPaths = new Set();
1678
+ if (configHandled) skippedPaths.add('openspec/config.yaml');
1679
+
1680
+ // 智能处理 settings.json(hook 属用户级组件,仅在确认卸载用户级时移除;保留用户配置)
1681
+ adapters.forEach(adapter => {
1682
+ if (!adapter.settingsFile || !adapter.hookEvents) return;
1683
+ const settingsPath = adapter.settingsFileIsUserLevel
1684
+ ? adapter.settingsFile
1685
+ : path.join(TARGET_DIR, adapter.settingsFile);
1686
+ // 项目级:需在清单中才处理
1687
+ if (!adapter.settingsFileIsUserLevel && !managedPaths.has(adapter.settingsFile)) return;
1688
+ if (removeUserLevel && settingsManager.removeHooks(settingsPath, adapter.hookEvents)) {
1689
+ log(`${adapter.settingsFile}: 移除版本检查 hook,保留用户配置`, 'success');
1690
+ }
1691
+ // 清理旧版本遗留的 SessionStart hook(无版本差异,无条件清理)
1692
+ if (settingsManager.removeLegacyHooks(settingsPath)) {
1693
+ log(`${adapter.settingsFile}: 清理遗留 SessionStart hook`, 'success');
1694
+ }
1695
+ skippedPaths.add(adapter.settingsFile);
1696
+ });
1697
+
1698
+ // 删除清单中记录的所有路径。
1699
+ // 注意:CLAUDE.md / AGENTS.md 等用户文档受保护,即使旧清单里有也不删除。
1700
+ let removed = 0;
1701
+ pathsToDelete(managedPaths, skippedPaths).forEach(relativePath => {
1702
+ const targetPath = path.join(TARGET_DIR, relativePath);
1703
+ if (fs.existsSync(targetPath)) {
1704
+ removePath(targetPath);
1705
+ log(`移除 ${relativePath}`, 'success');
1706
+ removed++;
1707
+ }
1708
+ });
1709
+ protectedKeptPaths(managedPaths, skippedPaths).forEach(kept => {
1710
+ log(`保留 ${kept}(可能含用户自有内容,请按需手动删除)`, 'warn');
1711
+ });
1712
+
1713
+ // 清理可能变空的父目录(从深到浅)
1714
+ const dirsToCheck = new Set();
1715
+ managedPaths.forEach(relativePath => {
1716
+ let dir = path.dirname(relativePath);
1717
+ while (dir && dir !== '.') {
1718
+ dirsToCheck.add(dir);
1719
+ dir = path.dirname(dir);
1720
+ }
1721
+ });
1722
+ // 按深度排序,深层先处理
1723
+ [...dirsToCheck].sort((a, b) => b.split('/').length - a.split('/').length).forEach(dir => {
1724
+ const fullPath = path.join(TARGET_DIR, dir);
1725
+ if (fs.existsSync(fullPath) && fs.readdirSync(fullPath).length === 0) {
1726
+ removePath(fullPath);
1727
+ log(`清理空目录 ${dir}/`, 'success');
1728
+ }
1729
+ });
1730
+
1731
+ // 删除清单文件和 .va 目录
1732
+ removePath(manifestDir);
1733
+ log('移除安装记录', 'success');
1734
+
1735
+ // 清理 .gitignore
1736
+ cleanupGitignore();
1737
+
1738
+ // 卸载用户级组件(如已确认)
1739
+ if (removeUserLevel) {
1740
+ console.log('');
1741
+ await uninstallUserLevelComponents();
1742
+ }
1743
+
1744
+ console.log(`\n✅ 卸载完成!共移除 ${removed} 个文件/目录\n`);
1745
+ }
1746
+
1747
+ // ── CLI 入口 ──────────────────────────────────────────────
1748
+
1749
+ const args = process.argv.slice(2);
1750
+ const command = args[0];
1751
+ const restArgs = args.slice(1);
1752
+
1753
+ switch (command) {
1754
+ case 'setup': {
1755
+ setupWorkspace().catch(err => {
1756
+ log(err.message, 'error');
1757
+ process.exit(1);
1758
+ });
1759
+ break;
1760
+ }
1761
+ case 'update': {
1762
+ const upstreamSuperpowers = restArgs.includes('--upstream-superpowers');
1763
+ const upstreamOpenspec = restArgs.includes('--upstream-openspec');
1764
+ if (upstreamSuperpowers || upstreamOpenspec) {
1765
+ // 维护者选项:从 GitHub 拉取上游原生内容到本包源仓库(发布流程用)
1766
+ if (upstreamSuperpowers) {
1767
+ superpowers.fetchLatestSkills().then(result => {
1768
+ if (result.skipped) {
1769
+ log(`superpowers skills 已是最新 (${result.sha.slice(0, 8)})`, 'success');
1770
+ }
1771
+ const validation = superpowers.validateSkills();
1772
+ if (!validation.valid) {
1773
+ if (validation.missing.length > 0) log(`缺少 skills: ${validation.missing.join(', ')}`, 'error');
1774
+ if (validation.incomplete.length > 0) log(`不完整 skills: ${validation.incomplete.join(', ')}`, 'error');
1775
+ process.exit(1);
1776
+ }
1777
+ log(`skills 完整性校验通过 (${superpowers.getSkillCount()} 个)`, 'success');
1778
+ }).catch(err => {
1779
+ log(`更新失败: ${err.message}`, 'error');
1780
+ process.exit(1);
1781
+ });
1782
+ }
1783
+ if (upstreamOpenspec) {
1784
+ openspec.fetchLatestOpenSpec().then(result => {
1785
+ if (!result.skipped) {
1786
+ const validation = openspec.validateDeployed();
1787
+ if (!validation.valid) {
1788
+ log(`部署不完整,缺失: ${validation.missing.join(', ')}`, 'error');
1789
+ process.exit(1);
1790
+ }
1791
+ log(`openspec 工作流完整性校验通过`, 'success');
1792
+ } else {
1793
+ const validation = openspec.validateDeployed();
1794
+ if (!validation.valid) {
1795
+ log(`本地 openspec 工作流不完整,请重新运行 cbb update --upstream-openspec:`, 'error');
1796
+ for (const m of validation.missing) log(` - 缺失 ${m}`, 'error');
1797
+ process.exit(1);
1798
+ }
1799
+ log(`openspec 工作流完整性校验通过`, 'success');
1800
+ }
1801
+ }).catch(err => {
1802
+ log(`更新失败: ${err.message}`, 'error');
1803
+ process.exit(1);
1804
+ });
1805
+ }
1806
+ } else {
1807
+ updateInstalled().catch(err => {
1808
+ log(err.message, 'error');
1809
+ process.exit(1);
1810
+ });
1811
+ }
1812
+ break;
1813
+ }
1814
+ case 'init': {
1815
+ const yes = restArgs.includes('-y') || restArgs.includes('--yes');
1816
+ init(undefined, { yes }).catch(err => {
1817
+ log(err.message, 'error');
1818
+ process.exit(1);
1819
+ });
1820
+ break;
1821
+ }
1822
+ case 'uninstall':
1823
+ uninstall().catch(err => {
1824
+ log(err.message, 'error');
1825
+ process.exit(1);
1826
+ });
1827
+ break;
1828
+ case 'check-update':
1829
+ require('../utils/check-update.js');
1830
+ break;
1831
+ case 'wp':
1832
+ require('../wiki/index.js').publish(restArgs);
1833
+ break;
1834
+ case 'wiki':
1835
+ require('../wiki/cli.js').run(restArgs);
1836
+ break;
1837
+ case 'help':
1838
+ case '-h':
1839
+ case '--help':
1840
+ case undefined:
1841
+ showHelp();
1842
+ break;
1843
+ default:
1844
+ log(`未知命令: ${command}`, 'error');
1845
+ showHelp();
1846
+ process.exit(1);
1847
+ }
1848
+
1849
+ function showHelp() {
1850
+ const pkg = require(path.join(SHARED_STANDARDS_DIR, 'package.json'));
1851
+ console.log(`
1852
+ @jspg-ai/coding-bb v${pkg.version} — AI Coding Agent 统一行为准则与工作流
1853
+
1854
+ 快速开始:
1855
+ cbb setup 初始化业务空间(克隆仓库 + 配置工作流,首次运行自动安装用户级组件)
1856
+ cbb update 更新已安装内容(用户级 + 当前空间/项目)
1857
+ cbb uninstall 卸载(交互确认范围)
1858
+
1859
+ 内部命令(由流程自动调用,无需手动执行):
1860
+ cbb init --yes worktree 沙箱内安装开发配置(init-worktree Step 4 自动调用)
1861
+ cbb check-update 版本检查(hook 每日自动触发)
1862
+
1863
+ Agent 命令(AI 通过技能调用,请勿手动执行):
1864
+ cbb wiki / cbb wp Wiki 查询与发布
1865
+
1866
+ 维护者选项(发布流程用):
1867
+ cbb update --upstream-superpowers 从 GitHub 拉取最新 superpowers skills 到源仓库
1868
+ cbb update --upstream-openspec 从 GitHub 拉取最新 OpenSpec 工作流到源仓库
1869
+
1870
+ 90% 的场景只需要:cbb setup
1871
+
1872
+ 前提条件(首次使用执行一次):
1873
+ npm install -g @jspg-ai/coding-bb
1874
+ `);
1875
+ }