specline 1.0.0 → 1.1.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/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- 前后端 code- 单元/ ✓ 完成
17
- creator reviewer 并行 reviewer 集成/
18
- E2E
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
- - **并行编码**:自动按前端/后端拆分任务,同批次并发派发 Coding Agent
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/ ← 7 个 Specline Agent 定义
61
+ │ ├── agents/ ← 9 个 Specline Agent 定义
54
62
  │ ├── commands/ ← 2 个 Slash 命令入口
55
63
  │ ├── skills/ ← 5 个 Skill 指令
56
- │ ├── hooks/ ← 4 个 Gate/Hook 脚本
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 分层 → 同批次前后端 Agent 并发
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
- 7 个子 Agent specline-pipeline- Cursor Hooks
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-code-reviewer` | 代码质量、安全性、可维护性审查 |
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,13 +1,163 @@
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
  const VERSION = '1.0.0';
10
12
 
13
+ // ============================================================
14
+ // 共享工具函数 — 锁文件读写、哈希计算
15
+ // ============================================================
16
+
17
+ /**
18
+ * 计算内容的 SHA-256 哈希,返回 sha256:<hex> 格式字符串
19
+ */
20
+ function sha256(content) {
21
+ const hash = createHash('sha256').update(content).digest('hex');
22
+ return `sha256:${hash}`;
23
+ }
24
+
25
+ /**
26
+ * 读取文件内容并计算 SHA-256 哈希
27
+ */
28
+ function computeFileHash(filePath) {
29
+ const content = readFileSync(filePath);
30
+ return sha256(content);
31
+ }
32
+
33
+ /**
34
+ * 读取 specline/.specline-lock.yaml,手工行解析器
35
+ * 返回 { version, synced_at, files: Map<string, string> } | null
36
+ */
37
+ function readLockFile(projectDir) {
38
+ const lockPath = join(projectDir, 'specline', '.specline-lock.yaml');
39
+ if (!existsSync(lockPath)) return null;
40
+
41
+ const lines = readFileSync(lockPath, 'utf-8').split('\n');
42
+ const result = { version: '', synced_at: '', files: new Map() };
43
+ let inFiles = false;
44
+
45
+ for (const line of lines) {
46
+ const trimmed = line.trim();
47
+ if (trimmed === '' || trimmed.startsWith('#')) continue;
48
+
49
+ if (trimmed.startsWith('version:')) {
50
+ result.version = trimmed.slice('version:'.length).trim().replace(/^"(.*)"$/, '$1');
51
+ } else if (trimmed.startsWith('synced_at:')) {
52
+ result.synced_at = trimmed.slice('synced_at:'.length).trim().replace(/^"(.*)"$/, '$1');
53
+ } else if (trimmed === 'files:') {
54
+ inFiles = true;
55
+ } else if (inFiles && trimmed.includes(':')) {
56
+ const colonIdx = trimmed.indexOf(':');
57
+ const key = trimmed.slice(0, colonIdx).trim();
58
+ const value = trimmed.slice(colonIdx + 1).trim();
59
+ result.files.set(key, value);
60
+ }
61
+ }
62
+
63
+ return result;
64
+ }
65
+
66
+ /**
67
+ * 将锁数据序列化为 YAML 格式写入 specline/.specline-lock.yaml
68
+ */
69
+ function writeLockFile(projectDir, lockData) {
70
+ const lockDir = join(projectDir, 'specline');
71
+ if (!existsSync(lockDir)) {
72
+ mkdirSync(lockDir, { recursive: true });
73
+ }
74
+ const lockPath = join(lockDir, '.specline-lock.yaml');
75
+ const lines = [
76
+ '# Specline Lock File — 自动生成,请勿手动编辑',
77
+ `version: "${lockData.version}"`,
78
+ `synced_at: "${lockData.synced_at}"`,
79
+ 'files:',
80
+ ];
81
+ for (const [key, value] of lockData.files) {
82
+ lines.push(` ${key}: ${value}`);
83
+ }
84
+ writeFileSync(lockPath, lines.join('\n') + '\n', 'utf-8');
85
+ }
86
+
87
+ /**
88
+ * 遍历 TEMPLATES_DIR 所有文件,构建锁数据结构
89
+ * 返回 { version, synced_at, files: Map<string, string> }
90
+ */
91
+ function buildLockData(projectDir) {
92
+ const files = new Map();
93
+
94
+ function walk(dir, base) {
95
+ const entries = readdirSync(dir, { withFileTypes: true });
96
+ for (const entry of entries) {
97
+ const fullPath = join(dir, entry.name);
98
+ const relPath = base ? `${base}/${entry.name}` : entry.name;
99
+ if (entry.isDirectory()) {
100
+ walk(fullPath, relPath);
101
+ } else {
102
+ files.set(relPath, computeFileHash(fullPath));
103
+ }
104
+ }
105
+ }
106
+
107
+ walk(TEMPLATES_DIR, '');
108
+
109
+ return {
110
+ version: VERSION,
111
+ synced_at: new Date().toISOString(),
112
+ files,
113
+ };
114
+ }
115
+
116
+ /**
117
+ * 版本号语义比较:返回 -1 (a<b)、0 (a==b)、1 (a>b)
118
+ */
119
+ function compareVersions(a, b) {
120
+ const aParts = a.split('.').map(Number);
121
+ const bParts = b.split('.').map(Number);
122
+ for (let i = 0; i < 3; i++) {
123
+ const av = aParts[i] || 0;
124
+ const bv = bParts[i] || 0;
125
+ if (av > bv) return 1;
126
+ if (av < bv) return -1;
127
+ }
128
+ return 0;
129
+ }
130
+
131
+ /**
132
+ * 九态决策树:根据模板哈希、锁记录、项目文件状态,分类文件同步策略
133
+ */
134
+ function classifyFile(templatePath, templateHash, lockEntry, projectPath) {
135
+ const projectExists = existsSync(projectPath);
136
+ if (!projectExists) return { type: 'NEW', path: templatePath };
137
+
138
+ const projectHash = computeFileHash(projectPath);
139
+
140
+ if (lockEntry) {
141
+ if (projectHash === lockEntry) {
142
+ // PRISTINE
143
+ if (templateHash === lockEntry) return { type: 'UNCHANGED', path: templatePath };
144
+ return { type: 'WILL_UPDATE', path: templatePath };
145
+ } else {
146
+ // MODIFIED
147
+ if (templateHash === lockEntry) return { type: 'MODIFIED_ONLY', path: templatePath };
148
+ return { type: 'CONFLICT', path: templatePath };
149
+ }
150
+ } else {
151
+ // 旧版项目,无 lock 记录
152
+ if (projectHash === templateHash) return { type: 'UNCHANGED', path: templatePath };
153
+ return { type: 'NO_LOCK_CONFLICT', path: templatePath };
154
+ }
155
+ }
156
+
157
+ // ============================================================
158
+ // 日志输出函数
159
+ // ============================================================
160
+
11
161
  function log(msg) {
12
162
  console.log(msg);
13
163
  }
@@ -122,6 +272,237 @@ initialized_at: "${new Date().toISOString()}"
122
272
  log(' /specline-pipeline "你的第一个需求"');
123
273
  log(' /specline-explore');
124
274
 
275
+ // 生成锁文件
276
+ const lockPath = join(target, 'specline', '.specline-lock.yaml');
277
+ if (existsSync(lockPath) && !forceMode) {
278
+ warn('锁文件已存在,跳过');
279
+ } else {
280
+ const lockData = buildLockData(target);
281
+ writeLockFile(target, lockData);
282
+ success('已生成锁文件');
283
+ }
284
+
285
+ process.exit(0);
286
+ }
287
+
288
+ function fetchLatestVersion() {
289
+ return new Promise((resolve, reject) => {
290
+ const req = get('https://registry.npmjs.org/specline/latest', (res) => {
291
+ let body = '';
292
+ if (res.statusCode !== 200) {
293
+ reject(new Error('PARSE_ERROR'));
294
+ return;
295
+ }
296
+ res.on('data', (chunk) => { body += chunk; });
297
+ res.on('end', () => {
298
+ try {
299
+ const data = JSON.parse(body);
300
+ resolve(data.version || null);
301
+ } catch {
302
+ reject(new Error('PARSE_ERROR'));
303
+ }
304
+ });
305
+ });
306
+ req.setTimeout(10000);
307
+ req.on('timeout', () => {
308
+ req.destroy();
309
+ reject(new Error('NETWORK_ERROR'));
310
+ });
311
+ req.on('error', () => {
312
+ reject(new Error('NETWORK_ERROR'));
313
+ });
314
+ });
315
+ }
316
+
317
+ async function cmd_update() {
318
+ let latest;
319
+ try {
320
+ latest = await fetchLatestVersion();
321
+ } catch (err) {
322
+ if (err.message === 'NETWORK_ERROR') {
323
+ warn('无法检查更新:网络连接失败');
324
+ } else {
325
+ warn('无法解析版本信息');
326
+ }
327
+ process.exit(0);
328
+ }
329
+
330
+ if (latest === null) {
331
+ warn('无法解析版本信息');
332
+ process.exit(0);
333
+ }
334
+
335
+ const currentParts = VERSION.split('.').map(Number);
336
+ const latestParts = latest.split('.').map(Number);
337
+
338
+ let isNewer = false;
339
+ for (let i = 0; i < 3; i++) {
340
+ const c = currentParts[i] || 0;
341
+ const l = latestParts[i] || 0;
342
+ if (l > c) {
343
+ isNewer = true;
344
+ break;
345
+ } else if (l < c) {
346
+ break;
347
+ }
348
+ }
349
+
350
+ if (isNewer) {
351
+ log('✨ 新版本可用: v' + latest + '(当前: v' + VERSION + ')\n运行 npm install -g specline@latest 更新');
352
+ } else {
353
+ success('已是最新版本 (v' + VERSION + ')');
354
+ }
355
+
356
+ process.exit(0);
357
+ }
358
+
359
+ function cmd_sync({ dryRun, targetPath }) {
360
+ const cwd = process.cwd();
361
+ const target = resolve(cwd, targetPath || '.');
362
+
363
+ // 1. 检查项目是否已初始化
364
+ const configFile = join(target, '.specline-config.yaml');
365
+ if (!existsSync(configFile)) {
366
+ error('未检测到 Specline 项目,请先运行 specline init');
367
+ process.exit(1);
368
+ }
369
+
370
+ // 2. 构建上游模板哈希映射
371
+ const upstreamData = buildLockData(target);
372
+ const upstreamFiles = upstreamData.files;
373
+
374
+ // 3. 读取锁文件
375
+ const lockData = readLockFile(target);
376
+
377
+ // 4. 版本校验
378
+ if (lockData) {
379
+ if (lockData.version === VERSION) {
380
+ success('项目模板已与 CLI 版本同步 (v' + VERSION + ')');
381
+ process.exit(0);
382
+ }
383
+ if (compareVersions(lockData.version, VERSION) > 0) {
384
+ warn('锁文件版本 (v' + lockData.version + ') 高于 CLI 版本 (v' + VERSION + '),继续同步可能导致问题');
385
+ if (!process.stdin.isTTY) {
386
+ error('非交互式环境,已跳过同步');
387
+ process.exit(1);
388
+ }
389
+ error('锁文件版本高于 CLI,请先更新 CLI');
390
+ process.exit(1);
391
+ }
392
+ }
393
+
394
+ // 5. 收集所有需要分类的路径
395
+ const allPaths = new Set();
396
+ for (const p of upstreamFiles.keys()) allPaths.add(p);
397
+ if (lockData) {
398
+ for (const p of lockData.files.keys()) {
399
+ if (!upstreamFiles.has(p)) allPaths.add(p);
400
+ }
401
+ }
402
+
403
+ // 6. 分类
404
+ const results = [];
405
+ for (const path of allPaths) {
406
+ const templateHash = upstreamFiles.get(path) || null;
407
+ const lockEntry = lockData ? (lockData.files.get(path) || null) : null;
408
+ const projectPath = join(target, path);
409
+
410
+ if (templateHash === null) {
411
+ results.push({ type: 'UPSTREAM_REMOVED', path });
412
+ } else {
413
+ results.push(classifyFile(path, templateHash, lockEntry, projectPath));
414
+ }
415
+ }
416
+
417
+ // 7. 统计
418
+ const stats = { newCount: 0, updated: 0, conflicted: 0, skippedModified: 0, unchanged: 0, upstreamRemoved: 0 };
419
+ for (const r of results) {
420
+ if (r.type === 'NEW') stats.newCount++;
421
+ else if (r.type === 'WILL_UPDATE') stats.updated++;
422
+ else if (r.type === 'CONFLICT' || r.type === 'NO_LOCK_CONFLICT') stats.conflicted++;
423
+ else if (r.type === 'MODIFIED_ONLY') stats.skippedModified++;
424
+ else if (r.type === 'UPSTREAM_REMOVED') stats.upstreamRemoved++;
425
+ else stats.unchanged++;
426
+ }
427
+
428
+ // 8. dryRun 模式只预览
429
+ if (dryRun) {
430
+ for (const r of results) {
431
+ if (r.type === 'UNCHANGED' || r.type === 'MODIFIED_ONLY') continue;
432
+ const labels = { NEW: '➕ 新增', WILL_UPDATE: '🔄 更新', CONFLICT: '⚠️ 冲突', NO_LOCK_CONFLICT: '⚠️ 无锁记录', UPSTREAM_REMOVED: '🗑️ 上游移除' };
433
+ log(labels[r.type] + ' ' + r.path);
434
+ }
435
+ if (stats.newCount === 0 && stats.updated === 0 && stats.conflicted === 0 && stats.upstreamRemoved === 0) {
436
+ log('所有模板文件已是最新,无需同步');
437
+ } else {
438
+ log('\n以上为预览,未实际执行。去掉 --dry-run 以执行同步。');
439
+ }
440
+ process.exit(0);
441
+ }
442
+
443
+ // 9. 执行写入
444
+ const newFiles = new Map();
445
+
446
+ for (const r of results) {
447
+ if (r.type === 'UNCHANGED' || r.type === 'MODIFIED_ONLY') {
448
+ const projectPath = join(target, r.path);
449
+ if (existsSync(projectPath)) {
450
+ newFiles.set(r.path, computeFileHash(projectPath));
451
+ }
452
+ continue;
453
+ }
454
+
455
+ if (r.type === 'UPSTREAM_REMOVED') {
456
+ warn('上游已移除:' + r.path);
457
+ continue;
458
+ }
459
+
460
+ // NEW/WILL_UPDATE/CONFLICT/NO_LOCK_CONFLICT: 复制模板文件
461
+ const srcPath = join(TEMPLATES_DIR, r.path);
462
+ const destPath = join(target, r.path);
463
+ const destDir = dirname(destPath);
464
+ if (!existsSync(destDir)) {
465
+ mkdirSync(destDir, { recursive: true });
466
+ }
467
+
468
+ try {
469
+ copyFileSync(srcPath, destPath);
470
+ newFiles.set(r.path, computeFileHash(destPath));
471
+
472
+ if (r.type === 'CONFLICT') {
473
+ warn('已覆盖(冲突): ' + r.path);
474
+ } else if (r.type === 'NO_LOCK_CONFLICT') {
475
+ warn('已覆盖(无锁文件记录): ' + r.path);
476
+ }
477
+ } catch (err) {
478
+ warn(r.path + ' 写入失败:' + err.message);
479
+ if (lockData && lockData.files.has(r.path)) {
480
+ newFiles.set(r.path, lockData.files.get(r.path));
481
+ }
482
+ }
483
+ }
484
+
485
+ // 10. 更新锁文件
486
+ writeLockFile(target, {
487
+ version: VERSION,
488
+ synced_at: new Date().toISOString(),
489
+ files: newFiles,
490
+ });
491
+
492
+ // 11. 输出摘要
493
+ if (stats.newCount === 0 && stats.updated === 0 && stats.conflicted === 0 && stats.upstreamRemoved === 0) {
494
+ log('所有模板文件已是最新,无需同步');
495
+ } else {
496
+ log('📊 同步摘要:');
497
+ log(' 总模板文件: ' + allPaths.size);
498
+ log(' ✅ 已新增: ' + stats.newCount);
499
+ log(' 🔄 已更新: ' + stats.updated);
500
+ log(' ⚠️ 已覆盖(冲突): ' + stats.conflicted);
501
+ log(' ⏭️ 已跳过(本地修改): ' + stats.skippedModified);
502
+ log(' 🗑️ 上游已移除: ' + stats.upstreamRemoved);
503
+ log(' ✨ 锁文件已更新至 v' + VERSION);
504
+ }
505
+
125
506
  process.exit(0);
126
507
  }
127
508
 
@@ -134,15 +515,18 @@ function cmd_help() {
134
515
  log(`specline v${VERSION} — Spec-driven AI coding pipeline for Cursor IDE
135
516
 
136
517
  用法:
137
- specline init [path] 在指定路径初始化流水线基础设施
138
- specline init --force 强制覆盖已有配置
139
- specline --version, -v 显示版本号
140
- specline --help, -h 显示此帮助信息
518
+ specline init [path] 在指定路径初始化流水线基础设施
519
+ specline init --force 强制覆盖已有配置
520
+ specline update 检查 CLI 自身更新(npm registry)
521
+ specline sync [--dry-run] [path] 同步项目模板文件到最新版本
522
+ specline --version, -v 显示版本号
523
+ specline --help, -h 显示此帮助信息
141
524
 
142
525
  示例:
143
- specline init 在当前目录初始化
144
- specline init ./my-project 在指定目录初始化
145
- npx specline init 无需全局安装即可使用
526
+ specline init 在当前目录初始化
527
+ specline init ./my-project 在指定目录初始化
528
+ specline sync --dry-run 预览模板文件更新
529
+ npx specline init 无需全局安装即可使用
146
530
  `);
147
531
  process.exit(0);
148
532
  }
@@ -152,11 +536,20 @@ const [,, command, ...args] = process.argv;
152
536
 
153
537
  switch (command) {
154
538
  case 'init': {
155
- // 过滤出 --force/-f 之外的真实路径参数
156
539
  const pathArg = args.filter(a => a !== '--force' && a !== '-f')[0];
157
540
  cmd_init(pathArg);
158
541
  break;
159
542
  }
543
+ case 'update': {
544
+ await cmd_update();
545
+ break;
546
+ }
547
+ case 'sync': {
548
+ const dryRun = args.includes('--dry-run');
549
+ const pathArg = args.filter(a => a !== '--dry-run')[0];
550
+ cmd_sync({ dryRun, targetPath: pathArg });
551
+ break;
552
+ }
160
553
  case '--version':
161
554
  case '-v':
162
555
  cmd_version();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specline",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
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
  }
@@ -34,7 +34,7 @@ description: 根据 Spec 编写后端代码(API 端点、数据模型、业务
34
34
 
35
35
  ## 产出报告
36
36
 
37
- 完成后输出 JSON 到 `.cursor/tmp/task-<task-id>-result.json`:
37
+ 完成后输出 JSON 到 `specline/changes/<change>/.tmp/task-<task-id>-result.json`:
38
38
 
39
39
  ```json
40
40
  {
@@ -25,7 +25,7 @@ description: 审查代码变更的质量、安全性和最佳实践。产出结
25
25
 
26
26
  ## 输出格式
27
27
 
28
- 产出 `code-review.json`:
28
+ 产出 `code-review.json` 到 `specline/changes/<change>/.tmp/code-review.json`:
29
29
 
30
30
  ```json
31
31
  {
@@ -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
+ ```