@jspg-ai/coding-bb 0.0.3-beta.8 → 0.0.4
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.
- package/cbb/dev-standards/rules/cbb-ai-behavior.md +0 -15
- package/cbb/dev-standards/rules/cbb-workspace-discipline.md +38 -0
- package/cbb/lib/external/index.js +228 -0
- package/cbb/lib/install/agents-nav.js +92 -0
- package/cbb/lib/install/claude-code.js +8 -1
- package/cbb/lib/install/command-skill.js +105 -0
- package/cbb/lib/install/init.js +399 -129
- package/cbb/lib/install/minimax.js +60 -0
- package/cbb/lib/install/rule-rewrite.js +1 -1
- package/cbb/lib/install/setup-decision.js +92 -0
- package/cbb/lib/install/tool-selection.js +23 -0
- package/cbb/lib/install/trae.js +18 -8
- package/cbb/lib/install/workbuddy.js +36 -0
- package/cbb/lib/install/workspaces.js +15 -61
- package/cbb/lib/utils/check-update.js +13 -5
- package/cbb/lib/utils/githook.js +230 -0
- package/cbb/lib/utils/gitignore.js +17 -4
- package/cbb/lib/utils/plugin-hook.js +92 -0
- package/cbb/worktrees/_shared/scripts/check-local-state.js +114 -0
- package/cbb/worktrees/_shared/scripts/check-unarchived.js +77 -0
- package/cbb/worktrees/_shared/scripts/delete-branches.js +147 -0
- package/cbb/worktrees/_shared/scripts/deprecate-branches.js +119 -0
- package/cbb/worktrees/_shared/scripts/find-target-worktree.js +148 -96
- package/cbb/worktrees/_shared/scripts/find-workspace-root.js +8 -8
- package/cbb/worktrees/_shared/scripts/install-git-hook.js +70 -0
- package/cbb/worktrees/_shared/scripts/junction-ai-config.js +192 -0
- package/cbb/worktrees/_shared/scripts/merge-branches.js +469 -0
- package/cbb/worktrees/_shared/scripts/push-core.js +1 -1
- package/cbb/worktrees/_shared/scripts/silence-popup.js +1 -1
- package/cbb/worktrees/_shared/scripts/start-local.js +180 -0
- package/cbb/worktrees/skills/cbb-local-mode/SKILL.md +194 -0
- package/cbb/worktrees/skills/cbb-local-mode/scripts/check-local-state.js +114 -0
- package/cbb/worktrees/skills/cbb-local-mode/scripts/delete-branches.js +147 -0
- package/cbb/worktrees/skills/cbb-local-mode/scripts/deprecate-branches.js +119 -0
- package/cbb/worktrees/skills/cbb-local-mode/scripts/install-git-hook.js +70 -0
- package/cbb/worktrees/skills/cbb-local-mode/scripts/merge-branches.js +469 -0
- package/cbb/worktrees/skills/cbb-local-mode/scripts/silence-popup.js +23 -0
- package/cbb/worktrees/skills/cbb-local-mode/scripts/start-local.js +180 -0
- package/cbb/worktrees/skills/cbb-worktree-close/SKILL.md +133 -81
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/check-env.js +3 -3
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/check-unarchived.js +77 -48
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/delete-branches.js +147 -136
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/deprecate-branches.js +119 -0
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/discover-apps.js +14 -5
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/find-target-worktree.js +148 -96
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/find-workspace-root.js +8 -8
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/junction-ai-config.js +192 -0
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/merge-branches.js +469 -0
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/remove-worktrees.js +44 -9
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/safety-check.js +15 -4
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/silence-popup.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-init/SKILL.md +136 -123
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/check-env.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/check-repos.js +2 -2
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/create-branches.js +33 -8
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/create-worktrees.js +60 -46
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/install-git-hook.js +70 -0
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/junction-ai-config.js +192 -0
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/parse-config.js +7 -7
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/push-branches.js +3 -3
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/push-core.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/silence-popup.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/sync-repos.js +11 -7
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/update-gitignore.js +37 -9
- package/cbb/worktrees/skills/cbb-worktree-push/SKILL.md +212 -56
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/check-unarchived.js +77 -0
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/commit-worktrees.js +70 -12
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/find-target-worktree.js +148 -96
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/find-workspace-root.js +8 -8
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/make-mr-urls.js +211 -0
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/merge-branches.js +469 -0
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/push-core.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/push-worktrees.js +67 -20
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/silence-popup.js +1 -1
- package/config/{workspace-config.sample.json → CBB-WORKSPACE.sample.json} +1 -1
- package/config/openspec/schemas/spec-driven/schema.yaml +20 -0
- package/config/upstream-mirrors.json +5 -0
- package/external/frontend-design/.version +6 -0
- package/external/frontend-design/LICENSE.txt +177 -0
- package/external/frontend-design/SKILL.md +71 -0
- package/openspec/.version +2 -2
- package/openspec/commands/archive.md +15 -8
- package/openspec/commands/bulk-archive.md +13 -6
- package/openspec/commands/continue.md +0 -1
- package/openspec/commands/explore.md +1 -1
- package/openspec/commands/ff.md +1 -1
- package/openspec/commands/onboard.md +9 -4
- package/openspec/commands/update.md +11 -5
- package/openspec/commands/verify.md +57 -26
- package/openspec/skills/openspec-apply-change/SKILL.md +1 -1
- package/openspec/skills/openspec-archive-change/SKILL.md +16 -9
- package/openspec/skills/openspec-bulk-archive-change/SKILL.md +14 -7
- package/openspec/skills/openspec-continue-change/SKILL.md +1 -2
- package/openspec/skills/openspec-explore/SKILL.md +2 -2
- package/openspec/skills/openspec-ff-change/SKILL.md +2 -2
- package/openspec/skills/openspec-new-change/SKILL.md +1 -1
- package/openspec/skills/openspec-onboard/SKILL.md +10 -5
- package/openspec/skills/openspec-propose/SKILL.md +1 -1
- package/openspec/skills/openspec-sync-specs/SKILL.md +1 -1
- package/openspec/skills/openspec-update-change/SKILL.md +12 -6
- package/openspec/skills/openspec-verify-change/SKILL.md +59 -27
- package/package.json +3 -2
- package/superpowers/.version +3 -3
- package/superpowers/skills/writing-plans/SKILL.md +32 -20
- package/cbb/lib/install/codebuddy.js +0 -30
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/find-workspace-root.js +0 -67
- package/config/workspace-agents.sample.md +0 -41
- package/superpowers/skills/writing-plans/plan-document-reviewer-prompt.md +0 -49
|
@@ -0,0 +1,230 @@
|
|
|
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
|
+
* - 主干上处于 merge 进行中(MERGE_HEAD 存在)→ 放行(ADR 0007 合并门禁的直合并通道)
|
|
13
|
+
* - 应用仓库在主干上 → 全拦(交付只走 MR;merge 进行中的合并提交除外)
|
|
14
|
+
* - 逃生口:用户显式 git commit --no-verify(AI 侧纪律禁止自动绕过)
|
|
15
|
+
*
|
|
16
|
+
* 本模块是 hook 源码的唯一真源;cbb/worktrees/_shared/scripts/install-git-hook.js
|
|
17
|
+
* 部署到业务空间后独立运行,内嵌同一份源码副本(改动须两处同步)。
|
|
18
|
+
*
|
|
19
|
+
* 提供纯函数(isCbbHookContent / buildPreCommitSource)与 I/O 包装
|
|
20
|
+
* (installGitHook / uninstallGitHook),供 init.js 的 setup / update / uninstall 调用。
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
const { execSync } = require('child_process');
|
|
26
|
+
|
|
27
|
+
const HOOK_MARKER = 'managed-by-cbb';
|
|
28
|
+
const HOOK_FILE = 'pre-commit';
|
|
29
|
+
const BACKUP_SUFFIX = '.cbb-backup';
|
|
30
|
+
|
|
31
|
+
// 部署到仓库后的 hook 源码(独立 Node 脚本,无外部依赖)。
|
|
32
|
+
// 注意:源码内不使用反引号,便于本模块以模板字符串承载。
|
|
33
|
+
const PRE_COMMIT_SOURCE = [
|
|
34
|
+
'#!/usr/bin/env node',
|
|
35
|
+
'// managed-by-cbb v1 —— 分支感知提交防线(ADR 0002)',
|
|
36
|
+
'// main 上:workspace 只放行控制面白名单,应用全拦(merge 提交除外);非 main 一律放行。',
|
|
37
|
+
'// 绕过逃生口仅限用户显式 git commit --no-verify。',
|
|
38
|
+
"const { execSync } = require('child_process');",
|
|
39
|
+
"const fs = require('fs');",
|
|
40
|
+
'function run(cmd) {',
|
|
41
|
+
" try { return execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim(); }",
|
|
42
|
+
' catch (e) { return null; }',
|
|
43
|
+
'}',
|
|
44
|
+
"const branch = run('git branch --show-current') || '';",
|
|
45
|
+
'// 主干名:优先远端 HEAD 指向;探测失败按本地 main/master 存在性回退',
|
|
46
|
+
'let main = null;',
|
|
47
|
+
"const remotes = run('git remote');",
|
|
48
|
+
'if (remotes) {',
|
|
49
|
+
" const r = remotes.split(/\\r?\\n/)[0];",
|
|
50
|
+
" const sym = run('git symbolic-ref \"refs/remotes/' + r + '/HEAD\"');",
|
|
51
|
+
" if (sym) main = sym.replace('refs/remotes/' + r + '/', '');",
|
|
52
|
+
'}',
|
|
53
|
+
"if (!main) {",
|
|
54
|
+
" if (run('git rev-parse --verify --quiet refs/heads/main')) main = 'main';",
|
|
55
|
+
" else if (run('git rev-parse --verify --quiet refs/heads/master')) main = 'master';",
|
|
56
|
+
'}',
|
|
57
|
+
'// 非主干(含 detached / 探测失败)→ 放行',
|
|
58
|
+
'if (!main || branch !== main) process.exit(0);',
|
|
59
|
+
'',
|
|
60
|
+
'// merge 进行中(MERGE_HEAD 存在)→ 显式人工合并提交,放行(ADR 0007 合并门禁)',
|
|
61
|
+
"if (run('git rev-parse --verify --quiet MERGE_HEAD')) process.exit(0);",
|
|
62
|
+
'',
|
|
63
|
+
"// 主干上的提交:收集 staged 变更(空提交放行)",
|
|
64
|
+
"const staged = run('git diff --cached --name-only') || '';",
|
|
65
|
+
"const files = staged.split(/\\r?\\n/).filter(Boolean);",
|
|
66
|
+
'if (files.length === 0) process.exit(0);',
|
|
67
|
+
'',
|
|
68
|
+
'// 仓库类型:workspace 仓库 = 检出含 CBB-WORKSPACE.json(或旧名,改名过渡期识别)',
|
|
69
|
+
"const isWorkspace = fs.existsSync('CBB-WORKSPACE.json') || fs.existsSync('workspace-config.json');",
|
|
70
|
+
'',
|
|
71
|
+
'function deny(bad) {',
|
|
72
|
+
" console.error('[cbb-guard] 主干上拦截载荷提交(ADR 0002):');",
|
|
73
|
+
" bad.forEach(f => console.error(' ' + f));",
|
|
74
|
+
" console.error('需求载荷须在需求分支上提交:本地模式先 git checkout -b <需求名>;worktree 模式在 .worktrees/ 工作树内提交。');",
|
|
75
|
+
" console.error('控制面白名单:CBB-WORKSPACE.json / .gitignore / AGENTS.md / openspec/config.yaml / openspec/schemas/** / .cbb/**');",
|
|
76
|
+
" console.error('如确需在主干提交,须由用户显式 git commit --no-verify。');",
|
|
77
|
+
' process.exit(1);',
|
|
78
|
+
'}',
|
|
79
|
+
'',
|
|
80
|
+
'if (isWorkspace) {',
|
|
81
|
+
' // 旧名仅放行删除条目:更名迁移提交(git mv)会产生旧名删除 + 新名新增两条',
|
|
82
|
+
" const EXACT = new Set(['CBB-WORKSPACE.json', 'workspace-config.json', '.gitignore', 'AGENTS.md', 'openspec/config.yaml']);",
|
|
83
|
+
" const PREFIXES = ['openspec/schemas/', '.cbb/'];",
|
|
84
|
+
' const bad = files.filter(f => !EXACT.has(f) && !PREFIXES.some(p => f.indexOf(p) === 0));',
|
|
85
|
+
' if (bad.length) deny(bad);',
|
|
86
|
+
'} else {',
|
|
87
|
+
' deny(files);',
|
|
88
|
+
'}',
|
|
89
|
+
'process.exit(0);',
|
|
90
|
+
].join('\n');
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 内容是否为本包管理的 hook(含标记行)。
|
|
94
|
+
*/
|
|
95
|
+
function isCbbHookContent(content) {
|
|
96
|
+
return typeof content === 'string' && content.includes(HOOK_MARKER);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* hook 源码(纯函数,便于单测与未来按仓库定制)。
|
|
101
|
+
*/
|
|
102
|
+
function buildPreCommitSource() {
|
|
103
|
+
return PRE_COMMIT_SOURCE + '\n';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 解析仓库的 hooks 目录。
|
|
108
|
+
* 优先 core.hooksPath 配置(相对路径基于仓库根);未配置或读取失败回退 .git/hooks。
|
|
109
|
+
* @returns {string|null} 绝对路径;目录不可用时返回 null(调用方跳过安装)
|
|
110
|
+
*/
|
|
111
|
+
function resolveHooksDir(repoPath) {
|
|
112
|
+
let configured = null;
|
|
113
|
+
try {
|
|
114
|
+
configured = execSync('git config core.hooksPath', {
|
|
115
|
+
cwd: repoPath,
|
|
116
|
+
encoding: 'utf-8',
|
|
117
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
118
|
+
timeout: 5000,
|
|
119
|
+
}).trim();
|
|
120
|
+
} catch (_) { /* 未配置 */ }
|
|
121
|
+
|
|
122
|
+
let dir;
|
|
123
|
+
if (configured) {
|
|
124
|
+
dir = path.isAbsolute(configured) ? configured : path.join(repoPath, configured);
|
|
125
|
+
} else {
|
|
126
|
+
const gitDir = path.join(repoPath, '.git');
|
|
127
|
+
if (!fs.existsSync(gitDir)) return null; // 非 git 仓库(或 .git 为 worktree 文件,见下)
|
|
128
|
+
// worktree 检出的 .git 是文件(内容 "gitdir: ...");hook 安装统一走主仓库,
|
|
129
|
+
// 调用方应传主仓库路径。此处对文件形态直接放弃,避免误建目录。
|
|
130
|
+
const st = fs.statSync(gitDir);
|
|
131
|
+
if (!st.isDirectory()) return null;
|
|
132
|
+
dir = path.join(gitDir, 'hooks');
|
|
133
|
+
}
|
|
134
|
+
return dir;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 安装(幂等,可用于升级覆盖):
|
|
139
|
+
* - 无既有 hook → 写入
|
|
140
|
+
* - 既有本包 hook → 覆盖(升级)
|
|
141
|
+
* - 既有用户 hook → 备份为 pre-commit.cbb-backup 后写入;卸载时恢复
|
|
142
|
+
* @param {string} repoPath 仓库根(主仓库;worktree 共享 hooks 无需重复安装)
|
|
143
|
+
* @returns {'installed' | 'updated' | 'skipped-not-git' | 'skipped-no-dir'}
|
|
144
|
+
*/
|
|
145
|
+
function installGitHook(repoPath) {
|
|
146
|
+
const dir = resolveHooksDir(repoPath);
|
|
147
|
+
if (!dir) return 'skipped-not-git';
|
|
148
|
+
|
|
149
|
+
const hookPath = path.join(dir, HOOK_FILE);
|
|
150
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
151
|
+
|
|
152
|
+
let prev = null;
|
|
153
|
+
if (fs.existsSync(hookPath)) {
|
|
154
|
+
prev = fs.readFileSync(hookPath, 'utf-8');
|
|
155
|
+
if (isCbbHookContent(prev)) {
|
|
156
|
+
if (prev === buildPreCommitSource()) return 'updated'; // 幂等:同版本
|
|
157
|
+
fs.writeFileSync(hookPath, buildPreCommitSource());
|
|
158
|
+
return 'updated';
|
|
159
|
+
}
|
|
160
|
+
// 用户自有 hook:备份后接管
|
|
161
|
+
fs.writeFileSync(hookPath + BACKUP_SUFFIX, prev);
|
|
162
|
+
}
|
|
163
|
+
fs.writeFileSync(hookPath, buildPreCommitSource());
|
|
164
|
+
try { fs.chmodSync(hookPath, 0o755); } catch (_) { /* Windows 无压力 */ }
|
|
165
|
+
return 'installed';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* 卸载:
|
|
170
|
+
* - 当前 hook 是本包的 → 删除;有备份则恢复
|
|
171
|
+
* - 当前 hook 非本包(用户已接管)→ 不动,仅清掉残留备份
|
|
172
|
+
* @returns {'removed' | 'restored-backup' | 'noop'}
|
|
173
|
+
*/
|
|
174
|
+
function uninstallGitHook(repoPath) {
|
|
175
|
+
const dir = resolveHooksDir(repoPath);
|
|
176
|
+
if (!dir) return 'noop';
|
|
177
|
+
|
|
178
|
+
const hookPath = path.join(dir, HOOK_FILE);
|
|
179
|
+
const backupPath = hookPath + BACKUP_SUFFIX;
|
|
180
|
+
let result = 'noop';
|
|
181
|
+
|
|
182
|
+
if (fs.existsSync(hookPath)) {
|
|
183
|
+
const content = fs.readFileSync(hookPath, 'utf-8');
|
|
184
|
+
if (isCbbHookContent(content)) {
|
|
185
|
+
fs.unlinkSync(hookPath);
|
|
186
|
+
result = 'removed';
|
|
187
|
+
if (fs.existsSync(backupPath)) {
|
|
188
|
+
fs.renameSync(backupPath, hookPath);
|
|
189
|
+
result = 'restored-backup';
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
} else if (fs.existsSync(backupPath)) {
|
|
193
|
+
// hook 已被用户删除但备份残留:不擅自恢复,只清备份
|
|
194
|
+
fs.unlinkSync(backupPath);
|
|
195
|
+
}
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 对业务空间批量安装:空间根仓库 + .codespace/ 下每个应用仓库。
|
|
201
|
+
* @param {string} wsRoot 业务空间根
|
|
202
|
+
* @returns {{ root: string, apps: {name: string, status: string}[] }}
|
|
203
|
+
*/
|
|
204
|
+
function installGitHooksForWorkspace(wsRoot) {
|
|
205
|
+
const root = installGitHook(wsRoot);
|
|
206
|
+
const apps = [];
|
|
207
|
+
const codeDir = path.join(wsRoot, '.codespace');
|
|
208
|
+
if (fs.existsSync(codeDir)) {
|
|
209
|
+
for (const name of fs.readdirSync(codeDir)) {
|
|
210
|
+
const appDir = path.join(codeDir, name);
|
|
211
|
+
if (fs.existsSync(path.join(appDir, '.git'))) {
|
|
212
|
+
apps.push({ name, status: installGitHook(appDir) });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return { root, apps };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
module.exports = {
|
|
220
|
+
HOOK_MARKER,
|
|
221
|
+
HOOK_FILE,
|
|
222
|
+
BACKUP_SUFFIX,
|
|
223
|
+
PRE_COMMIT_SOURCE,
|
|
224
|
+
isCbbHookContent,
|
|
225
|
+
buildPreCommitSource,
|
|
226
|
+
resolveHooksDir,
|
|
227
|
+
installGitHook,
|
|
228
|
+
uninstallGitHook,
|
|
229
|
+
installGitHooksForWorkspace,
|
|
230
|
+
};
|
|
@@ -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/ / .build/ 这类纯目录路径维持带斜杠。
|
|
8
9
|
* 卸载时:移除本包曾写入的所有形式(新旧版本均覆盖)。
|
|
9
10
|
*
|
|
10
11
|
* 注意:.gitignore 条目无 _managedBy 之类的标记,卸载按"形式匹配"移除,
|
|
@@ -13,14 +14,26 @@
|
|
|
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/'] },
|
|
22
|
-
|
|
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/'] },
|
|
23
32
|
{ add: '.trae', accept: ['.trae', '.trae/'] },
|
|
33
|
+
// .minimax:MiniMax Code 项目级配置根(rules / skills)
|
|
34
|
+
{ add: '.minimax', accept: ['.minimax', '.minimax/'] },
|
|
35
|
+
// .build/:0.0.3-beta.38 新增 —— 打包产物收集目录(空间纪律第 5 条:产物按需求聚合到 .build/<需求名>/)
|
|
36
|
+
{ add: '.build/', accept: ['.build/', '.build'] },
|
|
24
37
|
];
|
|
25
38
|
|
|
26
39
|
/** 将 .gitignore 文本拆为行数组。 */
|
|
@@ -31,12 +44,12 @@ function toLines(content) {
|
|
|
31
44
|
/**
|
|
32
45
|
* 给定 .gitignore 内容,返回需要新增的条目。
|
|
33
46
|
* 已含任一可接受形式(带/不带尾斜杠)的目录会被跳过;全部已存在时返回空数组。
|
|
34
|
-
* 返回的是写入形式(.claude / .qoder 无尾斜杠,.cbb/
|
|
47
|
+
* 返回的是写入形式(.claude / .qoder 无尾斜杠,.cbb/ 带尾斜杠;add 为 null 的纯遗留规则不新增)。
|
|
35
48
|
*/
|
|
36
49
|
function entriesToAdd(content) {
|
|
37
50
|
const lines = toLines(content).map(l => l.trim());
|
|
38
51
|
return GITIGNORE_RULES
|
|
39
|
-
.filter(r => !r.accept.some(a => lines.includes(a)))
|
|
52
|
+
.filter(r => r.add && !r.accept.some(a => lines.includes(a)))
|
|
40
53
|
.map(r => r.add);
|
|
41
54
|
}
|
|
42
55
|
|
|
@@ -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 };
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 本地模式状态自检(ADR 0002,改动清单 #12)。
|
|
4
|
+
*
|
|
5
|
+
* 用法:node check-local-state.js <ws_root>
|
|
6
|
+
*
|
|
7
|
+
* 扫描空间根 + 所有 .codespace/<app>/ 的 { 当前分支, dirty },
|
|
8
|
+
* 判定「干净态」(全部在主干且 clean)或「遗留态」(任一仓库检出在非主干分支)。
|
|
9
|
+
* 干净态 → 零交互直接进入模式选择;遗留态 → agent 升级为用户决策
|
|
10
|
+
* (继续 / 收尾 / 升级 worktree / 丢弃)。同名分支集合 = 遗留需求的仓库布局。
|
|
11
|
+
*
|
|
12
|
+
* 输出:
|
|
13
|
+
* 人类可读报告(逐仓库状态 + 态判定 + 推断的需求名)
|
|
14
|
+
* 末行 LOCAL_STATE { state, branches[], repos[{name,branch,dirty}] }
|
|
15
|
+
* - state: 'clean' | 'active'(active = 存在非主干检出)
|
|
16
|
+
* - branches: 非主干分支名集合(通常为 1 个 = 遗留需求名;多个不同名 → 多头遗留,需用户逐个处理)
|
|
17
|
+
*
|
|
18
|
+
* 退出码:
|
|
19
|
+
* 0 - 扫描完成(无论何种状态;态判定交给 agent 解读)
|
|
20
|
+
* 1 - 参数错误
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const { execSync } = require('child_process');
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
|
|
27
|
+
const wsRoot = process.argv[2];
|
|
28
|
+
|
|
29
|
+
if (!wsRoot) {
|
|
30
|
+
console.error('Usage: node check-local-state.js <ws_root>');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function run(cmd, cwd) {
|
|
35
|
+
try {
|
|
36
|
+
return execSync(cmd, { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
37
|
+
} catch (e) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 主干名:symbolic-ref 远端 HEAD → 本地 main/master 存在性回退;探测失败 null */
|
|
43
|
+
function detectMain(cwd) {
|
|
44
|
+
const remotes = run('git remote', cwd);
|
|
45
|
+
if (remotes) {
|
|
46
|
+
const r = remotes.split(/\r?\n/)[0];
|
|
47
|
+
const sym = run('git symbolic-ref "refs/remotes/' + r + '/HEAD"', cwd);
|
|
48
|
+
if (sym) return sym.replace('refs/remotes/' + r + '/', '');
|
|
49
|
+
}
|
|
50
|
+
if (run('git rev-parse --verify --quiet refs/heads/main', cwd)) return 'main';
|
|
51
|
+
if (run('git rev-parse --verify --quiet refs/heads/master', cwd)) return 'master';
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function scanRepo(cwd, name) {
|
|
56
|
+
const branch = run('git branch --show-current', cwd) || '(detached)';
|
|
57
|
+
const status = run('git status --porcelain', cwd);
|
|
58
|
+
const dirty = !!(status && status.length);
|
|
59
|
+
const main = detectMain(cwd);
|
|
60
|
+
const repo = { name, branch, dirty };
|
|
61
|
+
if (main) repo.main = main;
|
|
62
|
+
return repo;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── 扫描空间根 ─────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
if (!fs.existsSync(path.join(wsRoot, '.git'))) {
|
|
68
|
+
console.error('❌ ' + wsRoot + ' 不是 git 仓库');
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const repos = [scanRepo(wsRoot, 'workspace')];
|
|
73
|
+
|
|
74
|
+
// ── 扫描 .codespace/ 各应用 ───────────────────────────────
|
|
75
|
+
|
|
76
|
+
const codeDir = path.join(wsRoot, '.codespace');
|
|
77
|
+
if (fs.existsSync(codeDir)) {
|
|
78
|
+
for (const name of fs.readdirSync(codeDir).sort()) {
|
|
79
|
+
const appDir = path.join(codeDir, name);
|
|
80
|
+
if (fs.existsSync(path.join(appDir, '.git'))) {
|
|
81
|
+
repos.push(scanRepo(appDir, name));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── 态判定 ─────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
for (const r of repos) {
|
|
89
|
+
const icon = (r.main && r.branch === r.main) ? '📄' : '🌿';
|
|
90
|
+
const dirtyTag = r.dirty ? '(有未提交修改)' : '';
|
|
91
|
+
const baseTag = (r.main && r.branch !== r.main) ? ' ← 主干 ' + r.main : '';
|
|
92
|
+
console.log(icon + ' ' + r.name + ': ' + r.branch + dirtyTag + baseTag);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const active = repos.filter(r => !(r.main && r.branch === r.main));
|
|
96
|
+
const branches = [...new Set(active.map(r => r.branch))];
|
|
97
|
+
|
|
98
|
+
let state;
|
|
99
|
+
if (active.length === 0) {
|
|
100
|
+
state = 'clean';
|
|
101
|
+
console.log('✅ 干净态:全部仓库在主干且可开工(模式选择不受限)');
|
|
102
|
+
} else {
|
|
103
|
+
state = 'active';
|
|
104
|
+
console.log('⚠️ 遗留态:检测到本地需求进行中(分支:' + branches.join(', ') + ')');
|
|
105
|
+
console.log(' 参与仓库:' + active.map(r => r.name).join(', '));
|
|
106
|
+
if (branches.length > 1) {
|
|
107
|
+
console.log(' ⚠️ 存在多个不同分支名——多头遗留,需用户逐个处理');
|
|
108
|
+
}
|
|
109
|
+
console.log(' 处理选项:继续该需求 / 先收尾 / 升级 worktree(git worktree add)/ 丢弃');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
console.log('---');
|
|
113
|
+
console.log('LOCAL_STATE', JSON.stringify({ state, branches, repos }));
|
|
114
|
+
process.exit(0);
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
require('./silence-popup');
|
|
3
|
+
/**
|
|
4
|
+
* 未归档 change 检查(共享脚本,真源:cbb/worktrees/_shared/scripts/)
|
|
5
|
+
*
|
|
6
|
+
* 供 cbb-worktree-close(收尾安全评估)与 cbb-worktree-push(Step 7 直合并前置,ADR 0008)调用。
|
|
7
|
+
*
|
|
8
|
+
* 用法:node check-unarchived.js <ws_root> [branch]
|
|
9
|
+
*
|
|
10
|
+
* 双扫(ADR 0002):
|
|
11
|
+
* - <ws_root>/openspec/changes/ → 空间根当前检出(本地模式产物 / 空间根遗留)
|
|
12
|
+
* - <ws_root>/.worktrees/worktree-<branch>/openspec/changes/ → worktree 模式产物
|
|
13
|
+
* (branch 未传时跳过 worktree 扫描,保持旧调用兼容)
|
|
14
|
+
*
|
|
15
|
+
* 输出:changes 为两处合集(去重);detail 标注每个 change 的来源
|
|
16
|
+
* ('root' / 'worktree',同名两处共存时为 'both')。
|
|
17
|
+
* 本检查为信息收集性质,不阻断流程。
|
|
18
|
+
*
|
|
19
|
+
* 退出码:
|
|
20
|
+
* 0 - 检查完成(无论是否有未归档 change)
|
|
21
|
+
* 1 - 参数错误
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
|
|
27
|
+
const wsRoot = process.argv[2];
|
|
28
|
+
const branch = process.argv[3];
|
|
29
|
+
|
|
30
|
+
if (!wsRoot) {
|
|
31
|
+
console.error('Usage: node check-unarchived.js <ws_root> [branch]');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 扫描单个 changes 目录,返回未归档 change 名数组(目录不存在返回 [])。
|
|
37
|
+
*/
|
|
38
|
+
function scanDir(changesDir) {
|
|
39
|
+
if (!fs.existsSync(changesDir)) return [];
|
|
40
|
+
let entries;
|
|
41
|
+
try {
|
|
42
|
+
entries = fs.readdirSync(changesDir, { withFileTypes: true });
|
|
43
|
+
} catch (e) {
|
|
44
|
+
console.error('Failed to read ' + changesDir + ':', e.message);
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
return entries
|
|
48
|
+
.filter(e => e.isDirectory() && e.name !== 'archive')
|
|
49
|
+
.map(e => e.name);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const rootChanges = scanDir(path.join(wsRoot, 'openspec', 'changes'));
|
|
53
|
+
|
|
54
|
+
const worktreeDir = branch
|
|
55
|
+
? path.join(wsRoot, '.worktrees', 'worktree-' + branch, 'openspec', 'changes')
|
|
56
|
+
: null;
|
|
57
|
+
const wtChanges = worktreeDir ? scanDir(worktreeDir) : [];
|
|
58
|
+
|
|
59
|
+
const detail = {};
|
|
60
|
+
for (const name of rootChanges) detail[name] = 'root';
|
|
61
|
+
for (const name of wtChanges) {
|
|
62
|
+
detail[name] = detail[name] === 'root' ? 'both' : 'worktree';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const changes = Object.keys(detail).sort();
|
|
66
|
+
|
|
67
|
+
if (rootChanges.length === 0 && wtChanges.length === 0) {
|
|
68
|
+
console.log('ℹ️ 未发现未归档 change(根与 worktree' + (worktreeDir ? '' : '(未指定 branch,跳过)') + '均无)');
|
|
69
|
+
} else {
|
|
70
|
+
console.log('ℹ️ 未归档 change:' + (changes.join(', ') || '无'));
|
|
71
|
+
for (const name of changes) {
|
|
72
|
+
console.log(' - ' + name + '(来源:' + detail[name] + ')');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log('UNARCHIVED_RESULT', JSON.stringify({ changes, detail }));
|
|
77
|
+
process.exit(0);
|