@jspg-ai/coding-bb 0.0.3-beta.3 → 0.0.3-beta.31

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 (145) hide show
  1. package/cbb/dev-standards/rules/cbb-ai-behavior.md +135 -116
  2. package/cbb/lib/external/index.js +228 -0
  3. package/cbb/lib/install/agents-nav.js +92 -0
  4. package/cbb/lib/install/claude-code.js +7 -2
  5. package/cbb/lib/install/command-skill.js +105 -0
  6. package/cbb/lib/install/init.js +502 -168
  7. package/cbb/lib/install/minimax.js +60 -0
  8. package/cbb/lib/install/opencode.js +3 -5
  9. package/cbb/lib/install/qoder.js +1 -3
  10. package/cbb/lib/install/rule-rewrite.js +27 -0
  11. package/cbb/lib/install/setup-decision.js +48 -0
  12. package/cbb/lib/install/tool-selection.js +45 -0
  13. package/cbb/lib/install/trae.js +43 -0
  14. package/cbb/lib/install/workbuddy.js +36 -0
  15. package/cbb/lib/install/workspaces.js +21 -17
  16. package/cbb/lib/openspec/index.js +337 -554
  17. package/cbb/lib/superpowers/index.js +246 -265
  18. package/cbb/lib/utils/check-update.js +17 -2
  19. package/cbb/lib/utils/githook.js +226 -0
  20. package/cbb/lib/utils/gitignore.js +16 -3
  21. package/cbb/lib/utils/plugin-hook.js +92 -0
  22. package/cbb/lib/utils/settings.js +17 -4
  23. package/cbb/lib/utils/tar.js +92 -0
  24. package/cbb/lib/utils/upstream.js +90 -0
  25. package/cbb/worktrees/_shared/scripts/check-local-state.js +114 -0
  26. package/cbb/worktrees/_shared/scripts/find-target-worktree.js +148 -96
  27. package/cbb/worktrees/_shared/scripts/find-workspace-root.js +7 -7
  28. package/cbb/worktrees/_shared/scripts/install-git-hook.js +70 -0
  29. package/cbb/worktrees/_shared/scripts/junction-ai-config.js +127 -0
  30. package/cbb/worktrees/_shared/scripts/start-local.js +180 -0
  31. package/cbb/worktrees/skills/cbb-local-mode/SKILL.md +158 -0
  32. package/cbb/worktrees/skills/cbb-local-mode/scripts/check-local-state.js +114 -0
  33. package/cbb/worktrees/skills/cbb-local-mode/scripts/install-git-hook.js +70 -0
  34. package/cbb/worktrees/skills/cbb-local-mode/scripts/silence-popup.js +23 -0
  35. package/cbb/worktrees/skills/cbb-local-mode/scripts/start-local.js +180 -0
  36. package/cbb/worktrees/skills/cbb-worktree-close/SKILL.md +60 -43
  37. package/cbb/worktrees/skills/cbb-worktree-close/scripts/check-env.js +3 -3
  38. package/cbb/worktrees/skills/cbb-worktree-close/scripts/check-unarchived.js +75 -48
  39. package/cbb/worktrees/skills/cbb-worktree-close/scripts/delete-branches.js +1 -1
  40. package/cbb/worktrees/skills/cbb-worktree-close/scripts/discover-apps.js +14 -5
  41. package/cbb/worktrees/skills/cbb-worktree-close/scripts/find-target-worktree.js +148 -96
  42. package/cbb/worktrees/skills/cbb-worktree-close/scripts/find-workspace-root.js +7 -7
  43. package/cbb/worktrees/skills/cbb-worktree-close/scripts/remove-worktrees.js +13 -4
  44. package/cbb/worktrees/skills/cbb-worktree-close/scripts/safety-check.js +11 -2
  45. package/cbb/worktrees/skills/cbb-worktree-init/SKILL.md +138 -129
  46. package/cbb/worktrees/skills/cbb-worktree-init/scripts/check-env.js +1 -1
  47. package/cbb/worktrees/skills/cbb-worktree-init/scripts/check-repos.js +2 -2
  48. package/cbb/worktrees/skills/cbb-worktree-init/scripts/create-branches.js +33 -8
  49. package/cbb/worktrees/skills/cbb-worktree-init/scripts/create-worktrees.js +60 -46
  50. package/cbb/worktrees/skills/cbb-worktree-init/scripts/find-workspace-root.js +7 -7
  51. package/cbb/worktrees/skills/cbb-worktree-init/scripts/install-git-hook.js +70 -0
  52. package/cbb/worktrees/skills/cbb-worktree-init/scripts/junction-ai-config.js +127 -0
  53. package/cbb/worktrees/skills/cbb-worktree-init/scripts/parse-config.js +7 -7
  54. package/cbb/worktrees/skills/cbb-worktree-init/scripts/push-branches.js +3 -3
  55. package/cbb/worktrees/skills/cbb-worktree-init/scripts/sync-repos.js +11 -7
  56. package/cbb/worktrees/skills/cbb-worktree-init/scripts/update-gitignore.js +37 -9
  57. package/cbb/worktrees/skills/cbb-worktree-push/SKILL.md +140 -71
  58. package/cbb/worktrees/skills/cbb-worktree-push/scripts/commit-worktrees.js +70 -12
  59. package/cbb/worktrees/skills/cbb-worktree-push/scripts/find-target-worktree.js +148 -96
  60. package/cbb/worktrees/skills/cbb-worktree-push/scripts/find-workspace-root.js +7 -7
  61. package/cbb/worktrees/skills/cbb-worktree-push/scripts/make-mr-urls.js +211 -0
  62. package/cbb/worktrees/skills/cbb-worktree-push/scripts/push-worktrees.js +67 -20
  63. package/config/{workspace-config.sample.json → CBB-WORKSPACE.sample.json} +1 -1
  64. package/config/openspec/schemas/spec-driven/schema.yaml +40 -30
  65. package/config/openspec/schemas/spec-driven/templates/design.md +0 -18
  66. package/config/openspec/schemas/spec-driven/templates/proposal.md +4 -3
  67. package/config/upstream-mirrors.json +17 -0
  68. package/config/workspace-agents.sample.md +4 -33
  69. package/external/frontend-design/.version +6 -0
  70. package/external/frontend-design/LICENSE.txt +177 -0
  71. package/external/frontend-design/SKILL.md +71 -0
  72. package/openspec/.version +2 -3
  73. package/openspec/commands/apply.md +189 -175
  74. package/openspec/commands/archive.md +244 -216
  75. package/openspec/commands/bulk-archive.md +362 -327
  76. package/openspec/commands/continue.md +115 -105
  77. package/openspec/commands/explore.md +230 -199
  78. package/openspec/commands/ff.md +115 -104
  79. package/openspec/commands/new.md +74 -63
  80. package/openspec/commands/onboard.md +562 -548
  81. package/openspec/commands/propose.md +161 -150
  82. package/openspec/commands/sync.md +277 -249
  83. package/openspec/commands/update.md +98 -80
  84. package/openspec/commands/verify.md +206 -162
  85. package/openspec/skills/openspec-apply-change/SKILL.md +20 -5
  86. package/openspec/skills/openspec-archive-change/SKILL.md +40 -11
  87. package/openspec/skills/openspec-bulk-archive-change/SKILL.md +46 -9
  88. package/openspec/skills/openspec-continue-change/SKILL.md +14 -3
  89. package/openspec/skills/openspec-explore/SKILL.md +22 -10
  90. package/openspec/skills/openspec-ff-change/SKILL.md +15 -3
  91. package/openspec/skills/openspec-new-change/SKILL.md +13 -1
  92. package/openspec/skills/openspec-onboard/SKILL.md +58 -43
  93. package/openspec/skills/openspec-propose/SKILL.md +15 -3
  94. package/openspec/skills/openspec-sync-specs/SKILL.md +31 -2
  95. package/openspec/skills/openspec-update-change/SKILL.md +34 -15
  96. package/openspec/skills/openspec-verify-change/SKILL.md +72 -26
  97. package/package.json +3 -2
  98. package/superpowers/.version +4 -4
  99. package/superpowers/skills/brainstorming/SKILL.md +47 -12
  100. package/superpowers/skills/brainstorming/scripts/frame-template.html +213 -213
  101. package/superpowers/skills/brainstorming/scripts/server.cjs +723 -723
  102. package/superpowers/skills/brainstorming/visual-companion.md +6 -6
  103. package/superpowers/skills/diagnosing-superpowers/SKILL.md +120 -0
  104. package/superpowers/skills/diagnosing-superpowers/prompts/analyst-common.md +38 -0
  105. package/superpowers/skills/diagnosing-superpowers/prompts/cost-and-time.md +28 -0
  106. package/superpowers/skills/diagnosing-superpowers/prompts/plan-adherence.md +29 -0
  107. package/superpowers/skills/diagnosing-superpowers/prompts/quality-evidence.md +26 -0
  108. package/superpowers/skills/diagnosing-superpowers/prompts/repeated-work.md +30 -0
  109. package/superpowers/skills/diagnosing-superpowers/prompts/request-conflicts.md +20 -0
  110. package/superpowers/skills/diagnosing-superpowers/prompts/scrub-audit.md +33 -0
  111. package/superpowers/skills/diagnosing-superpowers/prompts/scrub.md +29 -0
  112. package/superpowers/skills/diagnosing-superpowers/prompts/similar-session.md +38 -0
  113. package/superpowers/skills/diagnosing-superpowers/prompts/skill-timeline.md +30 -0
  114. package/superpowers/skills/diagnosing-superpowers/prompts/stumbles.md +28 -0
  115. package/superpowers/skills/diagnosing-superpowers/references/context-safety.md +22 -0
  116. package/superpowers/skills/diagnosing-superpowers/references/github-issues.md +47 -0
  117. package/superpowers/skills/diagnosing-superpowers/references/redaction-policy.md +34 -0
  118. package/superpowers/skills/diagnosing-superpowers/references/session-discovery.md +31 -0
  119. package/superpowers/skills/diagnosing-superpowers/templates/bundle-README.md +77 -0
  120. package/superpowers/skills/diagnosing-superpowers/templates/case.md +64 -0
  121. package/superpowers/skills/diagnosing-superpowers/templates/issue.md +51 -0
  122. package/superpowers/skills/diagnosing-superpowers/templates/report.md +82 -0
  123. package/superpowers/skills/executing-plans/SKILL.md +350 -41
  124. package/superpowers/skills/executing-plans/scripts/task-done +52 -0
  125. package/superpowers/skills/executing-plans/scripts/task-start +28 -0
  126. package/superpowers/skills/requesting-code-review/SKILL.md +1 -1
  127. package/superpowers/skills/requesting-code-review/code-reviewer.md +17 -0
  128. package/superpowers/skills/subagent-driven-development/SKILL.md +18 -18
  129. package/superpowers/skills/subagent-driven-development/re-review-prompt.md +1 -1
  130. package/superpowers/skills/subagent-driven-development/scripts/review-package +53 -46
  131. package/superpowers/skills/subagent-driven-development/scripts/sdd-workspace +82 -40
  132. package/superpowers/skills/subagent-driven-development/scripts/task-brief +43 -41
  133. package/superpowers/skills/subagent-driven-development/task-reviewer-prompt.md +2 -2
  134. package/superpowers/skills/systematic-debugging/root-cause-tracing.md +1 -1
  135. package/superpowers/skills/test-driven-development/SKILL.md +10 -0
  136. package/superpowers/skills/using-superpowers/SKILL.md +2 -0
  137. package/superpowers/skills/using-superpowers/references/claude-code-tools.md +29 -0
  138. package/superpowers/skills/using-superpowers/references/muse-tools.md +35 -0
  139. package/superpowers/skills/writing-plans/SKILL.md +62 -29
  140. package/superpowers/skills/writing-skills/SKILL.md +4 -2
  141. package/superpowers/skills/writing-skills/graphviz-conventions.dot +171 -171
  142. package/cbb/worktrees/commands/worktree-close.md +0 -64
  143. package/cbb/worktrees/commands/worktree-init.md +0 -51
  144. package/cbb/worktrees/commands/worktree-push.md +0 -42
  145. package/superpowers/skills/writing-plans/plan-document-reviewer-prompt.md +0 -49
@@ -0,0 +1,226 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * git pre-commit hook:分支感知提交防线(ADR 0002)。
5
+ *
6
+ * 策略(对当前仓库生效,worktree 与主仓库共享 .git/hooks 故一并覆盖):
7
+ * - 当前分支非主干(含 detached HEAD)→ 一律放行(两模式开发态都在需求分支上)
8
+ * - workspace 仓库(检出含 CBB-WORKSPACE.json)在主干上 → 只放行控制面白名单路径:
9
+ * CBB-WORKSPACE.json / .gitignore / AGENTS.md / openspec/config.yaml /
10
+ * openspec/schemas/** / .cbb/**(配置变更与 cbb update 部署物,直改即推);
11
+ * 另放行旧名 workspace-config.json 的删除条目(仅用于一次性更名迁移)
12
+ * - 应用仓库在主干上 → 全拦(交付只走 MR)
13
+ * - 逃生口:用户显式 git commit --no-verify(AI 侧纪律禁止自动绕过)
14
+ *
15
+ * 本模块是 hook 源码的唯一真源;cbb/worktrees/_shared/scripts/install-git-hook.js
16
+ * 部署到业务空间后独立运行,内嵌同一份源码副本(改动须两处同步)。
17
+ *
18
+ * 提供纯函数(isCbbHookContent / buildPreCommitSource)与 I/O 包装
19
+ * (installGitHook / uninstallGitHook),供 init.js 的 setup / update / uninstall 调用。
20
+ */
21
+
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+ const { execSync } = require('child_process');
25
+
26
+ const HOOK_MARKER = 'managed-by-cbb';
27
+ const HOOK_FILE = 'pre-commit';
28
+ const BACKUP_SUFFIX = '.cbb-backup';
29
+
30
+ // 部署到仓库后的 hook 源码(独立 Node 脚本,无外部依赖)。
31
+ // 注意:源码内不使用反引号,便于本模块以模板字符串承载。
32
+ const PRE_COMMIT_SOURCE = [
33
+ '#!/usr/bin/env node',
34
+ '// managed-by-cbb v1 —— 分支感知提交防线(ADR 0002)',
35
+ '// main 上:workspace 仓库只放行控制面白名单,应用仓库全拦;非 main 一律放行。',
36
+ '// 绕过逃生口仅限用户显式 git commit --no-verify。',
37
+ "const { execSync } = require('child_process');",
38
+ "const fs = require('fs');",
39
+ 'function run(cmd) {',
40
+ " try { return execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim(); }",
41
+ ' catch (e) { return null; }',
42
+ '}',
43
+ "const branch = run('git branch --show-current') || '';",
44
+ '// 主干名:优先远端 HEAD 指向;探测失败按本地 main/master 存在性回退',
45
+ 'let main = null;',
46
+ "const remotes = run('git remote');",
47
+ 'if (remotes) {',
48
+ " const r = remotes.split(/\\r?\\n/)[0];",
49
+ " const sym = run('git symbolic-ref \"refs/remotes/' + r + '/HEAD\"');",
50
+ " if (sym) main = sym.replace('refs/remotes/' + r + '/', '');",
51
+ '}',
52
+ "if (!main) {",
53
+ " if (run('git rev-parse --verify --quiet refs/heads/main')) main = 'main';",
54
+ " else if (run('git rev-parse --verify --quiet refs/heads/master')) main = 'master';",
55
+ '}',
56
+ '// 非主干(含 detached / 探测失败)→ 放行',
57
+ 'if (!main || branch !== main) process.exit(0);',
58
+ '',
59
+ "// 主干上的提交:收集 staged 变更(空提交放行)",
60
+ "const staged = run('git diff --cached --name-only') || '';",
61
+ "const files = staged.split(/\\r?\\n/).filter(Boolean);",
62
+ 'if (files.length === 0) process.exit(0);',
63
+ '',
64
+ '// 仓库类型:workspace 仓库 = 检出含 CBB-WORKSPACE.json(或旧名,改名过渡期识别)',
65
+ "const isWorkspace = fs.existsSync('CBB-WORKSPACE.json') || fs.existsSync('workspace-config.json');",
66
+ '',
67
+ 'function deny(bad) {',
68
+ " console.error('[cbb-guard] 主干上拦截载荷提交(ADR 0002):');",
69
+ " bad.forEach(f => console.error(' ' + f));",
70
+ " console.error('需求载荷须在需求分支上提交:本地模式先 git checkout -b <需求名>;worktree 模式在 .worktrees/ 工作树内提交。');",
71
+ " console.error('控制面白名单:CBB-WORKSPACE.json / .gitignore / AGENTS.md / openspec/config.yaml / openspec/schemas/** / .cbb/**');",
72
+ " console.error('如确需在主干提交,须由用户显式 git commit --no-verify。');",
73
+ ' process.exit(1);',
74
+ '}',
75
+ '',
76
+ 'if (isWorkspace) {',
77
+ ' // 旧名仅放行删除条目:更名迁移提交(git mv)会产生旧名删除 + 新名新增两条',
78
+ " const EXACT = new Set(['CBB-WORKSPACE.json', 'workspace-config.json', '.gitignore', 'AGENTS.md', 'openspec/config.yaml']);",
79
+ " const PREFIXES = ['openspec/schemas/', '.cbb/'];",
80
+ ' const bad = files.filter(f => !EXACT.has(f) && !PREFIXES.some(p => f.indexOf(p) === 0));',
81
+ ' if (bad.length) deny(bad);',
82
+ '} else {',
83
+ ' deny(files);',
84
+ '}',
85
+ 'process.exit(0);',
86
+ ].join('\n');
87
+
88
+ /**
89
+ * 内容是否为本包管理的 hook(含标记行)。
90
+ */
91
+ function isCbbHookContent(content) {
92
+ return typeof content === 'string' && content.includes(HOOK_MARKER);
93
+ }
94
+
95
+ /**
96
+ * hook 源码(纯函数,便于单测与未来按仓库定制)。
97
+ */
98
+ function buildPreCommitSource() {
99
+ return PRE_COMMIT_SOURCE + '\n';
100
+ }
101
+
102
+ /**
103
+ * 解析仓库的 hooks 目录。
104
+ * 优先 core.hooksPath 配置(相对路径基于仓库根);未配置或读取失败回退 .git/hooks。
105
+ * @returns {string|null} 绝对路径;目录不可用时返回 null(调用方跳过安装)
106
+ */
107
+ function resolveHooksDir(repoPath) {
108
+ let configured = null;
109
+ try {
110
+ configured = execSync('git config core.hooksPath', {
111
+ cwd: repoPath,
112
+ encoding: 'utf-8',
113
+ stdio: ['ignore', 'pipe', 'ignore'],
114
+ timeout: 5000,
115
+ }).trim();
116
+ } catch (_) { /* 未配置 */ }
117
+
118
+ let dir;
119
+ if (configured) {
120
+ dir = path.isAbsolute(configured) ? configured : path.join(repoPath, configured);
121
+ } else {
122
+ const gitDir = path.join(repoPath, '.git');
123
+ if (!fs.existsSync(gitDir)) return null; // 非 git 仓库(或 .git 为 worktree 文件,见下)
124
+ // worktree 检出的 .git 是文件(内容 "gitdir: ...");hook 安装统一走主仓库,
125
+ // 调用方应传主仓库路径。此处对文件形态直接放弃,避免误建目录。
126
+ const st = fs.statSync(gitDir);
127
+ if (!st.isDirectory()) return null;
128
+ dir = path.join(gitDir, 'hooks');
129
+ }
130
+ return dir;
131
+ }
132
+
133
+ /**
134
+ * 安装(幂等,可用于升级覆盖):
135
+ * - 无既有 hook → 写入
136
+ * - 既有本包 hook → 覆盖(升级)
137
+ * - 既有用户 hook → 备份为 pre-commit.cbb-backup 后写入;卸载时恢复
138
+ * @param {string} repoPath 仓库根(主仓库;worktree 共享 hooks 无需重复安装)
139
+ * @returns {'installed' | 'updated' | 'skipped-not-git' | 'skipped-no-dir'}
140
+ */
141
+ function installGitHook(repoPath) {
142
+ const dir = resolveHooksDir(repoPath);
143
+ if (!dir) return 'skipped-not-git';
144
+
145
+ const hookPath = path.join(dir, HOOK_FILE);
146
+ fs.mkdirSync(dir, { recursive: true });
147
+
148
+ let prev = null;
149
+ if (fs.existsSync(hookPath)) {
150
+ prev = fs.readFileSync(hookPath, 'utf-8');
151
+ if (isCbbHookContent(prev)) {
152
+ if (prev === buildPreCommitSource()) return 'updated'; // 幂等:同版本
153
+ fs.writeFileSync(hookPath, buildPreCommitSource());
154
+ return 'updated';
155
+ }
156
+ // 用户自有 hook:备份后接管
157
+ fs.writeFileSync(hookPath + BACKUP_SUFFIX, prev);
158
+ }
159
+ fs.writeFileSync(hookPath, buildPreCommitSource());
160
+ try { fs.chmodSync(hookPath, 0o755); } catch (_) { /* Windows 无压力 */ }
161
+ return 'installed';
162
+ }
163
+
164
+ /**
165
+ * 卸载:
166
+ * - 当前 hook 是本包的 → 删除;有备份则恢复
167
+ * - 当前 hook 非本包(用户已接管)→ 不动,仅清掉残留备份
168
+ * @returns {'removed' | 'restored-backup' | 'noop'}
169
+ */
170
+ function uninstallGitHook(repoPath) {
171
+ const dir = resolveHooksDir(repoPath);
172
+ if (!dir) return 'noop';
173
+
174
+ const hookPath = path.join(dir, HOOK_FILE);
175
+ const backupPath = hookPath + BACKUP_SUFFIX;
176
+ let result = 'noop';
177
+
178
+ if (fs.existsSync(hookPath)) {
179
+ const content = fs.readFileSync(hookPath, 'utf-8');
180
+ if (isCbbHookContent(content)) {
181
+ fs.unlinkSync(hookPath);
182
+ result = 'removed';
183
+ if (fs.existsSync(backupPath)) {
184
+ fs.renameSync(backupPath, hookPath);
185
+ result = 'restored-backup';
186
+ }
187
+ }
188
+ } else if (fs.existsSync(backupPath)) {
189
+ // hook 已被用户删除但备份残留:不擅自恢复,只清备份
190
+ fs.unlinkSync(backupPath);
191
+ }
192
+ return result;
193
+ }
194
+
195
+ /**
196
+ * 对业务空间批量安装:空间根仓库 + .codespace/ 下每个应用仓库。
197
+ * @param {string} wsRoot 业务空间根
198
+ * @returns {{ root: string, apps: {name: string, status: string}[] }}
199
+ */
200
+ function installGitHooksForWorkspace(wsRoot) {
201
+ const root = installGitHook(wsRoot);
202
+ const apps = [];
203
+ const codeDir = path.join(wsRoot, '.codespace');
204
+ if (fs.existsSync(codeDir)) {
205
+ for (const name of fs.readdirSync(codeDir)) {
206
+ const appDir = path.join(codeDir, name);
207
+ if (fs.existsSync(path.join(appDir, '.git'))) {
208
+ apps.push({ name, status: installGitHook(appDir) });
209
+ }
210
+ }
211
+ }
212
+ return { root, apps };
213
+ }
214
+
215
+ module.exports = {
216
+ HOOK_MARKER,
217
+ HOOK_FILE,
218
+ BACKUP_SUFFIX,
219
+ PRE_COMMIT_SOURCE,
220
+ isCbbHookContent,
221
+ buildPreCommitSource,
222
+ resolveHooksDir,
223
+ installGitHook,
224
+ uninstallGitHook,
225
+ installGitHooksForWorkspace,
226
+ };
@@ -4,7 +4,8 @@
4
4
  * .gitignore 管理的纯函数工具(无副作用,便于单测;由 init.js 的 I/O 层调用)。
5
5
  *
6
6
  * 安装时:若 .gitignore 已含某目录(认可带/不带尾斜杠两种形式),则不重复添加;
7
- * 否则新增无尾斜杠形式(.claude / .qoder,更通用)。.cbb/ 维持带斜杠。
7
+ * 否则新增无尾斜杠形式(.claude / .qoder,更通用)。.cbb/ / .codespace/ /
8
+ * .worktrees/ 这类纯目录路径维持带斜杠。
8
9
  * 卸载时:移除本包曾写入的所有形式(新旧版本均覆盖)。
9
10
  *
10
11
  * 注意:.gitignore 条目无 _managedBy 之类的标记,卸载按"形式匹配"移除,
@@ -13,12 +14,24 @@
13
14
 
14
15
  // 各目录:add = 写入形式;accept = 视为"已存在 / 需卸载移除"的可接受形式(含尾斜杠变体)。
15
16
  // .claude / .qoder 写无尾斜杠形式,但检测/卸载时同时认可带斜杠变体,避免重复添加、覆盖旧版本。
17
+ // .worktrees/ 自 0.0.3-beta.22 起随 setup 写入(ADR 0002:本地模式下 main 工作区恒净,
18
+ // 不再依赖 worktree-init 把该条目提交到 feature 分支——那会导致首个分支合并前 main 恒脏)。
16
19
  const GITIGNORE_RULES = [
17
20
  { add: '.claude', accept: ['.claude', '.claude/'] },
18
21
  { add: '.qoder', accept: ['.qoder', '.qoder/'] },
19
22
  { add: '.cbb/', accept: ['.cbb/'] },
20
23
  { add: '.codespace/', accept: ['.codespace/', '.codespace'] },
24
+ { add: '.worktrees/', accept: ['.worktrees/', '.worktrees'] },
21
25
  { add: '.opencode', accept: ['.opencode', '.opencode/'] },
26
+ // .codebuddy:beta.15 及以前写入的旧目录,不再新增(add: null),但卸载/清理时仍移除其行
27
+ { add: null, accept: ['.codebuddy', '.codebuddy/'] },
28
+ { add: '.workbuddy', accept: ['.workbuddy', '.workbuddy/'] },
29
+ // .trae-cn:beta.19 误把中国版项目级目录当成 .trae-cn(实为用户级目录),beta.20 起不再新增,
30
+ // 但卸载/清理时仍移除其 gitignore 行
31
+ { add: null, accept: ['.trae-cn', '.trae-cn/'] },
32
+ { add: '.trae', accept: ['.trae', '.trae/'] },
33
+ // .minimax:MiniMax Code 项目级配置根(rules / skills)
34
+ { add: '.minimax', accept: ['.minimax', '.minimax/'] },
22
35
  ];
23
36
 
24
37
  /** 将 .gitignore 文本拆为行数组。 */
@@ -29,12 +42,12 @@ function toLines(content) {
29
42
  /**
30
43
  * 给定 .gitignore 内容,返回需要新增的条目。
31
44
  * 已含任一可接受形式(带/不带尾斜杠)的目录会被跳过;全部已存在时返回空数组。
32
- * 返回的是写入形式(.claude / .qoder 无尾斜杠,.cbb/ 带尾斜杠)。
45
+ * 返回的是写入形式(.claude / .qoder 无尾斜杠,.cbb/ 带尾斜杠;add 为 null 的纯遗留规则不新增)。
33
46
  */
34
47
  function entriesToAdd(content) {
35
48
  const lines = toLines(content).map(l => l.trim());
36
49
  return GITIGNORE_RULES
37
- .filter(r => !r.accept.some(a => lines.includes(a)))
50
+ .filter(r => r.add && !r.accept.some(a => lines.includes(a)))
38
51
  .map(r => r.add);
39
52
  }
40
53
 
@@ -0,0 +1,92 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * MiniMax Code 本地插件 hook 的安装 / 卸载工具。
5
+ *
6
+ * MiniMax 无 Claude 式 settings.json hooks(hooks 仅插件作用域),版本检查改由
7
+ * 用户级本地插件承担:
8
+ * <dataDir>/plugins/cbb-check-update/
9
+ * .claude-plugin/plugin.json 兼容 manifest(仅 name 必填,走 CLAUDE 兼容读取器)
10
+ * hooks/hooks.json manifest 未声明 hooks 时的默认路径
11
+ *
12
+ * 机制(源码考证):
13
+ * - 本地插件根为 path.join(dataDir, 'plugins'),由扫描自动发现并热加载
14
+ * (package-storage.ts + watcher),无需重启 mcode;
15
+ * - 本地插件默认启用(不在 pluginDisabledLocalRoots 表即启用);
16
+ * - hook 事件 UserPromptSubmit,stdin 收 session_id JSON(与 Claude 同构),
17
+ * 输出 hookSpecificOutput.additionalContext 注入对话;
18
+ * - timeout 单位为秒(1–10,超限视为非法回落 5s);非 0 退出码(exit 2)会拒绝
19
+ * prompt —— check-update.js 恒 exit 0,绝不阻塞;
20
+ * - Windows 下 CLAUDE 格式命令经 Git Bash / PowerShell 执行(不走 cmd.exe)。
21
+ *
22
+ * 本插件目录整目录归本包管理:安装幂等覆盖本包两个文件,卸载直接删除目录。
23
+ */
24
+
25
+ const fs = require('fs');
26
+ const path = require('path');
27
+
28
+ /** 插件目录:dataDir + plugins + dirName */
29
+ function pluginDir({ dataDir, dirName }) {
30
+ return path.join(dataDir, 'plugins', dirName);
31
+ }
32
+
33
+ /** 幂等写入(内容一致不重写,避免触发插件 digest 重算) */
34
+ function writeIfChanged(file, content) {
35
+ if (fs.existsSync(file) && fs.readFileSync(file, 'utf-8') === content) return false;
36
+ fs.mkdirSync(path.dirname(file), { recursive: true });
37
+ fs.writeFileSync(file, content);
38
+ return true;
39
+ }
40
+
41
+ /**
42
+ * 安装(幂等)版本检查插件。
43
+ *
44
+ * @param {object} opts
45
+ * @param {string} opts.dataDir mcode 数据目录(由适配器 resolveDataDir 解析)
46
+ * @param {string} opts.dirName 插件目录名(cbb-check-update)
47
+ * @param {string[]} opts.events hook 事件(['UserPromptSubmit'])
48
+ * @param {number} opts.timeout 秒(1–10)
49
+ * @param {string} opts.command hook 命令(node "<check-update.js>")
50
+ * @param {string} opts.version 插件 manifest 版本(= 本包版本)
51
+ * @returns {{dir: string, changed: boolean}}
52
+ */
53
+ function install({ dataDir, dirName, events, timeout, command, version }) {
54
+ const root = pluginDir({ dataDir, dirName });
55
+
56
+ const manifest = {
57
+ name: dirName,
58
+ description: '@jspg-ai/coding-bb 版本检查 hook(UserPromptSubmit 触发,每日最多请求一次 registry)',
59
+ version: version || '0.0.0',
60
+ author: 'jspg-ai',
61
+ // 刻意不声明 hooks 字段:CLAUDE 兼容读取器对未声明的 hooks 走默认路径
62
+ // hooks/hooks.json(readPluginCapabilities 的 defaultPath),结构最稳。
63
+ };
64
+
65
+ const hooksDoc = {
66
+ hooks: Object.fromEntries(events.map(event => [event, [{
67
+ hooks: [{ type: 'command', command, timeout }],
68
+ }]])),
69
+ };
70
+
71
+ const changedManifest = writeIfChanged(
72
+ path.join(root, '.claude-plugin', 'plugin.json'),
73
+ JSON.stringify(manifest, null, 2) + '\n');
74
+ const changedHooks = writeIfChanged(
75
+ path.join(root, 'hooks', 'hooks.json'),
76
+ JSON.stringify(hooksDoc, null, 2) + '\n');
77
+
78
+ return { dir: root, changed: changedManifest || changedHooks };
79
+ }
80
+
81
+ /**
82
+ * 卸载版本检查插件(整目录删除)。
83
+ * @returns {boolean} 是否有删除动作
84
+ */
85
+ function remove({ dataDir, dirName }) {
86
+ const root = pluginDir({ dataDir, dirName });
87
+ if (!fs.existsSync(root)) return false;
88
+ fs.rmSync(root, { recursive: true, force: true });
89
+ return true;
90
+ }
91
+
92
+ module.exports = { pluginDir, install, remove };
@@ -11,6 +11,9 @@
11
11
  * removeLegacyHooks 负责清理旧版本曾写入、现已弃用的 SessionStart hook:
12
12
  * 当前仅安装 UserPromptSubmit,但升级自旧版本的用户 settings.json 中可能仍残留
13
13
  * SessionStart 条目,升级 / 卸载时需主动移除,否则会永久残留。
14
+ *
15
+ * 方言:Trae 的 hooks.json 需顶层 `version: 1` 且非 Claude 的 settings.json,
16
+ * 由 opts.dialect === 'trae' 处理(写入时补 version,清理干净后移除)。
14
17
  */
15
18
 
16
19
  const fs = require('fs');
@@ -53,12 +56,16 @@ function stripManaged(arr) {
53
56
  * @param {string} settingsPath settings.json 路径
54
57
  * @param {string} command 要执行的命令
55
58
  * @param {string[]} events 要写入的事件名数组,如 ['UserPromptSubmit']
59
+ * @param {{dialect?: string}} [opts] 方言:'trae' 时补顶层 version: 1(Trae hooks.json 要求)
56
60
  * 已存在本包标记的条目会先移除再添加(保证命令为最新路径),不重复。
57
61
  */
58
- function addHooks(settingsPath, command, events) {
62
+ function addHooks(settingsPath, command, events, opts = {}) {
59
63
  const settings = readSettings(settingsPath);
60
64
  if (!settings.hooks || typeof settings.hooks !== 'object') settings.hooks = {};
61
65
 
66
+ // Trae hooks.json 需要顶层 version 字段(当前仅支持 1)
67
+ if (opts.dialect === 'trae' && settings.version === undefined) settings.version = 1;
68
+
62
69
  events.forEach(event => {
63
70
  settings.hooks[event] = stripManaged(settings.hooks[event]);
64
71
  const entry = { _managedBy: MANAGED_BY, hooks: [{ type: 'command', command }] };
@@ -74,9 +81,10 @@ function addHooks(settingsPath, command, events) {
74
81
  * 清理变空的事件数组 / hooks。
75
82
  * @param {string} settingsPath settings.json 路径
76
83
  * @param {string[]} events 要清理的事件名数组
84
+ * @param {{dialect?: string}} [opts] 方言:'trae' 时把 version 一并视为可清理字段
77
85
  * 返回 true 表示有改动。
78
86
  */
79
- function removeHooks(settingsPath, events) {
87
+ function removeHooks(settingsPath, events, opts = {}) {
80
88
  if (!fs.existsSync(settingsPath)) return false;
81
89
  const settings = readSettings(settingsPath);
82
90
  if (!settings.hooks || typeof settings.hooks !== 'object') return false;
@@ -94,6 +102,11 @@ function removeHooks(settingsPath, events) {
94
102
 
95
103
  if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
96
104
 
105
+ // Trae:hooks 清空后 version 只是本包写入的脚手架,一并移除
106
+ if (opts.dialect === 'trae' && settings.version !== undefined && !settings.hooks) {
107
+ delete settings.version;
108
+ }
109
+
97
110
  // 文件变成空对象则删除,否则写回
98
111
  if (Object.keys(settings).length === 0) {
99
112
  fs.unlinkSync(settingsPath);
@@ -107,8 +120,8 @@ function removeHooks(settingsPath, events) {
107
120
  * 清理遗留事件下本包标记的 hook 条目(旧版本写入的 SessionStart 等)。
108
121
  * 复用 removeHooks 的清理逻辑;返回 true 表示有改动。
109
122
  */
110
- function removeLegacyHooks(settingsPath) {
111
- return removeHooks(settingsPath, LEGACY_HOOK_EVENTS);
123
+ function removeLegacyHooks(settingsPath, opts = {}) {
124
+ return removeHooks(settingsPath, LEGACY_HOOK_EVENTS, opts);
112
125
  }
113
126
 
114
127
  module.exports = {
@@ -0,0 +1,92 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * 跨平台 .tar.gz 解压工具。
5
+ *
6
+ * 用 zlib + 手写 tar parser,避开 OS shell tar(Windows 上是 bsdtar、行为不完全一致),
7
+ * 与仓库「脚本语言统一 Node.js、禁止平台绑定脚本」的约定一致。
8
+ *
9
+ * 支持:
10
+ * - gzip 压缩的 tar(GitHub tarball / npm registry tgz 都是这种)
11
+ * - 目录项(type '5' 或名字以 '/' 结尾)
12
+ * - 跳过 pax 扩展头('g'/'x')等元数据伪条目
13
+ * - 路径穿越防护(拒绝 ../ 逃逸 destDir)
14
+ *
15
+ * 不支持(上游 tarball 均不使用,遇到时跳过而非静默损坏):
16
+ * - GNU longname/longlink('L'/'K')—— 超过 100 字符的路径
17
+ */
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const zlib = require('zlib');
22
+
23
+ // 非文件、非目录的 tar 条目类型(pax 扩展头、GNU 长名等)
24
+ const META_TYPE_FLAGS = new Set(['g', 'x', 'L', 'K', 'V', 'N']);
25
+
26
+ /**
27
+ * 解析 tar buffer,返回条目列表(不落盘)。
28
+ * @param {Buffer} data - 已解压的 tar 数据
29
+ * @returns {Array<{name: string, size: number, typeFlag: string, dataStart: number}>}
30
+ */
31
+ function listTar(data) {
32
+ const entries = [];
33
+ let off = 0;
34
+ while (off + 512 <= data.length) {
35
+ const header = data.subarray(off, off + 512);
36
+ off += 512;
37
+
38
+ // 连续两个 512 字节零块 = tar 终止
39
+ if (header.every(b => b === 0)) break;
40
+
41
+ // 文件名 header[0..100](NUL 终止)
42
+ let nameEnd = 0;
43
+ while (nameEnd < 100 && header[nameEnd] !== 0) nameEnd++;
44
+ const name = header.subarray(0, nameEnd).toString('utf-8');
45
+
46
+ // 文件大小 header[124..136](八进制 ASCII)
47
+ const size = parseInt(header.subarray(124, 136).toString('utf-8').trim(), 8) || 0;
48
+
49
+ // 类型标志 header[156]('0'/NUL = 普通文件,'5' = 目录)
50
+ const typeFlag = String.fromCharCode(header[156]);
51
+
52
+ entries.push({ name, size, typeFlag, dataStart: off });
53
+ off += Math.ceil(size / 512) * 512;
54
+ }
55
+ return entries;
56
+ }
57
+
58
+ /**
59
+ * 解压 .tar.gz buffer 到目标目录。
60
+ * @param {Buffer} compressed - .tar.gz 文件内容
61
+ * @param {string} destDir - 解压目标根目录(不存在则创建)
62
+ * @returns {{files: number, dirs: number}} 落盘计数
63
+ */
64
+ function extractTarGz(compressed, destDir) {
65
+ const data = zlib.gunzipSync(compressed);
66
+ const root = path.resolve(destDir);
67
+ let files = 0;
68
+ let dirs = 0;
69
+
70
+ for (const entry of listTar(data)) {
71
+ if (!entry.name) continue;
72
+ // 跳过 pax 扩展头等元数据伪条目
73
+ if (META_TYPE_FLAGS.has(entry.typeFlag)) continue;
74
+
75
+ const target = path.resolve(root, entry.name);
76
+ // 路径穿越防护:解压目标必须落在 destDir 内
77
+ if (target !== root && !target.startsWith(root + path.sep)) continue;
78
+
79
+ if (entry.typeFlag === '5' || entry.name.endsWith('/')) {
80
+ fs.mkdirSync(target, { recursive: true });
81
+ dirs += 1;
82
+ } else {
83
+ fs.mkdirSync(path.dirname(target), { recursive: true });
84
+ fs.writeFileSync(target, data.subarray(entry.dataStart, entry.dataStart + entry.size));
85
+ files += 1;
86
+ }
87
+ }
88
+
89
+ return { files, dirs };
90
+ }
91
+
92
+ module.exports = { extractTarGz, listTar };
@@ -0,0 +1,90 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * 上游内容拉取的共享工具。
5
+ *
6
+ * 维护者侧(cbbm update --upstream-*)需要访问 GitHub / npm registry。
7
+ * 本模块提供:
8
+ * - 镜像前缀列表加载(config/upstream-mirrors.json,仓库内维护)
9
+ * - 带重定向跟随的 HTTPS GET
10
+ * - 「按镜像列表依次尝试」的拉取策略(首个成功即用)
11
+ *
12
+ * 镜像前缀为空字符串表示直连;列表末位通常放 "" 作为兜底。
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const https = require('https');
17
+ const path = require('path');
18
+
19
+ const SHARED_STANDARDS_DIR = path.join(__dirname, '..', '..', '..');
20
+ const MIRRORS_FILE = path.join(SHARED_STANDARDS_DIR, 'config', 'upstream-mirrors.json');
21
+
22
+ /**
23
+ * 读取指定上游的镜像前缀列表。
24
+ * 文件缺失 / 解析失败 / 未配置该上游时返回 [''](仅直连)。
25
+ * @param {string} name - 上游名(如 'superpowers')
26
+ * @returns {string[]}
27
+ */
28
+ function loadMirrors(name) {
29
+ try {
30
+ const data = JSON.parse(fs.readFileSync(MIRRORS_FILE, 'utf-8'));
31
+ const list = data[name];
32
+ if (Array.isArray(list) && list.length > 0) {
33
+ return list.map(m => String(m));
34
+ }
35
+ } catch (_) { /* 回退到直连 */ }
36
+ return [''];
37
+ }
38
+
39
+ /**
40
+ * 发起 HTTPS GET,跟随重定向(最多 8 次),返回响应体 Buffer。
41
+ * @param {string} url
42
+ * @param {number} [redirectCount]
43
+ * @returns {Promise<Buffer>}
44
+ */
45
+ function httpsGetBuffer(url, redirectCount = 0) {
46
+ if (redirectCount > 8) return Promise.reject(new Error('Too many redirects'));
47
+ return new Promise((resolve, reject) => {
48
+ https.get(url, { headers: { 'User-Agent': 'cbb-cli' } }, res => {
49
+ if ([301, 302, 307, 308].includes(res.statusCode) && res.headers.location) {
50
+ res.resume();
51
+ return httpsGetBuffer(res.headers.location, redirectCount + 1).then(resolve, reject);
52
+ }
53
+ if (res.statusCode !== 200) {
54
+ res.resume();
55
+ return reject(new Error(`HTTP ${res.statusCode}: ${url}`));
56
+ }
57
+ const chunks = [];
58
+ res.on('data', c => chunks.push(c));
59
+ res.on('end', () => resolve(Buffer.concat(chunks)));
60
+ res.on('error', reject);
61
+ }).on('error', reject);
62
+ });
63
+ }
64
+
65
+ /**
66
+ * 按镜像列表依次尝试拉取,首个成功即返回。
67
+ * @param {string[]} mirrors - 镜像前缀列表('' = 直连)
68
+ * @param {(prefix: string) => string} buildUrl - 由前缀构造完整 URL
69
+ * @returns {Promise<{buffer: Buffer, mirror: string, url: string}>}
70
+ */
71
+ async function fetchWithMirrors(mirrors, buildUrl) {
72
+ const errors = [];
73
+ for (const mirror of mirrors) {
74
+ const url = buildUrl(mirror);
75
+ try {
76
+ const buffer = await httpsGetBuffer(url);
77
+ return { buffer, mirror, url };
78
+ } catch (err) {
79
+ errors.push(`${mirror || '(直连)'}: ${err.message}`);
80
+ }
81
+ }
82
+ throw new Error(`所有镜像均失败:\n ${errors.join('\n ')}`);
83
+ }
84
+
85
+ module.exports = {
86
+ MIRRORS_FILE,
87
+ loadMirrors,
88
+ httpsGetBuffer,
89
+ fetchWithMirrors,
90
+ };