specline 1.0.0 → 1.1.1
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/README.md +48 -12
- package/cli.mjs +407 -11
- package/package.json +5 -1
- package/templates/.cursor/agents/specline-backend-dev.md +1 -1
- package/templates/.cursor/agents/specline-code-reviewer.md +1 -1
- package/templates/.cursor/agents/specline-config-dev.md +52 -0
- package/templates/.cursor/agents/specline-config-reviewer.md +79 -0
- package/templates/.cursor/agents/specline-frontend-dev.md +1 -1
- package/templates/.cursor/agents/specline-test-writer.md +1 -1
- package/templates/.cursor/hooks/specline-agent-guard.sh +113 -5
- package/templates/.cursor/hooks/specline-phase-guard.sh +202 -0
- package/templates/.cursor/hooks/specline-pipeline-gate.sh +5 -2
- package/templates/.cursor/hooks/specline-reminder.sh +139 -0
- package/templates/.cursor/hooks/specline-session-start.sh +134 -0
- package/templates/.cursor/hooks.json +22 -1
- package/templates/.cursor/skills/specline-apply-change/SKILL.md +35 -8
- package/templates/.cursor/skills/specline-archive-change/SKILL.md +39 -4
- package/templates/.cursor/skills/specline-explore/SKILL.md +21 -18
- package/templates/.cursor/skills/specline-pipeline/SKILL.md +311 -184
- package/templates/.cursor/skills/specline-propose/SKILL.md +101 -42
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
面向 Cursor IDE 的 **Spec 驱动 AI 编码流水线**,内置确定性质量门禁。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
自然语言需求 → 自动走完 编写规格 → 编码 → 审查 → 测试 → 归档 全流程:
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
/specline-pipeline "实现用户登录功能"
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
```
|
|
14
14
|
自然语言需求 → Spec → 审核 → 编码 → 审查 → 测试 → 归档
|
|
15
15
|
↑ ↑ ↑ ↑ ↑ ↑
|
|
16
|
-
spec- spec-
|
|
17
|
-
creator reviewer
|
|
18
|
-
|
|
16
|
+
spec- spec- 前后端/ code/ 单元/ ✓ 完成
|
|
17
|
+
creator reviewer config config 集成/
|
|
18
|
+
并行 reviewer E2E
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
每个阶段都经过 **确定性门禁校验** —— 用 `grep`、`jq`、编译器退出码、测试退出码判断通过与否。**质量判断零 LLM 参与**。
|
|
@@ -23,11 +23,12 @@
|
|
|
23
23
|
## 核心特性
|
|
24
24
|
|
|
25
25
|
- **需求驱动**:自然语言 → 结构化规格文档(Requirements + Scenarios + WHEN/THEN)
|
|
26
|
-
-
|
|
26
|
+
- **并行编码**:自动按前端/后端/config 拆分任务,同批次并发派发 Coding Agent
|
|
27
27
|
- **确定性门禁**:每个阶段用 Shell 脚本的退出码判定是否通过,不做模糊判断
|
|
28
28
|
- **黑盒测试**:测试 Agent 只看 Spec 文档,不能读取任何实现源码
|
|
29
29
|
- **断点续跑**:随时中断,下次从最后一个可信门禁自动恢复(tasks.md 的 `[x]`/`[ ]` 标记进度)
|
|
30
30
|
- **人机协作**:3 个人工检查点——Spec 确认、Review 可选复核、归档确认
|
|
31
|
+
- **Hook 约束体系**:sessionStart 注入 pipeline 上下文 → preToolUse 违规拦截 → postToolUse 操作后提醒,确保长对话中 Agent 不偏离流水线逻辑
|
|
31
32
|
- **安全 Hook**:自动拦截危险 Shell 命令(如 `rm -rf`、`curl|bash`)+ 代码变更后自动格式化
|
|
32
33
|
- **零外部依赖**:不依赖 OpenSpec CLI,全部功能自包含
|
|
33
34
|
|
|
@@ -43,6 +44,13 @@ specline init
|
|
|
43
44
|
|
|
44
45
|
# 或者用 npx(无需安装)
|
|
45
46
|
npx specline init
|
|
47
|
+
|
|
48
|
+
# 检查 CLI 更新
|
|
49
|
+
specline update
|
|
50
|
+
|
|
51
|
+
# 同步项目模板文件到最新版本
|
|
52
|
+
specline sync
|
|
53
|
+
specline sync --dry-run # 预览变更
|
|
46
54
|
```
|
|
47
55
|
|
|
48
56
|
初始化后项目会获得完整的流水线基础设施:
|
|
@@ -50,10 +58,10 @@ npx specline init
|
|
|
50
58
|
```
|
|
51
59
|
my-project/
|
|
52
60
|
├── .cursor/
|
|
53
|
-
│ ├── agents/ ←
|
|
61
|
+
│ ├── agents/ ← 9 个 Specline Agent 定义
|
|
54
62
|
│ ├── commands/ ← 2 个 Slash 命令入口
|
|
55
63
|
│ ├── skills/ ← 5 个 Skill 指令
|
|
56
|
-
│ ├── hooks/ ←
|
|
64
|
+
│ ├── hooks/ ← 7 个 Gate/Hook 脚本
|
|
57
65
|
│ └── hooks.json ← Cursor Hook 配置
|
|
58
66
|
├── specline/ ← 运行时目录
|
|
59
67
|
│ ├── config.yaml
|
|
@@ -89,16 +97,17 @@ PHASE 1: SPEC(规格)
|
|
|
89
97
|
→ 🟡 人工确认 Spec 和任务规划
|
|
90
98
|
|
|
91
99
|
PHASE 2: CODING(编码)
|
|
92
|
-
解析 tasks.md → 按依赖 DAG 分层 →
|
|
100
|
+
解析 tasks.md → 按依赖 DAG 分层 → 同批次前后端/config Agent 并发
|
|
93
101
|
每完成一个任务,[ ] 自动标记为 [x]
|
|
94
102
|
→ Gate: 编译检查(tsc --noEmit / python -m compileall)
|
|
95
103
|
|
|
96
104
|
PHASE 3: REVIEW(审查)
|
|
97
|
-
specline-code-reviewer
|
|
105
|
+
specline-code-reviewer + specline-config-reviewer 分别审查代码和配置/文档
|
|
98
106
|
→ Gate: Lint 检查 + code-review.json error 计数
|
|
99
107
|
|
|
100
108
|
PHASE 4: TEST(测试)
|
|
101
109
|
单元测试 → 集成测试 → E2E 测试(黑盒,只看 Spec)
|
|
110
|
+
→ config/docs 变更自动跳过测试
|
|
102
111
|
→ 失败自动分析:测试写错了 / 代码写错了 / Spec 模糊
|
|
103
112
|
→ 自动重试最多 2 次
|
|
104
113
|
|
|
@@ -119,11 +128,21 @@ specline-pipeline SKILL ← 编排层(读状态、派发 Agent、调 Gate)
|
|
|
119
128
|
│
|
|
120
129
|
┌───┼──────────────────┬──────────────────────┐
|
|
121
130
|
▼ ▼ ▼ ▼
|
|
122
|
-
|
|
123
|
-
(创造性工作) gate.sh
|
|
131
|
+
9 个子 Agent specline-pipeline- Cursor Hooks
|
|
132
|
+
(创造性工作) gate.sh (安全网 + 约束)
|
|
124
133
|
(确定性门禁)
|
|
125
134
|
```
|
|
126
135
|
|
|
136
|
+
## CLI 命令
|
|
137
|
+
|
|
138
|
+
| 命令 | 说明 |
|
|
139
|
+
|------|------|
|
|
140
|
+
| `specline init [path]` | 在指定路径(默认当前目录)初始化 Specline 项目,复制模板文件并生成锁文件 |
|
|
141
|
+
| `specline update` | 检查 CLI 是否有新版本可用(npm registry),输出更新提示 |
|
|
142
|
+
| `specline sync [--dry-run] [path]` | 将上游最新模板文件同步到项目,基于 Lock File 智能识别安全更新/冲突/仅本地修改。`--dry-run` 预览变更不实际写入 |
|
|
143
|
+
| `specline --version` | 显示当前 CLI 版本号 |
|
|
144
|
+
| `specline --help` | 显示帮助信息 |
|
|
145
|
+
|
|
127
146
|
## 子 Agent 列表
|
|
128
147
|
|
|
129
148
|
| Agent | 职责 |
|
|
@@ -132,7 +151,9 @@ specline-pipeline SKILL ← 编排层(读状态、派发 Agent、调 Gate)
|
|
|
132
151
|
| `specline-spec-reviewer` | 审核规格的完整性、一致性和覆盖度 |
|
|
133
152
|
| `specline-frontend-dev` | UI 组件、页面、样式、交互逻辑(单个任务级别) |
|
|
134
153
|
| `specline-backend-dev` | API 端点、数据模型、业务逻辑(单个任务级别) |
|
|
135
|
-
| `specline-
|
|
154
|
+
| `specline-config-dev` | Shell 脚本、配置文件(JSON/YAML)、Markdown 文档(处理 Type: config/docs 任务) |
|
|
155
|
+
| `specline-code-reviewer` | 前端/后端代码质量、安全性、可维护性审查 |
|
|
156
|
+
| `specline-config-reviewer` | Shell 脚本安全性、配置文件语法和一致性、Markdown 文档结构审查 |
|
|
136
157
|
| `specline-test-writer` | 黑盒测试编写——只能看 Spec,不能读源码 |
|
|
137
158
|
| `specline-test-runner` | 执行测试并分类失败原因(测试问题/代码问题/Spec 模糊) |
|
|
138
159
|
|
|
@@ -148,6 +169,21 @@ specline-pipeline SKILL ← 编排层(读状态、派发 Agent、调 Gate)
|
|
|
148
169
|
| Test | 测试框架退出码 + 覆盖率阈值 |
|
|
149
170
|
| Archive | 归档目录结构 + 必要文件完整性 |
|
|
150
171
|
|
|
172
|
+
## Hook 约束体系
|
|
173
|
+
|
|
174
|
+
Specline 通过 Cursor Hooks 构建三层约束,确保长对话中 Agent 始终遵循流水线的阶段逻辑:
|
|
175
|
+
|
|
176
|
+
| Hook | 时机 | 作用 |
|
|
177
|
+
|------|------|------|
|
|
178
|
+
| `sessionStart` | 新会话启动 | 扫描活跃 pipeline,自动注入阶段上下文到 Agent 系统提示 |
|
|
179
|
+
| `preToolUse` | 工具调用前 | 阶段校验:SPEC 阶段拦截代码编辑、阶段不匹配的子 Agent 启动 |
|
|
180
|
+
| `postToolUse` | 工具调用后 | 注入下一步提醒:更新 tasks.md checkbox、运行 Gate 脚本 |
|
|
181
|
+
| `subagentStart` | 子 Agent 启动前 | 白名单 + 阶段匹配双校验 |
|
|
182
|
+
| `beforeShellExecution` | Shell 命令执行前 | 拦截危险命令(`rm -rf`、`curl\|bash`、`sudo`) |
|
|
183
|
+
| `afterFileEdit` | 文件编辑后 | 自动格式化代码 |
|
|
184
|
+
|
|
185
|
+
> 非流水线会话完全透明——所有 Hook 第一步检查「是否有活跃 pipeline」,无则直接放行。
|
|
186
|
+
|
|
151
187
|
## 环境要求
|
|
152
188
|
|
|
153
189
|
- **Cursor IDE**(支持 hooks 和 skills)
|
package/cli.mjs
CHANGED
|
@@ -1,12 +1,165 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { existsSync, mkdirSync, readdirSync, copyFileSync, writeFileSync } from 'fs';
|
|
4
|
-
import { join, dirname, resolve } from 'path';
|
|
3
|
+
import { existsSync, mkdirSync, readdirSync, copyFileSync, writeFileSync, readFileSync } from 'fs';
|
|
4
|
+
import { join, dirname, resolve, relative } from 'path';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
|
+
import { createHash } from 'crypto';
|
|
7
|
+
import { get } from 'https';
|
|
6
8
|
|
|
7
9
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
10
|
const TEMPLATES_DIR = join(__dirname, 'templates');
|
|
9
|
-
|
|
11
|
+
|
|
12
|
+
// 从 package.json 读取版本号(由 npm version 命令自动维护)
|
|
13
|
+
const PKG = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf-8'));
|
|
14
|
+
const VERSION = PKG.version;
|
|
15
|
+
|
|
16
|
+
// ============================================================
|
|
17
|
+
// 共享工具函数 — 锁文件读写、哈希计算
|
|
18
|
+
// ============================================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 计算内容的 SHA-256 哈希,返回 sha256:<hex> 格式字符串
|
|
22
|
+
*/
|
|
23
|
+
function sha256(content) {
|
|
24
|
+
const hash = createHash('sha256').update(content).digest('hex');
|
|
25
|
+
return `sha256:${hash}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 读取文件内容并计算 SHA-256 哈希
|
|
30
|
+
*/
|
|
31
|
+
function computeFileHash(filePath) {
|
|
32
|
+
const content = readFileSync(filePath);
|
|
33
|
+
return sha256(content);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 读取 specline/.specline-lock.yaml,手工行解析器
|
|
38
|
+
* 返回 { version, synced_at, files: Map<string, string> } | null
|
|
39
|
+
*/
|
|
40
|
+
function readLockFile(projectDir) {
|
|
41
|
+
const lockPath = join(projectDir, 'specline', '.specline-lock.yaml');
|
|
42
|
+
if (!existsSync(lockPath)) return null;
|
|
43
|
+
|
|
44
|
+
const lines = readFileSync(lockPath, 'utf-8').split('\n');
|
|
45
|
+
const result = { version: '', synced_at: '', files: new Map() };
|
|
46
|
+
let inFiles = false;
|
|
47
|
+
|
|
48
|
+
for (const line of lines) {
|
|
49
|
+
const trimmed = line.trim();
|
|
50
|
+
if (trimmed === '' || trimmed.startsWith('#')) continue;
|
|
51
|
+
|
|
52
|
+
if (trimmed.startsWith('version:')) {
|
|
53
|
+
result.version = trimmed.slice('version:'.length).trim().replace(/^"(.*)"$/, '$1');
|
|
54
|
+
} else if (trimmed.startsWith('synced_at:')) {
|
|
55
|
+
result.synced_at = trimmed.slice('synced_at:'.length).trim().replace(/^"(.*)"$/, '$1');
|
|
56
|
+
} else if (trimmed === 'files:') {
|
|
57
|
+
inFiles = true;
|
|
58
|
+
} else if (inFiles && trimmed.includes(':')) {
|
|
59
|
+
const colonIdx = trimmed.indexOf(':');
|
|
60
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
61
|
+
const value = trimmed.slice(colonIdx + 1).trim();
|
|
62
|
+
result.files.set(key, value);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 将锁数据序列化为 YAML 格式写入 specline/.specline-lock.yaml
|
|
71
|
+
*/
|
|
72
|
+
function writeLockFile(projectDir, lockData) {
|
|
73
|
+
const lockDir = join(projectDir, 'specline');
|
|
74
|
+
if (!existsSync(lockDir)) {
|
|
75
|
+
mkdirSync(lockDir, { recursive: true });
|
|
76
|
+
}
|
|
77
|
+
const lockPath = join(lockDir, '.specline-lock.yaml');
|
|
78
|
+
const lines = [
|
|
79
|
+
'# Specline Lock File — 自动生成,请勿手动编辑',
|
|
80
|
+
`version: "${lockData.version}"`,
|
|
81
|
+
`synced_at: "${lockData.synced_at}"`,
|
|
82
|
+
'files:',
|
|
83
|
+
];
|
|
84
|
+
for (const [key, value] of lockData.files) {
|
|
85
|
+
lines.push(` ${key}: ${value}`);
|
|
86
|
+
}
|
|
87
|
+
writeFileSync(lockPath, lines.join('\n') + '\n', 'utf-8');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 遍历 TEMPLATES_DIR 所有文件,构建锁数据结构
|
|
92
|
+
* 返回 { version, synced_at, files: Map<string, string> }
|
|
93
|
+
*/
|
|
94
|
+
function buildLockData(projectDir) {
|
|
95
|
+
const files = new Map();
|
|
96
|
+
|
|
97
|
+
function walk(dir, base) {
|
|
98
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
const fullPath = join(dir, entry.name);
|
|
101
|
+
const relPath = base ? `${base}/${entry.name}` : entry.name;
|
|
102
|
+
if (entry.isDirectory()) {
|
|
103
|
+
walk(fullPath, relPath);
|
|
104
|
+
} else {
|
|
105
|
+
files.set(relPath, computeFileHash(fullPath));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
walk(TEMPLATES_DIR, '');
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
version: VERSION,
|
|
114
|
+
synced_at: new Date().toISOString(),
|
|
115
|
+
files,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 版本号语义比较:返回 -1 (a<b)、0 (a==b)、1 (a>b)
|
|
121
|
+
*/
|
|
122
|
+
function compareVersions(a, b) {
|
|
123
|
+
const aParts = a.split('.').map(Number);
|
|
124
|
+
const bParts = b.split('.').map(Number);
|
|
125
|
+
for (let i = 0; i < 3; i++) {
|
|
126
|
+
const av = aParts[i] || 0;
|
|
127
|
+
const bv = bParts[i] || 0;
|
|
128
|
+
if (av > bv) return 1;
|
|
129
|
+
if (av < bv) return -1;
|
|
130
|
+
}
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 九态决策树:根据模板哈希、锁记录、项目文件状态,分类文件同步策略
|
|
136
|
+
*/
|
|
137
|
+
function classifyFile(templatePath, templateHash, lockEntry, projectPath) {
|
|
138
|
+
const projectExists = existsSync(projectPath);
|
|
139
|
+
if (!projectExists) return { type: 'NEW', path: templatePath };
|
|
140
|
+
|
|
141
|
+
const projectHash = computeFileHash(projectPath);
|
|
142
|
+
|
|
143
|
+
if (lockEntry) {
|
|
144
|
+
if (projectHash === lockEntry) {
|
|
145
|
+
// PRISTINE
|
|
146
|
+
if (templateHash === lockEntry) return { type: 'UNCHANGED', path: templatePath };
|
|
147
|
+
return { type: 'WILL_UPDATE', path: templatePath };
|
|
148
|
+
} else {
|
|
149
|
+
// MODIFIED
|
|
150
|
+
if (templateHash === lockEntry) return { type: 'MODIFIED_ONLY', path: templatePath };
|
|
151
|
+
return { type: 'CONFLICT', path: templatePath };
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
// 旧版项目,无 lock 记录
|
|
155
|
+
if (projectHash === templateHash) return { type: 'UNCHANGED', path: templatePath };
|
|
156
|
+
return { type: 'NO_LOCK_CONFLICT', path: templatePath };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ============================================================
|
|
161
|
+
// 日志输出函数
|
|
162
|
+
// ============================================================
|
|
10
163
|
|
|
11
164
|
function log(msg) {
|
|
12
165
|
console.log(msg);
|
|
@@ -122,6 +275,237 @@ initialized_at: "${new Date().toISOString()}"
|
|
|
122
275
|
log(' /specline-pipeline "你的第一个需求"');
|
|
123
276
|
log(' /specline-explore');
|
|
124
277
|
|
|
278
|
+
// 生成锁文件
|
|
279
|
+
const lockPath = join(target, 'specline', '.specline-lock.yaml');
|
|
280
|
+
if (existsSync(lockPath) && !forceMode) {
|
|
281
|
+
warn('锁文件已存在,跳过');
|
|
282
|
+
} else {
|
|
283
|
+
const lockData = buildLockData(target);
|
|
284
|
+
writeLockFile(target, lockData);
|
|
285
|
+
success('已生成锁文件');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
process.exit(0);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function fetchLatestVersion() {
|
|
292
|
+
return new Promise((resolve, reject) => {
|
|
293
|
+
const req = get('https://registry.npmjs.org/specline/latest', (res) => {
|
|
294
|
+
let body = '';
|
|
295
|
+
if (res.statusCode !== 200) {
|
|
296
|
+
reject(new Error('PARSE_ERROR'));
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
300
|
+
res.on('end', () => {
|
|
301
|
+
try {
|
|
302
|
+
const data = JSON.parse(body);
|
|
303
|
+
resolve(data.version || null);
|
|
304
|
+
} catch {
|
|
305
|
+
reject(new Error('PARSE_ERROR'));
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
req.setTimeout(10000);
|
|
310
|
+
req.on('timeout', () => {
|
|
311
|
+
req.destroy();
|
|
312
|
+
reject(new Error('NETWORK_ERROR'));
|
|
313
|
+
});
|
|
314
|
+
req.on('error', () => {
|
|
315
|
+
reject(new Error('NETWORK_ERROR'));
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function cmd_update() {
|
|
321
|
+
let latest;
|
|
322
|
+
try {
|
|
323
|
+
latest = await fetchLatestVersion();
|
|
324
|
+
} catch (err) {
|
|
325
|
+
if (err.message === 'NETWORK_ERROR') {
|
|
326
|
+
warn('无法检查更新:网络连接失败');
|
|
327
|
+
} else {
|
|
328
|
+
warn('无法解析版本信息');
|
|
329
|
+
}
|
|
330
|
+
process.exit(0);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (latest === null) {
|
|
334
|
+
warn('无法解析版本信息');
|
|
335
|
+
process.exit(0);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const currentParts = VERSION.split('.').map(Number);
|
|
339
|
+
const latestParts = latest.split('.').map(Number);
|
|
340
|
+
|
|
341
|
+
let isNewer = false;
|
|
342
|
+
for (let i = 0; i < 3; i++) {
|
|
343
|
+
const c = currentParts[i] || 0;
|
|
344
|
+
const l = latestParts[i] || 0;
|
|
345
|
+
if (l > c) {
|
|
346
|
+
isNewer = true;
|
|
347
|
+
break;
|
|
348
|
+
} else if (l < c) {
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (isNewer) {
|
|
354
|
+
log('✨ 新版本可用: v' + latest + '(当前: v' + VERSION + ')\n运行 npm install -g specline@latest 更新');
|
|
355
|
+
} else {
|
|
356
|
+
success('已是最新版本 (v' + VERSION + ')');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
process.exit(0);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function cmd_sync({ dryRun, targetPath }) {
|
|
363
|
+
const cwd = process.cwd();
|
|
364
|
+
const target = resolve(cwd, targetPath || '.');
|
|
365
|
+
|
|
366
|
+
// 1. 检查项目是否已初始化
|
|
367
|
+
const configFile = join(target, '.specline-config.yaml');
|
|
368
|
+
if (!existsSync(configFile)) {
|
|
369
|
+
error('未检测到 Specline 项目,请先运行 specline init');
|
|
370
|
+
process.exit(1);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// 2. 构建上游模板哈希映射
|
|
374
|
+
const upstreamData = buildLockData(target);
|
|
375
|
+
const upstreamFiles = upstreamData.files;
|
|
376
|
+
|
|
377
|
+
// 3. 读取锁文件
|
|
378
|
+
const lockData = readLockFile(target);
|
|
379
|
+
|
|
380
|
+
// 4. 版本校验
|
|
381
|
+
if (lockData) {
|
|
382
|
+
if (lockData.version === VERSION) {
|
|
383
|
+
success('项目模板已与 CLI 版本同步 (v' + VERSION + ')');
|
|
384
|
+
process.exit(0);
|
|
385
|
+
}
|
|
386
|
+
if (compareVersions(lockData.version, VERSION) > 0) {
|
|
387
|
+
warn('锁文件版本 (v' + lockData.version + ') 高于 CLI 版本 (v' + VERSION + '),继续同步可能导致问题');
|
|
388
|
+
if (!process.stdin.isTTY) {
|
|
389
|
+
error('非交互式环境,已跳过同步');
|
|
390
|
+
process.exit(1);
|
|
391
|
+
}
|
|
392
|
+
error('锁文件版本高于 CLI,请先更新 CLI');
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// 5. 收集所有需要分类的路径
|
|
398
|
+
const allPaths = new Set();
|
|
399
|
+
for (const p of upstreamFiles.keys()) allPaths.add(p);
|
|
400
|
+
if (lockData) {
|
|
401
|
+
for (const p of lockData.files.keys()) {
|
|
402
|
+
if (!upstreamFiles.has(p)) allPaths.add(p);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// 6. 分类
|
|
407
|
+
const results = [];
|
|
408
|
+
for (const path of allPaths) {
|
|
409
|
+
const templateHash = upstreamFiles.get(path) || null;
|
|
410
|
+
const lockEntry = lockData ? (lockData.files.get(path) || null) : null;
|
|
411
|
+
const projectPath = join(target, path);
|
|
412
|
+
|
|
413
|
+
if (templateHash === null) {
|
|
414
|
+
results.push({ type: 'UPSTREAM_REMOVED', path });
|
|
415
|
+
} else {
|
|
416
|
+
results.push(classifyFile(path, templateHash, lockEntry, projectPath));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// 7. 统计
|
|
421
|
+
const stats = { newCount: 0, updated: 0, conflicted: 0, skippedModified: 0, unchanged: 0, upstreamRemoved: 0 };
|
|
422
|
+
for (const r of results) {
|
|
423
|
+
if (r.type === 'NEW') stats.newCount++;
|
|
424
|
+
else if (r.type === 'WILL_UPDATE') stats.updated++;
|
|
425
|
+
else if (r.type === 'CONFLICT' || r.type === 'NO_LOCK_CONFLICT') stats.conflicted++;
|
|
426
|
+
else if (r.type === 'MODIFIED_ONLY') stats.skippedModified++;
|
|
427
|
+
else if (r.type === 'UPSTREAM_REMOVED') stats.upstreamRemoved++;
|
|
428
|
+
else stats.unchanged++;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// 8. dryRun 模式只预览
|
|
432
|
+
if (dryRun) {
|
|
433
|
+
for (const r of results) {
|
|
434
|
+
if (r.type === 'UNCHANGED' || r.type === 'MODIFIED_ONLY') continue;
|
|
435
|
+
const labels = { NEW: '➕ 新增', WILL_UPDATE: '🔄 更新', CONFLICT: '⚠️ 冲突', NO_LOCK_CONFLICT: '⚠️ 无锁记录', UPSTREAM_REMOVED: '🗑️ 上游移除' };
|
|
436
|
+
log(labels[r.type] + ' ' + r.path);
|
|
437
|
+
}
|
|
438
|
+
if (stats.newCount === 0 && stats.updated === 0 && stats.conflicted === 0 && stats.upstreamRemoved === 0) {
|
|
439
|
+
log('所有模板文件已是最新,无需同步');
|
|
440
|
+
} else {
|
|
441
|
+
log('\n以上为预览,未实际执行。去掉 --dry-run 以执行同步。');
|
|
442
|
+
}
|
|
443
|
+
process.exit(0);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// 9. 执行写入
|
|
447
|
+
const newFiles = new Map();
|
|
448
|
+
|
|
449
|
+
for (const r of results) {
|
|
450
|
+
if (r.type === 'UNCHANGED' || r.type === 'MODIFIED_ONLY') {
|
|
451
|
+
const projectPath = join(target, r.path);
|
|
452
|
+
if (existsSync(projectPath)) {
|
|
453
|
+
newFiles.set(r.path, computeFileHash(projectPath));
|
|
454
|
+
}
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (r.type === 'UPSTREAM_REMOVED') {
|
|
459
|
+
warn('上游已移除:' + r.path);
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// NEW/WILL_UPDATE/CONFLICT/NO_LOCK_CONFLICT: 复制模板文件
|
|
464
|
+
const srcPath = join(TEMPLATES_DIR, r.path);
|
|
465
|
+
const destPath = join(target, r.path);
|
|
466
|
+
const destDir = dirname(destPath);
|
|
467
|
+
if (!existsSync(destDir)) {
|
|
468
|
+
mkdirSync(destDir, { recursive: true });
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
try {
|
|
472
|
+
copyFileSync(srcPath, destPath);
|
|
473
|
+
newFiles.set(r.path, computeFileHash(destPath));
|
|
474
|
+
|
|
475
|
+
if (r.type === 'CONFLICT') {
|
|
476
|
+
warn('已覆盖(冲突): ' + r.path);
|
|
477
|
+
} else if (r.type === 'NO_LOCK_CONFLICT') {
|
|
478
|
+
warn('已覆盖(无锁文件记录): ' + r.path);
|
|
479
|
+
}
|
|
480
|
+
} catch (err) {
|
|
481
|
+
warn(r.path + ' 写入失败:' + err.message);
|
|
482
|
+
if (lockData && lockData.files.has(r.path)) {
|
|
483
|
+
newFiles.set(r.path, lockData.files.get(r.path));
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// 10. 更新锁文件
|
|
489
|
+
writeLockFile(target, {
|
|
490
|
+
version: VERSION,
|
|
491
|
+
synced_at: new Date().toISOString(),
|
|
492
|
+
files: newFiles,
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// 11. 输出摘要
|
|
496
|
+
if (stats.newCount === 0 && stats.updated === 0 && stats.conflicted === 0 && stats.upstreamRemoved === 0) {
|
|
497
|
+
log('所有模板文件已是最新,无需同步');
|
|
498
|
+
} else {
|
|
499
|
+
log('📊 同步摘要:');
|
|
500
|
+
log(' 总模板文件: ' + allPaths.size);
|
|
501
|
+
log(' ✅ 已新增: ' + stats.newCount);
|
|
502
|
+
log(' 🔄 已更新: ' + stats.updated);
|
|
503
|
+
log(' ⚠️ 已覆盖(冲突): ' + stats.conflicted);
|
|
504
|
+
log(' ⏭️ 已跳过(本地修改): ' + stats.skippedModified);
|
|
505
|
+
log(' 🗑️ 上游已移除: ' + stats.upstreamRemoved);
|
|
506
|
+
log(' ✨ 锁文件已更新至 v' + VERSION);
|
|
507
|
+
}
|
|
508
|
+
|
|
125
509
|
process.exit(0);
|
|
126
510
|
}
|
|
127
511
|
|
|
@@ -134,15 +518,18 @@ function cmd_help() {
|
|
|
134
518
|
log(`specline v${VERSION} — Spec-driven AI coding pipeline for Cursor IDE
|
|
135
519
|
|
|
136
520
|
用法:
|
|
137
|
-
specline init [path]
|
|
138
|
-
specline init --force
|
|
139
|
-
specline
|
|
140
|
-
specline --
|
|
521
|
+
specline init [path] 在指定路径初始化流水线基础设施
|
|
522
|
+
specline init --force 强制覆盖已有配置
|
|
523
|
+
specline update 检查 CLI 自身更新(npm registry)
|
|
524
|
+
specline sync [--dry-run] [path] 同步项目模板文件到最新版本
|
|
525
|
+
specline --version, -v 显示版本号
|
|
526
|
+
specline --help, -h 显示此帮助信息
|
|
141
527
|
|
|
142
528
|
示例:
|
|
143
|
-
specline init
|
|
144
|
-
specline init ./my-project
|
|
145
|
-
|
|
529
|
+
specline init 在当前目录初始化
|
|
530
|
+
specline init ./my-project 在指定目录初始化
|
|
531
|
+
specline sync --dry-run 预览模板文件更新
|
|
532
|
+
npx specline init 无需全局安装即可使用
|
|
146
533
|
`);
|
|
147
534
|
process.exit(0);
|
|
148
535
|
}
|
|
@@ -152,11 +539,20 @@ const [,, command, ...args] = process.argv;
|
|
|
152
539
|
|
|
153
540
|
switch (command) {
|
|
154
541
|
case 'init': {
|
|
155
|
-
// 过滤出 --force/-f 之外的真实路径参数
|
|
156
542
|
const pathArg = args.filter(a => a !== '--force' && a !== '-f')[0];
|
|
157
543
|
cmd_init(pathArg);
|
|
158
544
|
break;
|
|
159
545
|
}
|
|
546
|
+
case 'update': {
|
|
547
|
+
await cmd_update();
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
case 'sync': {
|
|
551
|
+
const dryRun = args.includes('--dry-run');
|
|
552
|
+
const pathArg = args.filter(a => a !== '--dry-run')[0];
|
|
553
|
+
cmd_sync({ dryRun, targetPath: pathArg });
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
160
556
|
case '--version':
|
|
161
557
|
case '-v':
|
|
162
558
|
cmd_version();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specline",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Spec-driven AI coding pipeline with deterministic quality gates for Cursor IDE",
|
|
5
5
|
"bin": {
|
|
6
6
|
"specline": "./cli.mjs"
|
|
@@ -26,5 +26,9 @@
|
|
|
26
26
|
},
|
|
27
27
|
"engines": {
|
|
28
28
|
"node": ">=20.0.0"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"test": "node --test tests/",
|
|
32
|
+
"test:update-sync": "node --test tests/cli-update-sync.test.mjs"
|
|
29
33
|
}
|
|
30
34
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: specline-config-dev
|
|
3
|
+
description: 处理 Type: config 和 Type: docs 的编码任务——shell 脚本、配置文件(JSON/YAML)、Markdown 文档。只操作任务 Files 范围内的文件。支持 task-aware 模式。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
你是 **specline-config-dev**,专门处理 `Type: config` 和 `Type: docs` 的编码任务。
|
|
7
|
+
|
|
8
|
+
## 角色定位
|
|
9
|
+
|
|
10
|
+
你负责创建和修改:
|
|
11
|
+
- **Shell 脚本**(`.sh`):Hook 脚本、Gate 脚本、构建脚本
|
|
12
|
+
- **配置文件**(`.json`、`.yaml`、`.yml`):hooks.json、package.json、config.yaml
|
|
13
|
+
- **Markdown 文档**(`.md`):Agent 定义、SKILL.md、proposal.md、design.md
|
|
14
|
+
|
|
15
|
+
## 输入上下文
|
|
16
|
+
|
|
17
|
+
编排者会传入以下信息:
|
|
18
|
+
- **当前任务描述**:从 tasks.md 中提取的任务完整内容(含 Type、Covers、Files)
|
|
19
|
+
- **Spec 文档**:`specline/changes/<change>/specs/*/spec.md` — 功能需求
|
|
20
|
+
- **Design 文档**:`specline/changes/<change>/design.md` — 技术设计
|
|
21
|
+
- **Tasks 文档**:`specline/changes/<change>/tasks.md` — 任务列表
|
|
22
|
+
|
|
23
|
+
## 工作方式
|
|
24
|
+
|
|
25
|
+
1. **理解任务范围**:确认任务 `Type` 是 `config` 或 `docs`。如果不是,拒绝执行并返回错误信息:"specline-config-dev 只能处理 Type: config 或 Type: docs 的任务"
|
|
26
|
+
2. **阅读 Spec 和 Design**:确认技术决策、文件路径、依赖关系
|
|
27
|
+
3. **实现变更**:只操作任务 `Files` 字段中列出的文件
|
|
28
|
+
4. **检查安全**:对 shell 脚本检查常见注入风险,对 JSON 验证语法合法性
|
|
29
|
+
5. **标记进度**:将 tasks.md 中本任务的 `[ ]` 改为 `[x]`(方便断点续跑识别进度)
|
|
30
|
+
|
|
31
|
+
## 约束
|
|
32
|
+
|
|
33
|
+
1. 只修改本任务 `Files` 范围内的文件
|
|
34
|
+
2. 不修改其他任务负责的文件
|
|
35
|
+
3. 如果是模板文件(`templates/` 目录),同时检查对应的运行时文件是否需要同步
|
|
36
|
+
4. 确认过 design.md 中的技术决策后再动手
|
|
37
|
+
5. 保持代码风格一致
|
|
38
|
+
|
|
39
|
+
## 产出报告
|
|
40
|
+
|
|
41
|
+
完成后在 `specline/changes/<change>/.tmp/task-<task-id>-result.json` 写入:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"task_id": "<task-id>",
|
|
46
|
+
"type": "config|docs",
|
|
47
|
+
"covers": "<covers 声明>",
|
|
48
|
+
"status": "completed",
|
|
49
|
+
"files_changed": ["file1", "file2"],
|
|
50
|
+
"summary": "变更摘要"
|
|
51
|
+
}
|
|
52
|
+
```
|