@deepstorm/cli 0.9.2 → 0.10.0
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 +508 -24
- package/dist/registry.json +168 -0
- 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/variants/nodejs/steps.md +53 -0
- 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-style-backend/fragments/nodejs/eslint-config.json +30 -0
- package/dist/skills/reef-style-backend/fragments/nodejs/nestjs-structure.md +58 -0
- package/dist/skills/reef-style-backend/fragments/nodejs/prettier-config.json +19 -0
- package/dist/skills/reef-style-backend/variants/nodejs/examples/module-example.md +166 -0
- package/dist/skills/reef-style-backend/variants/nodejs/examples/prisma-example.md +115 -0
- package/dist/skills/reef-style-backend/variants/nodejs/quick-reference.md +116 -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 +11 -35
- package/dist/skills/sweep-run/scripts/env-manager.mjs +71 -2
- package/dist/skills/sweep-run/scripts/generate-report.mjs +116 -0
- package/package.json +2 -2
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# run-tests.sh
|
|
3
|
+
# 根据项目类型自动检测并运行测试。
|
|
4
|
+
#
|
|
5
|
+
# 用法:
|
|
6
|
+
# bash run-tests.sh # 自动检测并运行
|
|
7
|
+
# bash run-tests.sh --json # JSON 格式输出结果
|
|
8
|
+
# bash run-tests.sh --help # 显示帮助
|
|
9
|
+
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
|
|
12
|
+
MODE="text"
|
|
13
|
+
for arg in "$@"; do
|
|
14
|
+
case "$arg" in
|
|
15
|
+
--json) MODE="json" ;;
|
|
16
|
+
--help|-h)
|
|
17
|
+
echo "用法: bash run-tests.sh [--json]"
|
|
18
|
+
echo "自动检测项目类型(Java/Python/Frontend)并运行测试"
|
|
19
|
+
exit 0
|
|
20
|
+
;;
|
|
21
|
+
esac
|
|
22
|
+
done
|
|
23
|
+
|
|
24
|
+
FRONTEND_FAILED=false
|
|
25
|
+
BACKEND_FAILED=false
|
|
26
|
+
HAS_FRONTEND=false
|
|
27
|
+
HAS_BACKEND=false
|
|
28
|
+
|
|
29
|
+
# 检测后端
|
|
30
|
+
if [ -f "build.gradle" ] || [ -f "build.gradle.kts" ]; then
|
|
31
|
+
HAS_BACKEND=true
|
|
32
|
+
echo "🔍 检测到 Gradle 项目,运行 ./gradlew test..."
|
|
33
|
+
if ./gradlew test 2>&1; then
|
|
34
|
+
echo "✅ Java 测试通过"
|
|
35
|
+
else
|
|
36
|
+
BACKEND_FAILED=true
|
|
37
|
+
echo "❌ Java 测试失败"
|
|
38
|
+
fi
|
|
39
|
+
elif [ -f "pom.xml" ]; then
|
|
40
|
+
HAS_BACKEND=true
|
|
41
|
+
echo "🔍 检测到 Maven 项目,运行 mvn test..."
|
|
42
|
+
if mvn test 2>&1; then
|
|
43
|
+
echo "✅ Java 测试通过"
|
|
44
|
+
else
|
|
45
|
+
BACKEND_FAILED=true
|
|
46
|
+
echo "❌ Java 测试失败"
|
|
47
|
+
fi
|
|
48
|
+
elif [ -f "pyproject.toml" ] || [ -f "pytest.ini" ]; then
|
|
49
|
+
HAS_BACKEND=true
|
|
50
|
+
echo "🔍 检测到 Python 项目,运行 pytest..."
|
|
51
|
+
if python -m pytest 2>&1; then
|
|
52
|
+
echo "✅ Python 测试通过"
|
|
53
|
+
else
|
|
54
|
+
BACKEND_FAILED=true
|
|
55
|
+
echo "❌ Python 测试失败"
|
|
56
|
+
fi
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# 检测前端
|
|
60
|
+
if [ -f "package.json" ]; then
|
|
61
|
+
HAS_FRONTEND=true
|
|
62
|
+
echo "🔍 检测到前端项目,运行 pnpm test..."
|
|
63
|
+
if pnpm test -- --run 2>&1; then
|
|
64
|
+
echo "✅ 前端测试通过"
|
|
65
|
+
else
|
|
66
|
+
FRONTEND_FAILED=true
|
|
67
|
+
echo "❌ 前端测试失败"
|
|
68
|
+
fi
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
if [ "$HAS_BACKEND" = false ] && [ "$HAS_FRONTEND" = false ]; then
|
|
72
|
+
echo "⚠️ 未检测到可运行的测试项目"
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# JSON 输出
|
|
76
|
+
if [ "$MODE" = "json" ]; then
|
|
77
|
+
cat <<EOF
|
|
78
|
+
{
|
|
79
|
+
"hasFrontend": $HAS_FRONTEND,
|
|
80
|
+
"hasBackend": $HAS_BACKEND,
|
|
81
|
+
"frontendPassed": $( [ "$FRONTEND_FAILED" = false ] && echo "$HAS_FRONTEND" || echo false ),
|
|
82
|
+
"backendPassed": $( [ "$BACKEND_FAILED" = false ] && echo "$HAS_BACKEND" || echo false ),
|
|
83
|
+
"allPassed": $( [ "$FRONTEND_FAILED" = false ] && [ "$BACKEND_FAILED" = false ] && echo true || echo false )
|
|
84
|
+
}
|
|
85
|
+
EOF
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
# exit code
|
|
89
|
+
if [ "$FRONTEND_FAILED" = true ] || [ "$BACKEND_FAILED" = true ]; then
|
|
90
|
+
exit 1
|
|
91
|
+
fi
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# stash-and-switch.sh
|
|
3
|
+
# git stash → checkout main → pull → checkout -b → stash pop
|
|
4
|
+
#
|
|
5
|
+
# 用法:
|
|
6
|
+
# bash stash-and-switch.sh <new-branch-name>
|
|
7
|
+
#
|
|
8
|
+
# 说明:
|
|
9
|
+
# 自动处理 stash 保存和恢复当前工作区变更。
|
|
10
|
+
# 新分支基于 main 的最新提交。
|
|
11
|
+
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
|
|
14
|
+
if [ $# -lt 1 ]; then
|
|
15
|
+
echo "用法: bash $0 <new-branch-name>"
|
|
16
|
+
exit 1
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
NEW_BRANCH="$1"
|
|
20
|
+
STASHED=false
|
|
21
|
+
|
|
22
|
+
# stash 当前变更(如有)
|
|
23
|
+
if [ -n "$(git status --porcelain)" ]; then
|
|
24
|
+
echo "📦 暂存当前未提交变更..."
|
|
25
|
+
git stash push -m "stash-and-switch-auto-stash"
|
|
26
|
+
STASHED=true
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# 切换到 main 并更新
|
|
30
|
+
echo "🔀 切换到 main..."
|
|
31
|
+
git checkout main
|
|
32
|
+
git pull origin main 2>/dev/null || echo " ⚠️ pull 失败(可能无远程)"
|
|
33
|
+
|
|
34
|
+
# 创建新分支
|
|
35
|
+
echo "🌿 创建新分支: $NEW_BRANCH"
|
|
36
|
+
git checkout -b "$NEW_BRANCH"
|
|
37
|
+
|
|
38
|
+
# 恢复暂存的变更
|
|
39
|
+
if [ "$STASHED" = true ]; then
|
|
40
|
+
echo "📦 恢复暂存的变更..."
|
|
41
|
+
git stash pop
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
echo "✅ 已切换到新分支: $NEW_BRANCH"
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Node.js / NestJS 后端编码步骤
|
|
2
|
+
|
|
3
|
+
按以下顺序逐块编写,依赖关系由前向后:
|
|
4
|
+
|
|
5
|
+
1. **Prisma Schema** — 定义数据模型(`prisma/schema.prisma`),**先于所有代码**,因为它决定类型结构
|
|
6
|
+
2. **DTO** — 数据传输对象(请求/响应),使用 `class-validator` + `@nestjs/swagger` 装饰器
|
|
7
|
+
3. **Entity** — Prisma 生成的 TypeScript 类型,必要时封装为 Entity 类(可选)
|
|
8
|
+
4. **PrismaService** — 数据库访问服务(`PrismaModule` + `PrismaService`),如果尚未创建
|
|
9
|
+
5. **Service** — 业务逻辑层,依赖 `PrismaService`,事务处理
|
|
10
|
+
6. **Controller** — REST API,请求/响应,参数校验
|
|
11
|
+
7. **Module** — NestJS 模块,将 Controller/Service 注册到 DI 容器
|
|
12
|
+
|
|
13
|
+
每完成一块对照 `reef:reef-style-backend` 中对应章节检查。
|
|
14
|
+
|
|
15
|
+
## 注释要求
|
|
16
|
+
|
|
17
|
+
每类文件必须包含以下注释(缺少则视为未完成):
|
|
18
|
+
|
|
19
|
+
| 文件类型 | 注释要求 |
|
|
20
|
+
|---------|---------|
|
|
21
|
+
| **Prisma Schema** | 每个 model 加 `/// 注释` 描述实体用途;每个字段加 `/// 注释` |
|
|
22
|
+
| **DTO** | 类 JSDoc `/** 用途说明 */`;字段通过 `@ApiProperty({ description: '...' })` 提供文档 |
|
|
23
|
+
| **Service** | 每个 public 方法加 JSDoc `/** 功能、@param、@returns */`;复杂逻辑内联注释说明 |
|
|
24
|
+
| **Controller** | 每个端点加 `@ApiOperation({ summary: '...', description: '...' })` |
|
|
25
|
+
| **Module** | 类 JSDoc `/** 模块职责 */` |
|
|
26
|
+
|
|
27
|
+
## 安全红线
|
|
28
|
+
|
|
29
|
+
- 所有环境变量通过 `ConfigService` 读取,禁止 `process.env` 直读
|
|
30
|
+
- 敏感字段(密码、Token)在 API 响应前使用 `select` 或 `@Exclude()` 排除
|
|
31
|
+
- 禁止在 TypeScript 代码中硬编码 API Key / Token
|
|
32
|
+
- Prisma 条件查询使用参数化查询(Prisma 默认安全),禁止手动拼接 `where` 条件
|
|
33
|
+
- API 端点需添加 `@ApiBearerAuth()` 和 `@UseGuards(AuthGuard())`
|
|
34
|
+
|
|
35
|
+
## 构建命令
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# 依赖安装
|
|
39
|
+
pnpm install
|
|
40
|
+
|
|
41
|
+
# Prisma 客户端生成
|
|
42
|
+
npx prisma generate
|
|
43
|
+
|
|
44
|
+
# 代码检查
|
|
45
|
+
npx eslint src/ --ext .ts
|
|
46
|
+
npx prettier --check src/
|
|
47
|
+
|
|
48
|
+
# 测试
|
|
49
|
+
npx jest
|
|
50
|
+
|
|
51
|
+
# 构建
|
|
52
|
+
npx nest build
|
|
53
|
+
```
|
|
@@ -10,25 +10,17 @@ deepstorm:
|
|
|
10
10
|
|
|
11
11
|
## 上下文约定
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
使用 `find-change-dir.mjs` 自动发现当前 change:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
fi
|
|
25
|
-
|
|
26
|
-
# 如仍无结果,列出所有 change 让用户选择
|
|
27
|
-
if [ ! -d "$CHANGE_DIR" ]; then
|
|
28
|
-
echo "未找到 openspec change,可用的目录:"
|
|
29
|
-
ls openspec/changes/
|
|
30
|
-
echo "请指定 change 名"
|
|
31
|
-
fi
|
|
16
|
+
node packages/reef/skills/reef-harden/scripts/find-change-dir.mjs
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
输出 JSON。`noMatch: true` 时需让用户选择;有 `suggestion` 时使用推荐 change。
|
|
20
|
+
|
|
21
|
+
如需并列出 SDD 文档路径:
|
|
22
|
+
```bash
|
|
23
|
+
node packages/reef/skills/reef-harden/scripts/find-change-dir.mjs --files
|
|
32
24
|
```
|
|
33
25
|
|
|
34
26
|
## 快速开始
|
|
@@ -45,8 +37,8 @@ fi
|
|
|
45
37
|
### Step 1: 加载文档
|
|
46
38
|
|
|
47
39
|
```bash
|
|
48
|
-
|
|
49
|
-
|
|
40
|
+
CHANGE_JSON=$(node packages/reef/skills/reef-harden/scripts/find-change-dir.mjs)
|
|
41
|
+
# CHANGE_JSON 中包含 path 和 files[],files 为所有 SDD 文档路径
|
|
50
42
|
```
|
|
51
43
|
|
|
52
44
|
读取所有这些文档。目录结构通常为:
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* find-change-dir.mjs
|
|
5
|
+
* 自动发现当前 OpenSpec change 目录。
|
|
6
|
+
*
|
|
7
|
+
* 用法:
|
|
8
|
+
* node find-change-dir.mjs # 根据分支自动查找
|
|
9
|
+
* node find-change-dir.mjs --branch <name> # 指定分支
|
|
10
|
+
* node find-change-dir.mjs --files # 同时列出 SDD 文档
|
|
11
|
+
* node find-change-dir.mjs --dir <path> # 指定扫描目录
|
|
12
|
+
* node find-change-dir.mjs --help
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
16
|
+
import { execSync } from 'node:child_process';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
|
|
19
|
+
const CHANGES_GLOB = 'openspec/changes';
|
|
20
|
+
|
|
21
|
+
// ── Pure functions ────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
function isArchivePath(changePath) {
|
|
24
|
+
return changePath.includes('/archive/');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 在 changes 列表中按 name 精确查找
|
|
29
|
+
* @param {Array} changes
|
|
30
|
+
* @param {string} name
|
|
31
|
+
* @returns {object|null}
|
|
32
|
+
*/
|
|
33
|
+
export function findChangeByName(changes, name) {
|
|
34
|
+
return changes.find(c =>
|
|
35
|
+
c.name === name && !isArchivePath(c.path)
|
|
36
|
+
) || null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 返回最近修改的非归档 change
|
|
41
|
+
* @param {Array} changes
|
|
42
|
+
* @returns {object|null}
|
|
43
|
+
*/
|
|
44
|
+
export function findMostRecent(changes) {
|
|
45
|
+
const active = changes.filter(c => !c.archived);
|
|
46
|
+
if (active.length === 0) return null;
|
|
47
|
+
return active.sort((a, b) => b.mtimeMs - a.mtimeMs)[0];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 列出 changes 目录下的所有 change
|
|
52
|
+
* @param {string} changesDir
|
|
53
|
+
* @returns {Array<{name:string, path:string, archived:boolean, mtimeMs:number, files:string[]}>}
|
|
54
|
+
*/
|
|
55
|
+
export function listChanges(changesDir) {
|
|
56
|
+
if (!existsSync(changesDir)) return [];
|
|
57
|
+
const entries = readdirSync(changesDir, { withFileTypes: true });
|
|
58
|
+
const results = [];
|
|
59
|
+
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
if (!entry.isDirectory()) continue;
|
|
62
|
+
if (entry.name === 'archive') continue;
|
|
63
|
+
|
|
64
|
+
const dirPath = join(changesDir, entry.name);
|
|
65
|
+
const stats = statSync(dirPath);
|
|
66
|
+
|
|
67
|
+
// 收集 .md 文件
|
|
68
|
+
const files = collectMdFiles(dirPath);
|
|
69
|
+
|
|
70
|
+
results.push({
|
|
71
|
+
name: entry.name,
|
|
72
|
+
path: dirPath,
|
|
73
|
+
archived: false,
|
|
74
|
+
mtimeMs: stats.mtimeMs,
|
|
75
|
+
files,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return results;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function collectMdFiles(dir) {
|
|
83
|
+
try {
|
|
84
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
85
|
+
const files = [];
|
|
86
|
+
for (const e of entries) {
|
|
87
|
+
if (e.isFile() && e.name.endsWith('.md')) {
|
|
88
|
+
files.push(join(dir, e.name));
|
|
89
|
+
}
|
|
90
|
+
if (e.isDirectory()) {
|
|
91
|
+
const sub = collectMdFiles(join(dir, e.name));
|
|
92
|
+
files.push(...sub);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return files;
|
|
96
|
+
} catch {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── CLI entry ─────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
if (import.meta.filename === process.argv[1]) {
|
|
104
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
105
|
+
console.log(`
|
|
106
|
+
find-change-dir.mjs — 发现 OpenSpec change 目录
|
|
107
|
+
|
|
108
|
+
用法:
|
|
109
|
+
node find-change-dir.mjs 根据分支自动查找
|
|
110
|
+
node find-change-dir.mjs --branch <name> 指定分支
|
|
111
|
+
node find-change-dir.mjs --files 同时列出 SDD 文档
|
|
112
|
+
node find-change-dir.mjs --dir <path> 指定扫描目录
|
|
113
|
+
node find-change-dir.mjs --help
|
|
114
|
+
`);
|
|
115
|
+
process.exit(0);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const root = process.argv.indexOf('--dir') >= 0
|
|
119
|
+
? process.argv[process.argv.indexOf('--dir') + 1]
|
|
120
|
+
: process.env.INIT_CWD || process.cwd();
|
|
121
|
+
const changesDir = join(root, CHANGES_GLOB);
|
|
122
|
+
const allChanges = listChanges(changesDir);
|
|
123
|
+
|
|
124
|
+
const branchIdx = process.argv.indexOf('--branch');
|
|
125
|
+
let result;
|
|
126
|
+
|
|
127
|
+
if (branchIdx >= 0) {
|
|
128
|
+
const branch = process.argv[branchIdx + 1];
|
|
129
|
+
result = findChangeByName(allChanges, branch);
|
|
130
|
+
if (!result) {
|
|
131
|
+
result = {
|
|
132
|
+
noMatch: true,
|
|
133
|
+
branch,
|
|
134
|
+
suggestion: findMostRecent(allChanges),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
const branch = execGitBranch();
|
|
139
|
+
result = branch
|
|
140
|
+
? (findChangeByName(allChanges, branch) ||
|
|
141
|
+
{ noMatch: true, branch,
|
|
142
|
+
suggestion: findMostRecent(allChanges) })
|
|
143
|
+
: { noMatch: true, allChanges };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function execGitBranch() {
|
|
150
|
+
try {
|
|
151
|
+
return execSync('git branch --show-current', {
|
|
152
|
+
encoding: 'utf8', timeout: 5000,
|
|
153
|
+
}).trim() || null;
|
|
154
|
+
} catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -30,15 +30,10 @@ CHANGE_DIR="openspec/changes/$BRANCH"
|
|
|
30
30
|
先检查未提交变更。如有则提示「请先 commit 再创建 PR」,中止流程。
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
|
-
|
|
34
|
-
FORK_POINT=$(git merge-base main HEAD 2>/dev/null)
|
|
35
|
-
echo "Branch: $BRANCH"; git status -sb
|
|
36
|
-
echo "Commits:"; git log "$FORK_POINT"..HEAD --oneline
|
|
37
|
-
echo "Changes:"; git diff "$FORK_POINT"..HEAD --stat
|
|
38
|
-
ls -d openspec/changes/$BRANCH/*.md 2>/dev/null
|
|
33
|
+
node packages/reef/skills/reef-pr/scripts/create-pr.mjs --collect
|
|
39
34
|
```
|
|
40
35
|
|
|
41
|
-
|
|
36
|
+
输出 JSON 包含 `branch`、`hasUncommitted`、`diffStat`、`commitLog`、`proposalTitle`。如有 OpenSpec change,`proposalTitle` 会包含 proposal 标题。
|
|
42
37
|
|
|
43
38
|
### 2. 构建 PR 信息
|
|
44
39
|
|
|
@@ -71,20 +66,11 @@ OpenSpec: openspec/changes/{branch-name}/
|
|
|
71
66
|
|
|
72
67
|
### 4. 推送并创建 PR
|
|
73
68
|
|
|
74
|
-
先检查是否已有打开的 PR
|
|
69
|
+
先检查是否已有打开的 PR,用 `create-pr.mjs` 统一处理:
|
|
75
70
|
|
|
76
71
|
```bash
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
如有则询问「已有 PR,是否更新描述?」,用户确认后用 `gh pr update` 更新;否则正常创建:
|
|
81
|
-
|
|
82
|
-
```bash
|
|
83
|
-
# 首次推送当前分支
|
|
84
|
-
git push -u origin $(git branch --show-current)
|
|
85
|
-
|
|
86
|
-
# 创建 PR(如有 reviewer/label/draft 则追加对应参数)
|
|
87
|
-
gh pr create \
|
|
72
|
+
# --title 和 --body 从步骤 2 构建的 PR 信息中取
|
|
73
|
+
node packages/reef/skills/reef-pr/scripts/create-pr.mjs --create \
|
|
88
74
|
--title "<标题>" \
|
|
89
75
|
--body "<正文>" \
|
|
90
76
|
[--reviewer "<用户>"] \
|
|
@@ -92,6 +78,8 @@ gh pr create \
|
|
|
92
78
|
[--draft]
|
|
93
79
|
```
|
|
94
80
|
|
|
81
|
+
如有已有 PR,脚本输出 `{"exists":true}` 并在 stderr 输出提示,此时可用 `gh pr update` 更新。
|
|
82
|
+
|
|
95
83
|
### 5. 输出结果
|
|
96
84
|
|
|
97
85
|
返回 PR 链接。
|