@deepstorm/cli 0.9.1 → 0.9.3
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/dist/cli.js +8 -5
- package/dist/skills/reef-commit/SKILL.md +27 -100
- package/dist/skills/reef-commit/scripts/branch-check.mjs +109 -0
- package/dist/skills/reef-commit/scripts/check-openspec-status.mjs +92 -0
- package/dist/skills/reef-commit/scripts/collect-git-context.mjs +186 -0
- package/dist/skills/reef-commit/scripts/run-tests.sh +91 -0
- package/dist/skills/reef-commit/scripts/stash-and-switch.sh +44 -0
- package/dist/skills/reef-gen-backend/SKILL.md.tmpl +10 -4
- package/dist/skills/reef-gen-frontend/SKILL.md.tmpl +10 -4
- package/dist/skills/reef-harden/SKILL.md +11 -19
- package/dist/skills/reef-harden/scripts/find-change-dir.mjs +157 -0
- package/dist/skills/reef-pr/SKILL.md +7 -19
- package/dist/skills/reef-pr/scripts/create-pr.mjs +217 -0
- package/dist/skills/reef-start/SKILL.md.tmpl +1 -0
- package/dist/skills/reef-start/references/stage-4-implementation.md +9 -5
- package/dist/skills/reef-start/references/superpowers-gate.md +3 -0
- package/dist/skills/sweep-init/SKILL.md +12 -125
- package/dist/skills/sweep-init/scripts/init-project.mjs +166 -0
- package/dist/skills/sweep-run/SKILL.md +33 -49
- package/dist/skills/sweep-run/scripts/env-manager.mjs +71 -2
- package/dist/skills/sweep-run/scripts/flow-selector.mjs +58 -1
- package/dist/skills/sweep-run/scripts/generate-report.mjs +116 -0
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -6351,10 +6351,11 @@ async function promptContinueSetup(targetDir, projectName, registry2) {
|
|
|
6351
6351
|
}
|
|
6352
6352
|
function buildContext(opts) {
|
|
6353
6353
|
const projectName = opts.projectName || "project";
|
|
6354
|
+
const packageName = projectName.replace(/[_-]/g, "").toLowerCase();
|
|
6354
6355
|
return {
|
|
6355
6356
|
projectName,
|
|
6356
|
-
packageName
|
|
6357
|
-
groupId:
|
|
6357
|
+
packageName,
|
|
6358
|
+
groupId: `com.${packageName}`,
|
|
6358
6359
|
frontend: opts.frontend || false,
|
|
6359
6360
|
backend: opts.backend || false,
|
|
6360
6361
|
uiLib: opts.uiLib || false,
|
|
@@ -6822,7 +6823,7 @@ tasks.withType<Test> {
|
|
|
6822
6823
|
useJUnitPlatform()
|
|
6823
6824
|
}
|
|
6824
6825
|
`);
|
|
6825
|
-
const appPackage =
|
|
6826
|
+
const appPackage = ctx.groupId.replace(/[.-]/g, ".");
|
|
6826
6827
|
const appDir = `src/main/java/${appPackage.replace(/\./g, "/")}`;
|
|
6827
6828
|
ensureDir2(path2.join(projectDir, appDir));
|
|
6828
6829
|
writeTemplate(projectDir, `${appDir}/Application.java`, `package ${appPackage};
|
|
@@ -6917,6 +6918,8 @@ logging:
|
|
|
6917
6918
|
${ctx.packageName}: DEBUG
|
|
6918
6919
|
`);
|
|
6919
6920
|
if (hasLiquibase) {
|
|
6921
|
+
const now = /* @__PURE__ */ new Date();
|
|
6922
|
+
const ts = now.toISOString().replace(/\D/g, "").slice(0, 12);
|
|
6920
6923
|
ensureDir2(path2.join(projectDir, "src/main/resources/db/changelog"));
|
|
6921
6924
|
writeTemplate(projectDir, "src/main/resources/db/changelog/db.changelog-master.xml", `<?xml version="1.0" encoding="UTF-8"?>
|
|
6922
6925
|
<databaseChangeLog
|
|
@@ -6925,7 +6928,7 @@ logging:
|
|
|
6925
6928
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
|
6926
6929
|
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
|
|
6927
6930
|
|
|
6928
|
-
<changeSet id="001" author="deepstorm">
|
|
6931
|
+
<changeSet id="${ts}-001" author="deepstorm">
|
|
6929
6932
|
<createTable tableName="flyway_schema_history">
|
|
6930
6933
|
<column name="version" type="VARCHAR(50)"/>
|
|
6931
6934
|
</createTable>
|
|
@@ -9979,7 +9982,7 @@ function runDoctor(targetDir) {
|
|
|
9979
9982
|
checks.push({
|
|
9980
9983
|
name: "CLI \u7248\u672C",
|
|
9981
9984
|
status: "pass",
|
|
9982
|
-
message: `v${version}\uFF08\u8FD0\u884C deepstorm update
|
|
9985
|
+
message: `v${version}\uFF08\u8FD0\u884C deepstorm update \u68C0\u67E5\u6700\u65B0\u7248\u672C\uFF09`
|
|
9983
9986
|
});
|
|
9984
9987
|
let hasDeepStorm = false;
|
|
9985
9988
|
if (!fs25.existsSync(settingsPath)) {
|
|
@@ -41,75 +41,32 @@ git status --short
|
|
|
41
41
|
|
|
42
42
|
### 2. 分支名与任务相关性检查
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
通过 `branch-check.mjs` 检查当前分支是否合法:
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
|
-
|
|
48
|
-
echo "当前分支: $BRANCH"
|
|
49
|
-
|
|
50
|
-
# 检测是否需要创建新分支
|
|
51
|
-
MUST_NEW_BRANCH=false
|
|
52
|
-
|
|
53
|
-
# 条件一:在 main 或 master 上必须创建新分支
|
|
54
|
-
if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then
|
|
55
|
-
echo "⚠️ 当前在 $BRANCH 分支上,不允许直接提交,必须创建新分支"
|
|
56
|
-
MUST_NEW_BRANCH=true
|
|
57
|
-
fi
|
|
58
|
-
|
|
59
|
-
# 条件二:检查分支名是否包含 temp、wip、test、tmp、dev 等临时名称
|
|
60
|
-
TEMP_PATTERN='^(temp|wip|test|tmp|dev)(/.*)?$'
|
|
61
|
-
if echo "$BRANCH" | grep -qE "$TEMP_PATTERN"; then
|
|
62
|
-
echo "⚠️ 当前分支名 ($BRANCH) 看起来是临时分支,建议创建有意义的新分支"
|
|
63
|
-
fi
|
|
64
|
-
|
|
65
|
-
# 收集 OpenSpec 任务上下文
|
|
66
|
-
for dir in openspec/changes/*/; do
|
|
67
|
-
if [ -f "$dir/proposal.md" ]; then
|
|
68
|
-
TASK_NAME=$(basename "$dir")
|
|
69
|
-
echo "发现 OpenSpec 任务: $TASK_NAME"
|
|
70
|
-
fi
|
|
71
|
-
done
|
|
47
|
+
node packages/reef/skills/reef-commit/scripts/branch-check.mjs
|
|
72
48
|
```
|
|
73
49
|
|
|
74
|
-
|
|
50
|
+
输出 JSON:
|
|
51
|
+
```json
|
|
52
|
+
{"isValid":true,"warning":false,"action":"continue"}
|
|
53
|
+
```
|
|
75
54
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
- 用户不同意 → 继续当前分支
|
|
82
|
-
3. **如果分支名合理**(如与 OpenSpec 任务同名,或包含功能描述的 kebab-case):
|
|
83
|
-
- 直接继续
|
|
55
|
+
**判断规则:**
|
|
56
|
+
- `isValid: false, action: "create-branch"`(当前在 main/master)→ **直接进入步骤 3**
|
|
57
|
+
- `warning: true, action: "suggest-rename"`(分支名像临时分支)→ 询问用户是否改名
|
|
58
|
+
- `matchedTask` 有值 → 说明已匹配到 OpenSpec 任务
|
|
59
|
+
- 其他 → 直接继续
|
|
84
60
|
|
|
85
61
|
### 3. 创建新分支
|
|
86
62
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
**分支名生成规则(优先级从高到低):**
|
|
90
|
-
1. **OpenSpec 任务名**:如果检测到 `openspec/changes/<task>/proposal.md`,使用 `<task>` 作为分支名
|
|
63
|
+
按以下逻辑确定分支名(优先级从高到低):
|
|
64
|
+
1. **OpenSpec 任务名**:由 `branch-check.mjs` 输出的 `matchedTask` 确定
|
|
91
65
|
2. **用户输入**:询问用户想要的分支名
|
|
92
|
-
3. **AI 推导**:根据变更内容总结生成 kebab-case
|
|
66
|
+
3. **AI 推导**:根据变更内容总结生成 kebab-case 分支名
|
|
93
67
|
|
|
94
68
|
```bash
|
|
95
|
-
|
|
96
|
-
STASHED=false
|
|
97
|
-
if [ -n "$(git status --porcelain)" ]; then
|
|
98
|
-
git stash push -m "reef-commit-auto-stash"
|
|
99
|
-
STASHED=true
|
|
100
|
-
fi
|
|
101
|
-
|
|
102
|
-
# 创建并切换到新分支(基于 main)
|
|
103
|
-
git checkout main
|
|
104
|
-
git pull origin main 2>/dev/null || true
|
|
105
|
-
git checkout -b <new-branch-name>
|
|
106
|
-
|
|
107
|
-
# 恢复暂存的变更
|
|
108
|
-
if [ "$STASHED" = true ]; then
|
|
109
|
-
git stash pop
|
|
110
|
-
fi
|
|
111
|
-
|
|
112
|
-
echo "✅ 已切换到新分支: <new-branch-name>"
|
|
69
|
+
bash packages/reef/skills/reef-commit/scripts/stash-and-switch.sh <new-branch-name>
|
|
113
70
|
```
|
|
114
71
|
|
|
115
72
|
> **注意**:创建新分支后,后续步骤(审查文件、范围检查、测试等)在新分支上继续执行。
|
|
@@ -129,9 +86,6 @@ git diff --stat
|
|
|
129
86
|
检查当前分支是否涉及多个业务领域。如果跨领域,给出拆分建议并中止提交。
|
|
130
87
|
|
|
131
88
|
```bash
|
|
132
|
-
BRANCH=$(git branch --show-current)
|
|
133
|
-
FORK_POINT=$(git merge-base main HEAD 2>/dev/null || echo "")
|
|
134
|
-
echo "检查分支范围(基准: main)..."
|
|
135
89
|
SCOPE_HOOK="packages/reef/hooks/reef-scope-gate.sh"
|
|
136
90
|
if [ -f "$SCOPE_HOOK" ]; then
|
|
137
91
|
bash "$SCOPE_HOOK" || {
|
|
@@ -146,64 +100,37 @@ fi
|
|
|
146
100
|
### 6. 运行单元测试
|
|
147
101
|
|
|
148
102
|
```bash
|
|
149
|
-
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
如有变更则全量运行测试:
|
|
153
|
-
|
|
154
|
-
```bash
|
|
155
|
-
./gradlew test
|
|
156
|
-
(cd src/main/web && pnpm test -- --run)
|
|
103
|
+
bash packages/reef/skills/reef-commit/scripts/run-tests.sh --json
|
|
157
104
|
```
|
|
158
105
|
|
|
159
|
-
-
|
|
160
|
-
-
|
|
106
|
+
- 全部通过(`allPassed: true`)→ 继续
|
|
107
|
+
- 任一测试失败(`allPassed: false`)→ 提示用户修复后再提交
|
|
161
108
|
|
|
162
109
|
### 6.5 OpenSpec 验证与归档检查
|
|
163
110
|
|
|
164
111
|
> 提交前检查关联的 OpenSpec change 是否已完成验证和归档。如果 `openspec/` 目录不存在或无活跃 change 则跳过本步骤。
|
|
165
112
|
|
|
166
|
-
|
|
113
|
+
**判断流程:**
|
|
167
114
|
|
|
168
115
|
1. **查找关联的 OpenSpec change:**
|
|
169
116
|
```bash
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
for dir in openspec/changes/*/; do
|
|
174
|
-
CHANGE_NAME=$(basename "$dir")
|
|
175
|
-
if [ -f "$dir/.openspec.yaml" ] && [ "$CHANGE_NAME" != "archive" ]; then
|
|
176
|
-
echo "发现活跃 OpenSpec change: $CHANGE_NAME"
|
|
177
|
-
cat "$dir/.openspec.yaml"
|
|
178
|
-
fi
|
|
179
|
-
done
|
|
117
|
+
node packages/reef/skills/reef-commit/scripts/check-openspec-status.mjs \
|
|
118
|
+
--branch "$(git branch --show-current)"
|
|
180
119
|
```
|
|
181
120
|
|
|
182
|
-
2. **匹配规则:**
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
4. **运行验证:** 确认 `tasks.md` 全部 checkbox 已完成 → 通过 Skill 工具自动调用 `/opsx:verify`。有 CRITICAL 问题则中止;仅 WARNING/SUGGESTION 则通过。
|
|
187
|
-
|
|
121
|
+
2. **匹配规则:** 脚本输出中 `name` 匹配当前分支名,`tasksAllDone` 检查任务完成状态。
|
|
122
|
+
3. **检查归档状态:** 无输出(`noMatch: true`)→ 跳过。`hasTasksMd: true` + `tasksAllDone: true` → 已全部完成 → 调用 `/opsx:verify` 和 `/opsx:archive`。
|
|
123
|
+
4. **运行验证:** 确认后通过 Skill 工具自动调用 `/opsx:verify`。有 CRITICAL 问题则中止;仅 WARNING/SUGGESTION 则通过。
|
|
188
124
|
5. **运行归档:** 验证通过后 → 通过 Skill 工具自动调用 `/opsx:archive`。执行失败则提示用户手动处理。
|
|
189
125
|
|
|
190
|
-
6. **确认已就绪:** 校验状态并向用户报告。
|
|
191
|
-
|
|
192
|
-
> **提示:** verify/archive 执行后产生额外文件变更的,后续步骤会重新检测并纳入提交。
|
|
193
|
-
|
|
194
126
|
### 7. 收集上下文
|
|
195
127
|
|
|
196
128
|
```bash
|
|
197
|
-
|
|
198
|
-
FORK_POINT=$(git merge-base main HEAD 2>/dev/null)
|
|
199
|
-
echo "Branch: $BRANCH"; git diff "$FORK_POINT"..HEAD --stat
|
|
200
|
-
ls -d openspec/changes/*/ 2>/dev/null | grep -v archive
|
|
201
|
-
[ -f "openspec/changes/$BRANCH/proposal.md" ] && head -5 "openspec/changes/$BRANCH/proposal.md"
|
|
202
|
-
# 从 proposal/commit 提取 Issue 引用
|
|
203
|
-
grep -iE '(issue|jira|lc-|proj-)' "openspec/changes/$BRANCH/proposal.md" 2>/dev/null | head -3
|
|
204
|
-
git log "$FORK_POINT"..HEAD --format="%B" 2>/dev/null | grep -ioP '[A-Z]+-\d+' | head -1
|
|
129
|
+
node packages/reef/skills/reef-commit/scripts/collect-git-context.mjs
|
|
205
130
|
```
|
|
206
131
|
|
|
132
|
+
输出 JSON 包含 `branch`、`diffStat`、`commitLog`、`openspecChanges` 等字段。提取 `openspecChanges` 中 `matched: true` 项的 `proposal.md` 标题作为提交信息素材。
|
|
133
|
+
|
|
207
134
|
### 8. 生成提交信息
|
|
208
135
|
|
|
209
136
|
**标题:** 优先 OpenSpec proposal 标题 → 分支名(kebab-case 转中文)→ 变更总结。依赖更新用 `更新 {依赖} 至 {版本}`,问题修复用 `解决{问题描述}的问题`。
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* branch-check.mjs
|
|
5
|
+
* 检查当前分支是否合法。
|
|
6
|
+
*
|
|
7
|
+
* 用法:
|
|
8
|
+
* node branch-check.mjs
|
|
9
|
+
* node branch-check.mjs --branch feat/add-auth
|
|
10
|
+
* node branch-check.mjs --help
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { execSync } from 'node:child_process';
|
|
14
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
|
|
17
|
+
// ── Exported constants (for test access) ──────────────────────────
|
|
18
|
+
|
|
19
|
+
/** 临时分支名正则 */
|
|
20
|
+
export const TEMP_PATTERN = /^(temp|wip|test|tmp|dev)(\/.*)?$/;
|
|
21
|
+
|
|
22
|
+
// ── Pure functions ────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
export function isMainOrMaster(branch) {
|
|
25
|
+
return branch === 'main' || branch === 'master';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 检查分支是否合法。
|
|
30
|
+
* @param {string} branch
|
|
31
|
+
* @param {Array<{name:string}>} openspecTasks
|
|
32
|
+
* @returns {{isValid:boolean, reason?:string, action:string,
|
|
33
|
+
* warning:boolean, matchedTask?:string}}
|
|
34
|
+
*/
|
|
35
|
+
export function checkBranch(branch, openspecTasks = []) {
|
|
36
|
+
if (isMainOrMaster(branch)) {
|
|
37
|
+
return {
|
|
38
|
+
isValid: false,
|
|
39
|
+
reason: `不允许在 ${branch} 分支上直接提交`,
|
|
40
|
+
action: 'create-branch',
|
|
41
|
+
warning: false,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const isTemp = TEMP_PATTERN.test(branch);
|
|
46
|
+
const action = isTemp ? 'suggest-rename' : 'continue';
|
|
47
|
+
const matchedTask = openspecTasks.find(t =>
|
|
48
|
+
branch === t.name || branch.includes(t.name)
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
isValid: true,
|
|
53
|
+
warning: isTemp,
|
|
54
|
+
action,
|
|
55
|
+
...(isTemp ? { reason: '分支名像临时分支,建议改名' } : {}),
|
|
56
|
+
...(matchedTask ? { matchedTask: matchedTask.name } : {}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── Shell helpers ─────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
function exec(command) {
|
|
63
|
+
try {
|
|
64
|
+
return execSync(command, { encoding: 'utf8', timeout: 5000 }).trim();
|
|
65
|
+
} catch {
|
|
66
|
+
return '';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function findOpenspecTasks() {
|
|
71
|
+
const root = exec(
|
|
72
|
+
'git rev-parse --show-toplevel 2>/dev/null || echo ""'
|
|
73
|
+
);
|
|
74
|
+
if (!root) return [];
|
|
75
|
+
const changesDir = join(root, 'openspec', 'changes');
|
|
76
|
+
if (!existsSync(changesDir)) return [];
|
|
77
|
+
|
|
78
|
+
const entries = readdirSync(changesDir, { withFileTypes: true });
|
|
79
|
+
return entries
|
|
80
|
+
.filter(e => e.isDirectory() && e.name !== 'archive')
|
|
81
|
+
.map(e => ({
|
|
82
|
+
name: e.name,
|
|
83
|
+
hasProposal: existsSync(join(changesDir, e.name, 'proposal.md')),
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── CLI entry ─────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
if (import.meta.filename === process.argv[1]) {
|
|
90
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
91
|
+
console.log(`
|
|
92
|
+
branch-check.mjs — 检查分支合法性
|
|
93
|
+
|
|
94
|
+
用法:
|
|
95
|
+
node branch-check.mjs 自动检测
|
|
96
|
+
node branch-check.mjs --branch <name> 指定分支
|
|
97
|
+
node branch-check.mjs --help 显示帮助
|
|
98
|
+
`);
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const idx = process.argv.indexOf('--branch');
|
|
103
|
+
const branch = idx >= 0
|
|
104
|
+
? process.argv[idx + 1]
|
|
105
|
+
: exec('git branch --show-current');
|
|
106
|
+
const tasks = findOpenspecTasks();
|
|
107
|
+
const result = checkBranch(branch, tasks);
|
|
108
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
109
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* check-openspec-status.mjs
|
|
5
|
+
* 扫描 OpenSpec change 的状态。
|
|
6
|
+
*
|
|
7
|
+
* 用法:
|
|
8
|
+
* node check-openspec-status.mjs
|
|
9
|
+
* node check-openspec-status.mjs --branch add-auth
|
|
10
|
+
* node check-openspec-status.mjs --help
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
|
|
16
|
+
const TASKS_CHECKBOX_RE = /^- \[([ xX])\]/gm;
|
|
17
|
+
|
|
18
|
+
// ── Pure functions ────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export function isArchived(changePath) {
|
|
21
|
+
return changePath.includes('/archive/');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function parseTasksStatus(content) {
|
|
25
|
+
const matches = [...content.matchAll(TASKS_CHECKBOX_RE)];
|
|
26
|
+
const total = matches.length;
|
|
27
|
+
const complete = matches.filter(m => m[1] !== ' ').length;
|
|
28
|
+
return {
|
|
29
|
+
total,
|
|
30
|
+
complete,
|
|
31
|
+
allDone: total === 0 ? true : total === complete,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function scanChanges(changesDir) {
|
|
36
|
+
if (!existsSync(changesDir)) return [];
|
|
37
|
+
const entries = readdirSync(changesDir, { withFileTypes: true });
|
|
38
|
+
return entries
|
|
39
|
+
.filter(e => e.isDirectory() && e.name !== 'archive')
|
|
40
|
+
.map(e => {
|
|
41
|
+
const dirPath = join(changesDir, e.name);
|
|
42
|
+
const tasksPath = join(dirPath, 'tasks.md');
|
|
43
|
+
const hasTasksMd = existsSync(tasksPath);
|
|
44
|
+
let tasksStatus = null;
|
|
45
|
+
if (hasTasksMd) {
|
|
46
|
+
tasksStatus = parseTasksStatus(
|
|
47
|
+
readFileSync(tasksPath, 'utf8')
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
name: e.name,
|
|
52
|
+
archived: false,
|
|
53
|
+
hasTasksMd,
|
|
54
|
+
tasksAllDone: tasksStatus ? tasksStatus.allDone : null,
|
|
55
|
+
tasksTotal: tasksStatus ? tasksStatus.total : 0,
|
|
56
|
+
tasksComplete: tasksStatus ? tasksStatus.complete : 0,
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── CLI entry ─────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
if (import.meta.filename === process.argv[1]) {
|
|
64
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
65
|
+
console.log(`
|
|
66
|
+
check-openspec-status.mjs — 扫描 OpenSpec change 状态
|
|
67
|
+
|
|
68
|
+
用法:
|
|
69
|
+
node check-openspec-status.mjs
|
|
70
|
+
node check-openspec-status.mjs --branch <name>
|
|
71
|
+
node check-openspec-status.mjs --help
|
|
72
|
+
`);
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const root = process.env.INIT_CWD || process.cwd();
|
|
77
|
+
const changesDir = join(root, 'openspec', 'changes');
|
|
78
|
+
const results = scanChanges(changesDir);
|
|
79
|
+
|
|
80
|
+
const branchIdx = process.argv.indexOf('--branch');
|
|
81
|
+
if (branchIdx >= 0) {
|
|
82
|
+
const branch = process.argv[branchIdx + 1];
|
|
83
|
+
const matched = results.filter(r =>
|
|
84
|
+
r.name === branch || branch.includes(r.name)
|
|
85
|
+
);
|
|
86
|
+
process.stdout.write(JSON.stringify(
|
|
87
|
+
matched.length > 0 ? matched : { noMatch: true, branch }, null, 2
|
|
88
|
+
) + '\n');
|
|
89
|
+
} else {
|
|
90
|
+
process.stdout.write(JSON.stringify(results, null, 2) + '\n');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* collect-git-context.mjs
|
|
5
|
+
* 收集 git 上下文,输出结构化 JSON。
|
|
6
|
+
*
|
|
7
|
+
* 用法:
|
|
8
|
+
* node collect-git-context.mjs # 输出 JSON
|
|
9
|
+
* node collect-git-context.mjs --help # 查看帮助
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { execSync } from 'node:child_process';
|
|
13
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
|
|
16
|
+
// ── Pure parse functions (testable) ──────────────────────────────────
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 解析 `git diff --stat` 输出
|
|
20
|
+
* @param {string} output
|
|
21
|
+
* @returns {{ files: number, insertions: number, deletions: number, changedFiles: string[] }}
|
|
22
|
+
*/
|
|
23
|
+
export function parseDiffStat(output) {
|
|
24
|
+
const lines = output.trim().split('\n').filter(Boolean);
|
|
25
|
+
const changedFiles = [];
|
|
26
|
+
let files = 0, insertions = 0, deletions = 0;
|
|
27
|
+
|
|
28
|
+
for (const line of lines) {
|
|
29
|
+
// 匹配汇总行: "2 files changed, 13 insertions(+), 2 deletions(-)"
|
|
30
|
+
const summaryMatch = line.match(/(\d+)\s+files?\s+changed/);
|
|
31
|
+
if (summaryMatch) {
|
|
32
|
+
files = parseInt(summaryMatch[1], 10);
|
|
33
|
+
const insMatch = line.match(/(\d+)\s+insertions?\(\+\)/);
|
|
34
|
+
if (insMatch) insertions = parseInt(insMatch[1], 10);
|
|
35
|
+
const delMatch = line.match(/(\d+)\s+deletions?\(-\)/);
|
|
36
|
+
if (delMatch) deletions = parseInt(delMatch[1], 10);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
// 匹配文件行: "src/main.ts | 10 ++++++++--"
|
|
40
|
+
const fileMatch = line.match(/^\s*(.+?)\s+\|/);
|
|
41
|
+
if (fileMatch) {
|
|
42
|
+
changedFiles.push(fileMatch[1].trim());
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { files, insertions, deletions, changedFiles };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 解析 `git log --oneline` 输出
|
|
51
|
+
* @param {string} output
|
|
52
|
+
* @returns {Array<{ hash: string, message: string }>}
|
|
53
|
+
*/
|
|
54
|
+
export function parseCommitLog(output) {
|
|
55
|
+
const lines = output.trim().split('\n').filter(Boolean);
|
|
56
|
+
return lines.map(line => {
|
|
57
|
+
const match = line.match(/^(\S+)\s+(.*)/);
|
|
58
|
+
return match ? { hash: match[1], message: match[2] } : null;
|
|
59
|
+
}).filter(Boolean);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 从分支名或 commit 信息中提取 JIRA 引用编号
|
|
64
|
+
* @param {string} branchName
|
|
65
|
+
* @param {Array<{hash:string,message:string}>} [commitLog]
|
|
66
|
+
* @returns {string|null}
|
|
67
|
+
*/
|
|
68
|
+
export function extractJiraRef(branchName, commitLog) {
|
|
69
|
+
const JIRA_RE = /[A-Z]{2,}-\d+/;
|
|
70
|
+
const branchMatch = branchName.match(JIRA_RE);
|
|
71
|
+
if (branchMatch) return branchMatch[0];
|
|
72
|
+
if (commitLog) {
|
|
73
|
+
for (const commit of commitLog) {
|
|
74
|
+
const match = commit.message.match(JIRA_RE);
|
|
75
|
+
if (match) return match[0];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Shell exec helpers ──────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
function exec(command) {
|
|
84
|
+
try {
|
|
85
|
+
return execSync(command, { encoding: 'utf8', timeout: 10000 }).trim();
|
|
86
|
+
} catch {
|
|
87
|
+
return '';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Context collection ──────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 收集完整的 git 上下文
|
|
95
|
+
* @returns {{ branch: string, forkPoint: string|null, diffStat: object|null, commitLog: Array, hasUncommitted: boolean, openspecChanges: Array|null }}
|
|
96
|
+
*/
|
|
97
|
+
export function collectContext() {
|
|
98
|
+
const branch = exec('git branch --show-current');
|
|
99
|
+
let forkPoint;
|
|
100
|
+
let error;
|
|
101
|
+
try {
|
|
102
|
+
forkPoint = execSync('git merge-base main HEAD', { encoding: 'utf8' }).trim();
|
|
103
|
+
} catch {
|
|
104
|
+
error = '无法找到基准点';
|
|
105
|
+
}
|
|
106
|
+
const diffStat = exec(`git diff "${forkPoint || 'main'}..HEAD" --stat`);
|
|
107
|
+
const commitLogRaw = forkPoint
|
|
108
|
+
? exec(`git log "${forkPoint}"..HEAD --oneline`)
|
|
109
|
+
: '';
|
|
110
|
+
const statusShort = exec('git status --short');
|
|
111
|
+
const hasUncommitted = statusShort.length > 0;
|
|
112
|
+
|
|
113
|
+
// OpenSpec change 扫描
|
|
114
|
+
let openspecChanges = null;
|
|
115
|
+
const root = exec('git rev-parse --show-toplevel 2>/dev/null || echo ""');
|
|
116
|
+
if (root) {
|
|
117
|
+
const changesDir = join(root, 'openspec', 'changes');
|
|
118
|
+
if (existsSync(changesDir)) {
|
|
119
|
+
try {
|
|
120
|
+
const entries = readdirSync(changesDir, { withFileTypes: true });
|
|
121
|
+
const changeDirs = entries
|
|
122
|
+
.filter(e => e.isDirectory() && e.name !== 'archive')
|
|
123
|
+
.map(e => {
|
|
124
|
+
const proposalPath = join(changesDir, e.name, 'proposal.md');
|
|
125
|
+
const hasProposal = existsSync(proposalPath);
|
|
126
|
+
let title = e.name;
|
|
127
|
+
if (hasProposal) {
|
|
128
|
+
const firstLine = readFileSync(proposalPath, 'utf8').split('\n')[0] || '';
|
|
129
|
+
title = firstLine.replace(/^#\s*/, '').trim() || e.name;
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
name: e.name,
|
|
133
|
+
hasProposal,
|
|
134
|
+
matched: branch === e.name,
|
|
135
|
+
title,
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
if (changeDirs.length > 0) openspecChanges = changeDirs;
|
|
139
|
+
} catch {
|
|
140
|
+
// ignore
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const parsedCommitLog = commitLogRaw ? parseCommitLog(commitLogRaw) : [];
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
branch,
|
|
149
|
+
forkPoint: forkPoint || null,
|
|
150
|
+
diffStat: diffStat ? parseDiffStat(diffStat) : null,
|
|
151
|
+
commitLog: parsedCommitLog,
|
|
152
|
+
hasUncommitted,
|
|
153
|
+
openspecChanges,
|
|
154
|
+
jiraRef: extractJiraRef(branch, parsedCommitLog),
|
|
155
|
+
...(error ? { error } : {}),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── CLI entry ───────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
function printHelp() {
|
|
162
|
+
console.log(`
|
|
163
|
+
collect-git-context.mjs — 收集 git 上下文
|
|
164
|
+
|
|
165
|
+
用法:
|
|
166
|
+
node collect-git-context.mjs 输出 JSON 格式的 git 上下文
|
|
167
|
+
node collect-git-context.mjs --help 显示本帮助
|
|
168
|
+
|
|
169
|
+
输出字段:
|
|
170
|
+
branch 当前分支名
|
|
171
|
+
forkPoint main 的 merge-base
|
|
172
|
+
diffStat { files, insertions, deletions, changedFiles }
|
|
173
|
+
commitLog [{ hash, message }]
|
|
174
|
+
hasUncommitted 是否有未提交变更
|
|
175
|
+
openspecChanges [{ name, hasProposal, matched, title }] 或 null
|
|
176
|
+
`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (import.meta.filename === process.argv[1]) {
|
|
180
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
181
|
+
printHelp();
|
|
182
|
+
process.exit(0);
|
|
183
|
+
}
|
|
184
|
+
const context = collectContext();
|
|
185
|
+
process.stdout.write(JSON.stringify(context, null, 2) + '\n');
|
|
186
|
+
}
|