@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.
@@ -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"
@@ -30,9 +30,15 @@ git diff "$FORK_POINT"..HEAD --diff-filter=M --name-only
30
30
 
31
31
  **规则**:不凭空写新文件。先读一个真实存在的同类文件,理解模式后再动手。写新模块时参考已有模块的完整实现。
32
32
 
33
- ### 2. 查阅规范
33
+ ### 2. 🔴 加载编码规范(硬性门禁)
34
34
 
35
- 加载 `reef:reef-style-backend` 技能,阅读 `quick-reference.md` 和必要示例。
35
+ > **必须先通过 Skill tool 加载 `reef:reef-style-backend`,阅读 `quick-reference.md` 了解核心编码规范,并根据当前变更类型阅读对应的维度规范文件(如 `spring-boot.md`、`hibernate.md`、`fastapi-quick-reference.md`、`pytest-testing.md` 等)。完成后方可进入后续代码编写步骤。未加载 code-style 技能不得编写代码,否则视为违反工作流纪律。**
36
+
37
+ 加载完成后,AI MUST 输出以下声明:
38
+
39
+ ```
40
+ ✅ [CODE-STYLE] 已加载后端编码规范(quick-reference + 必要维度规范),所有新增/修改代码将遵循项目编码规范。
41
+ ```
36
42
 
37
43
  涉及库/框架 API 用法时,使用 context7 获取最新文档:`resolve-library-id` → `query-docs`。
38
44
 
@@ -83,6 +89,6 @@ ruff check . && mypy . && python -m pytest
83
89
  ```
84
90
  {{/if}}
85
91
 
86
- ### {{#if reef.backend.java.dbMigration.styleRef}}6{{else}}{{#if reef.backend.python.dbMigration.styleRef}}6{{else}}5{{/if}}{{/if}}. 提交前自检
92
+ ### {{#if reef.backend.java.dbMigration.styleRef}}6{{else}}{{#if reef.backend.python.dbMigration.styleRef}}6{{else}}5{{/if}}{{/if}}. 🔴 提交前自检(后置检查)
87
93
 
88
- 加载 `reef:reef-style-backend` 技能,逐项检查所有规范要求。**未通过不得提交。**
94
+ 重新加载 `reef:reef-style-backend` 技能,逐项对照所有规范要求检查本变更中的每处代码修改。**检查未通过不得提交。**
@@ -31,9 +31,15 @@ git diff "$FORK_POINT"..HEAD --diff-filter=M --name-only
31
31
 
32
32
  **规则**:不凭空写新文件。先读一个真实存在的同类文件,理解模式后再动手。
33
33
 
34
- ### 2. 查阅规范
34
+ ### 2. 🔴 加载编码规范(硬性门禁)
35
35
 
36
- 加载 `reef:reef-style-frontend` 技能,阅读 `quick-reference.md` 和必要示例。
36
+ > **必须先通过 Skill tool 加载 `reef:reef-style-frontend`,阅读 `quick-reference.md` 了解核心编码规范,并根据当前变更类型阅读对应的维度规范文件(框架规范、UI 组件库规范、CSS 方案、TypeScript 配置、测试框架等)。完成后方可进入后续代码编写步骤。未加载 code-style 技能不得编写代码,否则视为违反工作流纪律。**
37
+
38
+ 加载完成后,AI MUST 输出以下声明:
39
+
40
+ ```
41
+ ✅ [CODE-STYLE] 已加载前端编码规范(quick-reference + 必要维度规范),所有新增/修改代码将遵循项目编码规范。
42
+ ```
37
43
 
38
44
  涉及库/框架 API 用法时,使用 context7 获取最新文档:`resolve-library-id` → `query-docs`。
39
45
 
@@ -79,6 +85,6 @@ git diff "$FORK_POINT"..HEAD --diff-filter=M --name-only
79
85
  {{reef.frontend.framework.formatCmd}} lint && {{reef.frontend.framework.formatCmd}} typecheck && {{reef.frontend.framework.formatCmd}} test
80
86
  ```
81
87
 
82
- ### {{#if reef.frontend.framework.sourcePath}}6{{else}}5{{/if}}. 提交前自检
88
+ ### {{#if reef.frontend.framework.sourcePath}}6{{else}}5{{/if}}. 🔴 提交前自检(后置检查)
83
89
 
84
- 加载 `reef:reef-style-frontend` 技能,逐项检查所有规范要求。**未通过不得提交。**
90
+ 重新加载 `reef:reef-style-frontend` 技能,逐项对照所有规范要求检查本变更中的每处代码修改。**检查未通过不得提交。**
@@ -10,25 +10,17 @@ deepstorm:
10
10
 
11
11
  ## 上下文约定
12
12
 
13
- 当前 change = 当前分支名(由 jira-start 阶段三建立):
13
+ 使用 `find-change-dir.mjs` 自动发现当前 change:
14
14
 
15
15
  ```bash
16
- # 自动发现当前 change
17
- CHANGE=$(git branch --show-current)
18
- CHANGE_DIR="openspec/changes/$CHANGE"
19
-
20
- # 如分支名与 change 目录不匹配,遍历所有 change 取最近一个
21
- if [ ! -d "$CHANGE_DIR" ]; then
22
- CHANGE=$(ls -t openspec/changes/ 2>/dev/null | head -1)
23
- CHANGE_DIR="openspec/changes/$CHANGE"
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
- # 列出当前 change 下所有 SDD 文档
49
- find "$CHANGE_DIR" -name "*.md" -maxdepth 2 | sort
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
- BRANCH=$(git branch --show-current)
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
- 如有 OpenSpec change,读取 `proposal.md` `tasks.md` 作为描述素材。
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
- EXISTING_PR=$(gh pr view --json url 2>/dev/null)
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 链接。
@@ -0,0 +1,217 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * create-pr.mjs — PR 上下文收集与创建
5
+ *
6
+ * 用法:
7
+ * node create-pr.mjs --collect # 收集 PR 上下文
8
+ * node create-pr.mjs --create <args> # 创建 PR
9
+ * node create-pr.mjs --help
10
+ */
11
+
12
+ import { execSync } from 'node:child_process';
13
+ import { existsSync, readFileSync } from 'node:fs';
14
+ import { join } from 'node:path';
15
+
16
+ // ── Pure functions ────────────────────────────────────────────────
17
+
18
+ export function checkUncommitted(statusShort) {
19
+ return statusShort.trim().length > 0;
20
+ }
21
+
22
+ export function buildPrBody(ctx) {
23
+ const lines = [];
24
+
25
+ lines.push('## 概要');
26
+ if (ctx.proposalTitle) {
27
+ lines.push(ctx.proposalTitle);
28
+ } else if (ctx.commitLog && ctx.commitLog.length > 0) {
29
+ lines.push(ctx.commitLog[0].message);
30
+ } else {
31
+ lines.push(ctx.branch);
32
+ }
33
+
34
+ lines.push('');
35
+ lines.push('## 关联');
36
+ lines.push(`Branch: ${ctx.branch}`);
37
+
38
+ if (ctx.diffStat) {
39
+ const s = ctx.diffStat;
40
+ lines.push(`Changes: ${s.files} 文件, +${s.insertions}/-${s.deletions}`);
41
+ lines.push('');
42
+ lines.push('## 变更清单');
43
+ s.changedFiles.forEach(f => lines.push(`- \`${f}\``));
44
+ }
45
+
46
+ if (ctx.commitLog && ctx.commitLog.length > 0) {
47
+ lines.push('');
48
+ lines.push('## Commits');
49
+ ctx.commitLog.forEach(c => lines.push(`- ${c.hash} ${c.message}`));
50
+ }
51
+
52
+ return lines.join('\n');
53
+ }
54
+
55
+ export function buildGhCreateArgs(opts) {
56
+ const args = ['gh pr create'];
57
+ args.push(`--title "${opts.title}"`);
58
+ args.push(`--body "${opts.body}"`);
59
+ if (opts.reviewer) args.push(`--reviewer ${opts.reviewer}`);
60
+ if (opts.label) args.push(`--label ${opts.label}`);
61
+ if (opts.draft) args.push('--draft');
62
+ return args.join(' ');
63
+ }
64
+
65
+ // ── Git helpers ────────────────────────────────────────────────────
66
+
67
+ function exec(command) {
68
+ try {
69
+ return execSync(command, { encoding: 'utf8', timeout: 10000 }).trim();
70
+ } catch {
71
+ return '';
72
+ }
73
+ }
74
+
75
+ function collectContext() {
76
+ const branch = exec('git branch --show-current');
77
+ const forkPoint = exec(
78
+ 'git merge-base main HEAD 2>/dev/null || echo ""'
79
+ );
80
+ const diffStat = exec(
81
+ `git diff "${forkPoint || 'main'}..HEAD" --stat`
82
+ );
83
+ const commitLog = forkPoint
84
+ ? exec(`git log "${forkPoint}"..HEAD --oneline`)
85
+ : '';
86
+ const statusShort = exec('git status --short');
87
+
88
+ // 查找关联 proposal
89
+ let proposalTitle = '';
90
+ const root = exec(
91
+ 'git rev-parse --show-toplevel 2>/dev/null || echo ""'
92
+ );
93
+ if (root && branch) {
94
+ const p = join(root, 'openspec', 'changes', branch, 'proposal.md');
95
+ if (existsSync(p)) {
96
+ const firstLine = readFileSync(p, 'utf8')
97
+ .split('\n')[0] || '';
98
+ proposalTitle = firstLine.replace(/^#+\s*/, '').trim();
99
+ }
100
+ }
101
+
102
+ return {
103
+ branch,
104
+ hasUncommitted: checkUncommitted(statusShort),
105
+ proposalTitle,
106
+ commitLog: commitLog
107
+ ? commitLog.split('\n').filter(Boolean).map(l => {
108
+ const m = l.match(/^(\S+)\s+(.*)/);
109
+ return m ? { hash: m[1], message: m[2] } : null;
110
+ }).filter(Boolean)
111
+ : [],
112
+ diffStat: diffStat ? parseDiffStat(diffStat) : null,
113
+ };
114
+ }
115
+
116
+ function parseDiffStat(output) {
117
+ const lines = output.trim().split('\n').filter(Boolean);
118
+ const changedFiles = [];
119
+ let files = 0, insertions = 0, deletions = 0;
120
+
121
+ for (const line of lines) {
122
+ const sm = line.match(/(\d+)\s+files?\s+changed/);
123
+ if (sm) {
124
+ files = parseInt(sm[1], 10);
125
+ const im = line.match(/(\d+)\s+insertions?\(\+\)/);
126
+ if (im) insertions = parseInt(im[1], 10);
127
+ const dm = line.match(/(\d+)\s+deletions?\(-\)/);
128
+ if (dm) deletions = parseInt(dm[1], 10);
129
+ continue;
130
+ }
131
+ const fm = line.match(/^\s*(.+?)\s+\|/);
132
+ if (fm) changedFiles.push(fm[1].trim());
133
+ }
134
+
135
+ return { files, insertions, deletions, changedFiles };
136
+ }
137
+
138
+ function checkExistingPR() {
139
+ const out = exec('gh pr view --json url 2>/dev/null || true');
140
+ return out ? { exists: true, url: out } : { exists: false };
141
+ }
142
+
143
+ // ── CLI entry ─────────────────────────────────────────────────────
144
+
145
+ if (import.meta.filename === process.argv[1]) {
146
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
147
+ console.log(`
148
+ create-pr.mjs — PR 上下文收集与创建
149
+
150
+ 用法:
151
+ node create-pr.mjs --collect 收集 PR 上下文(JSON)
152
+ node create-pr.mjs --create 创建 PR
153
+ node create-pr.mjs --help 显示帮助
154
+
155
+ --create 选项:
156
+ --title <text> PR 标题
157
+ --body <text> PR 正文
158
+ --reviewer <user> Reviewer
159
+ --label <label> 标签
160
+ --draft 创建 Draft PR
161
+ `);
162
+ process.exit(0);
163
+ }
164
+
165
+ if (process.argv.includes('--collect')) {
166
+ const ctx = collectContext();
167
+ process.stdout.write(JSON.stringify(ctx, null, 2) + '\n');
168
+ process.exit(0);
169
+ }
170
+
171
+ if (process.argv.includes('--create')) {
172
+ const titleArg = process.argv.indexOf('--title');
173
+ const bodyArg = process.argv.indexOf('--body');
174
+ const reviewerArg = process.argv.indexOf('--reviewer');
175
+ const labelArg = process.argv.indexOf('--label');
176
+ const isDraft = process.argv.includes('--draft');
177
+
178
+ if (titleArg < 0 || bodyArg < 0) {
179
+ console.error('--title 和 --body 必填');
180
+ process.exit(1);
181
+ }
182
+
183
+ const existing = checkExistingPR();
184
+ if (existing.exists) {
185
+ process.stdout.write(JSON.stringify(existing, null, 2) + '\n');
186
+ process.exit(0);
187
+ }
188
+
189
+ const branch = exec('git branch --show-current');
190
+ const pushOut = exec(`git push -u origin ${branch} 2>&1`);
191
+ if (!pushOut) {
192
+ console.error('git push 失败');
193
+ process.exit(1);
194
+ }
195
+
196
+ const cmd = buildGhCreateArgs({
197
+ title: process.argv[titleArg + 1],
198
+ body: process.argv[bodyArg + 1],
199
+ branch,
200
+ reviewer: reviewerArg >= 0
201
+ ? process.argv[reviewerArg + 1] : null,
202
+ label: labelArg >= 0 ? process.argv[labelArg + 1] : null,
203
+ draft: isDraft,
204
+ });
205
+
206
+ const out = exec(cmd);
207
+ process.stdout.write(JSON.stringify({
208
+ created: true,
209
+ url: out,
210
+ }, null, 2) + '\n');
211
+ process.exit(0);
212
+ }
213
+
214
+ // 默认:--collect
215
+ const ctx = collectContext();
216
+ process.stdout.write(JSON.stringify(ctx, null, 2) + '\n');
217
+ }
@@ -377,6 +377,7 @@ openspec instructions tasks --change "$CHANGE" --json
377
377
  ### 流程
378
378
 
379
379
  1. **调用 Skill 工具** — 根据 tasks.md 的任务范围,加载可能适用的 superpowers:
380
+ - `reef:reef-style-backend` 或 `reef:reef-style-frontend`(如果 tasks 涉及后端/前端**代码改动**,**必须**加载对应 code-style 技能,未加载不得进入阶段四)
380
381
  - `superpowers:test-driven-development`(如果 tasks 涉及 TypeScript/Java/Python 等代码改动)
381
382
  - `superpowers:verification-before-completion`
382
383
  - 根据 tasks.md 内容判断是否适用其他 superpowers