specline 1.4.0 → 2.0.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 +132 -125
- package/adapters/claude/deploy.json +12 -0
- package/adapters/claude/hooks/hooks.json +12 -0
- package/adapters/claude/hooks.json +12 -0
- package/adapters/claude/orchestration.md +17 -0
- package/adapters/codex/agent.toml.hbs +7 -0
- package/adapters/codex/deploy.json +12 -0
- package/adapters/codex/hooks.json +12 -0
- package/adapters/codex/orchestration.md +18 -0
- package/adapters/cursor/deploy.json +12 -0
- package/adapters/cursor/hooks.json +9 -0
- package/adapters/cursor/orchestration.md +17 -0
- package/adapters/opencode/deploy.json +12 -0
- package/adapters/opencode/orchestration.md +18 -0
- package/adapters/opencode/plugin.js +10 -0
- package/cli.mjs +161 -558
- package/core/agents/specline-backend-dev.yaml +45 -0
- package/core/agents/specline-code-reviewer.yaml +67 -0
- package/core/agents/specline-config-dev.yaml +50 -0
- package/core/agents/specline-config-reviewer.yaml +70 -0
- package/core/agents/specline-explore-assistant.yaml +79 -0
- package/core/agents/specline-frontend-dev.yaml +45 -0
- package/core/agents/specline-spec-creator.yaml +58 -0
- package/core/agents/specline-spec-reviewer.yaml +58 -0
- package/core/agents/specline-test-runner.yaml +62 -0
- package/core/agents/specline-test-writer.yaml +67 -0
- package/core/bootstrap/using-specline.md +14 -0
- package/core/gates/pipeline-gate-checks/a1-covers-ref.sh +125 -0
- package/core/gates/pipeline-gate-checks/a2-a3-reverse.sh +171 -0
- package/core/gates/pipeline-gate-checks/c1-exception.sh +71 -0
- package/core/gates/pipeline-gate-checks/c2-vague.sh +60 -0
- package/core/gates/pipeline-gate-checks/common.sh +68 -0
- package/core/gates/pipeline-gate-checks/d1-cycle.sh +149 -0
- package/core/gates/pipeline-gate-checks/d3-type-file.sh +260 -0
- package/core/gates/pipeline-gate.sh +1456 -0
- package/core/hooks/session-start.sh +259 -0
- package/core/skills/specline-apply-change/SKILL.md +197 -0
- package/core/skills/specline-archive-change/SKILL.md +173 -0
- package/core/skills/specline-explore/SKILL.md +504 -0
- package/core/skills/specline-knowledge/SKILL.md +539 -0
- package/core/skills/specline-pipeline/SKILL.md +604 -0
- package/core/skills/specline-pipeline/references/error-recovery-details.md +49 -0
- package/core/skills/specline-pipeline/references/event-log-spec.md +59 -0
- package/core/skills/specline-pipeline/references/pipeline-state-schema.md +87 -0
- package/core/skills/specline-pipeline/templates/subagent-prompts.md +397 -0
- package/core/skills/specline-propose/SKILL.md +186 -0
- package/core/skills/specline-quickfix/SKILL.md +289 -0
- package/core/templates/AGENTS.md.hbs +5 -0
- package/core/templates/specline/config.yaml +15 -0
- package/lib/deploy-claude.mjs +80 -0
- package/lib/deploy-codex.mjs +77 -0
- package/lib/deploy-opencode.mjs +93 -0
- package/lib/deploy.mjs +668 -0
- package/lib/gate.mjs +103 -0
- package/lib/hash.mjs +13 -0
- package/lib/hook.mjs +105 -0
- package/lib/init.mjs +122 -0
- package/lib/lock.mjs +99 -0
- package/lib/merge.mjs +184 -0
- package/lib/paths.mjs +40 -0
- package/lib/platforms.mjs +74 -0
- package/lib/render-agents.mjs +88 -0
- package/lib/render.mjs +126 -0
- package/lib/sync.mjs +253 -0
- package/lib/tty-select.mjs +89 -0
- package/package.json +4 -1
- package/templates/.cursor/README.md +18 -0
- package/templates/.cursor/agents/specline-code-reviewer.md +18 -2
- package/templates/.cursor/agents/specline-spec-creator.md +51 -2
- package/templates/.cursor/agents/specline-test-runner.md +10 -1
- package/templates/.cursor/agents/specline-test-writer.md +58 -7
- package/templates/.cursor/hooks/specline-pipeline-gate-checks/a2-a3-reverse.sh +1 -1
- package/templates/.cursor/hooks/specline-pipeline-gate.sh +118 -0
- package/templates/.cursor/skills/specline-pipeline/SKILL.md +10 -4
- package/templates/.cursor/skills/specline-propose/SKILL.md +3 -3
package/cli.mjs
CHANGED
|
@@ -1,99 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { existsSync, mkdirSync, readdirSync, copyFileSync, writeFileSync, readFileSync } from 'fs';
|
|
4
|
-
import { join, dirname, resolve
|
|
5
|
-
import { fileURLToPath } from 'url';
|
|
6
|
-
import { createHash } from 'crypto';
|
|
4
|
+
import { join, dirname, resolve } from 'path';
|
|
7
5
|
import { get } from 'https';
|
|
8
6
|
import { execSync, spawnSync } from 'child_process';
|
|
9
7
|
import { createInterface } from 'readline/promises';
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
import { PACKAGE_ROOT, TEMPLATES_DIR } from './lib/paths.mjs';
|
|
10
|
+
import { computeFileHash } from './lib/hash.mjs';
|
|
11
|
+
import { readLockFile, writeLockFile } from './lib/lock.mjs';
|
|
12
|
+
import { cliGate } from './lib/gate.mjs';
|
|
13
|
+
import { cliHook } from './lib/hook.mjs';
|
|
14
|
+
import { cliPlatforms } from './lib/platforms.mjs';
|
|
15
|
+
import { runInit, resolvePlatforms, parsePlatformList } from './lib/init.mjs';
|
|
16
|
+
import { runSync } from './lib/sync.mjs';
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
const PKG = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf-8'));
|
|
18
|
+
const PKG = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf-8'));
|
|
16
19
|
const VERSION = PKG.version;
|
|
17
20
|
|
|
18
21
|
// ============================================================
|
|
19
|
-
// 共享工具函数
|
|
22
|
+
// 共享工具函数
|
|
20
23
|
// ============================================================
|
|
21
24
|
|
|
22
|
-
/**
|
|
23
|
-
* 计算内容的 SHA-256 哈希,返回 sha256:<hex> 格式字符串
|
|
24
|
-
*/
|
|
25
|
-
function sha256(content) {
|
|
26
|
-
const hash = createHash('sha256').update(content).digest('hex');
|
|
27
|
-
return `sha256:${hash}`;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* 读取文件内容并计算 SHA-256 哈希
|
|
32
|
-
*/
|
|
33
|
-
function computeFileHash(filePath) {
|
|
34
|
-
const content = readFileSync(filePath);
|
|
35
|
-
return sha256(content);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* 读取 specline/.specline-lock.yaml,手工行解析器
|
|
40
|
-
* 返回 { version, synced_at, files: Map<string, string> } | null
|
|
41
|
-
*/
|
|
42
|
-
function readLockFile(projectDir) {
|
|
43
|
-
const lockPath = join(projectDir, 'specline', '.specline-lock.yaml');
|
|
44
|
-
if (!existsSync(lockPath)) return null;
|
|
45
|
-
|
|
46
|
-
const lines = readFileSync(lockPath, 'utf-8').split('\n');
|
|
47
|
-
const result = { version: '', synced_at: '', files: new Map() };
|
|
48
|
-
let inFiles = false;
|
|
49
|
-
|
|
50
|
-
for (const line of lines) {
|
|
51
|
-
const trimmed = line.trim();
|
|
52
|
-
if (trimmed === '' || trimmed.startsWith('#')) continue;
|
|
53
|
-
|
|
54
|
-
if (trimmed.startsWith('version:')) {
|
|
55
|
-
result.version = trimmed.slice('version:'.length).trim().replace(/^"(.*)"$/, '$1');
|
|
56
|
-
} else if (trimmed.startsWith('synced_at:')) {
|
|
57
|
-
result.synced_at = trimmed.slice('synced_at:'.length).trim().replace(/^"(.*)"$/, '$1');
|
|
58
|
-
} else if (trimmed === 'files:') {
|
|
59
|
-
inFiles = true;
|
|
60
|
-
} else if (inFiles && trimmed.includes(':')) {
|
|
61
|
-
const colonIdx = trimmed.indexOf(':');
|
|
62
|
-
const key = trimmed.slice(0, colonIdx).trim();
|
|
63
|
-
const value = trimmed.slice(colonIdx + 1).trim();
|
|
64
|
-
result.files.set(key, value);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return result;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* 将锁数据序列化为 YAML 格式写入 specline/.specline-lock.yaml
|
|
73
|
-
*/
|
|
74
|
-
function writeLockFile(projectDir, lockData) {
|
|
75
|
-
const lockDir = join(projectDir, 'specline');
|
|
76
|
-
if (!existsSync(lockDir)) {
|
|
77
|
-
mkdirSync(lockDir, { recursive: true });
|
|
78
|
-
}
|
|
79
|
-
const lockPath = join(lockDir, '.specline-lock.yaml');
|
|
80
|
-
const lines = [
|
|
81
|
-
'# Specline Lock File — 自动生成,请勿手动编辑',
|
|
82
|
-
`version: "${lockData.version}"`,
|
|
83
|
-
`synced_at: "${lockData.synced_at}"`,
|
|
84
|
-
'files:',
|
|
85
|
-
];
|
|
86
|
-
for (const [key, value] of lockData.files) {
|
|
87
|
-
lines.push(` ${key}: ${value}`);
|
|
88
|
-
}
|
|
89
|
-
writeFileSync(lockPath, lines.join('\n') + '\n', 'utf-8');
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* 遍历指定目录所有文件,构建锁数据结构
|
|
94
|
-
* rootDir: 要遍历的根目录(必须是目标项目目录,这样 init 后锁哈希与实际文件一致)
|
|
95
|
-
* 返回 { version, synced_at, files: Map<string, string> }
|
|
96
|
-
*/
|
|
97
25
|
function buildLockData(projectDir, rootDir) {
|
|
98
26
|
const files = new Map();
|
|
99
27
|
const walkRoot = rootDir || TEMPLATES_DIR;
|
|
@@ -120,9 +48,6 @@ function buildLockData(projectDir, rootDir) {
|
|
|
120
48
|
};
|
|
121
49
|
}
|
|
122
50
|
|
|
123
|
-
/**
|
|
124
|
-
* 版本号语义比较:返回 -1 (a<b)、0 (a==b)、1 (a>b)
|
|
125
|
-
*/
|
|
126
51
|
function compareVersions(a, b) {
|
|
127
52
|
const aParts = a.split('.').map(Number);
|
|
128
53
|
const bParts = b.split('.').map(Number);
|
|
@@ -135,32 +60,6 @@ function compareVersions(a, b) {
|
|
|
135
60
|
return 0;
|
|
136
61
|
}
|
|
137
62
|
|
|
138
|
-
/**
|
|
139
|
-
* 九态决策树:根据模板哈希、锁记录、项目文件状态,分类文件同步策略
|
|
140
|
-
*/
|
|
141
|
-
function classifyFile(templatePath, templateHash, lockEntry, projectPath) {
|
|
142
|
-
const projectExists = existsSync(projectPath);
|
|
143
|
-
if (!projectExists) return { type: 'NEW', path: templatePath };
|
|
144
|
-
|
|
145
|
-
const projectHash = computeFileHash(projectPath);
|
|
146
|
-
|
|
147
|
-
if (lockEntry) {
|
|
148
|
-
if (projectHash === lockEntry) {
|
|
149
|
-
// PRISTINE
|
|
150
|
-
if (templateHash === lockEntry) return { type: 'UNCHANGED', path: templatePath };
|
|
151
|
-
return { type: 'WILL_UPDATE', path: templatePath };
|
|
152
|
-
} else {
|
|
153
|
-
// MODIFIED
|
|
154
|
-
if (templateHash === lockEntry) return { type: 'MODIFIED_ONLY', path: templatePath };
|
|
155
|
-
return { type: 'CONFLICT', path: templatePath };
|
|
156
|
-
}
|
|
157
|
-
} else {
|
|
158
|
-
// 旧版项目,无 lock 记录
|
|
159
|
-
if (projectHash === templateHash) return { type: 'UNCHANGED', path: templatePath };
|
|
160
|
-
return { type: 'NO_LOCK_CONFLICT', path: templatePath };
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
63
|
// ============================================================
|
|
165
64
|
// 日志输出函数
|
|
166
65
|
// ============================================================
|
|
@@ -197,150 +96,25 @@ function copyDirRecursive(src, dest) {
|
|
|
197
96
|
}
|
|
198
97
|
}
|
|
199
98
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* hooks.json 语义合并:清理所有 specline-* 条目,注入模板最新官方 hook
|
|
206
|
-
*/
|
|
207
|
-
function mergeHooksJson(existingContent, templateContent) {
|
|
208
|
-
let existingObj, templateObj;
|
|
209
|
-
try {
|
|
210
|
-
existingObj = JSON.parse(existingContent);
|
|
211
|
-
} catch {
|
|
212
|
-
warn('hooks.json 解析失败,将使用模板完整替换');
|
|
213
|
-
return templateContent;
|
|
214
|
-
}
|
|
99
|
+
async function askConfirm(question) {
|
|
100
|
+
if (!process.stdin.isTTY) return true;
|
|
101
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
215
102
|
try {
|
|
216
|
-
|
|
103
|
+
const answer = await rl.question(question + ' ');
|
|
104
|
+
const trimmed = answer.trim().toLowerCase();
|
|
105
|
+
return trimmed === '' || trimmed === 'y' || trimmed === 'yes';
|
|
217
106
|
} catch {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
for (const eventName of Object.keys(templateObj.hooks || {})) {
|
|
223
|
-
if (!existingObj.hooks) {
|
|
224
|
-
existingObj.hooks = {};
|
|
225
|
-
}
|
|
226
|
-
if (!existingObj.hooks[eventName]) {
|
|
227
|
-
existingObj.hooks[eventName] = [];
|
|
228
|
-
}
|
|
229
|
-
existingObj.hooks[eventName] = existingObj.hooks[eventName].filter(
|
|
230
|
-
(entry) => !(entry.command || '').includes('specline-')
|
|
231
|
-
);
|
|
232
|
-
existingObj.hooks[eventName] = [
|
|
233
|
-
...templateObj.hooks[eventName],
|
|
234
|
-
...existingObj.hooks[eventName],
|
|
235
|
-
];
|
|
236
|
-
}
|
|
237
|
-
return JSON.stringify(existingObj, null, 2) + '\n';
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function countCustomHooks(hooksObj) {
|
|
241
|
-
let count = 0;
|
|
242
|
-
for (const eventName of Object.keys(hooksObj.hooks || {})) {
|
|
243
|
-
for (const entry of (hooksObj.hooks[eventName] || [])) {
|
|
244
|
-
if (!(entry.command || '').includes('specline-')) count++;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
return count;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* YAML 段落结构
|
|
252
|
-
*/
|
|
253
|
-
function parseYamlSections(content) {
|
|
254
|
-
const lines = content.split('\n');
|
|
255
|
-
const sections = [];
|
|
256
|
-
let currentComments = [];
|
|
257
|
-
let currentKey = null;
|
|
258
|
-
let currentBodyLines = [];
|
|
259
|
-
let inBody = false;
|
|
260
|
-
|
|
261
|
-
function flushSection() {
|
|
262
|
-
if (currentComments.length > 0 || currentBodyLines.length > 0 || currentKey) {
|
|
263
|
-
const bodyStr = currentBodyLines.join('\n');
|
|
264
|
-
// 判定 isEmpty:body 为空、纯注释、或仅声明 key 但无实际值
|
|
265
|
-
const bodyTrimmed = bodyStr.trim();
|
|
266
|
-
const onlyKeyDeclaration = currentKey !== null &&
|
|
267
|
-
currentBodyLines.length === 1 &&
|
|
268
|
-
bodyTrimmed.match(/^\w[\w_-]*\s*:\s*$/) !== null;
|
|
269
|
-
const isEmpty = bodyTrimmed === '' ||
|
|
270
|
-
bodyTrimmed.startsWith('#') ||
|
|
271
|
-
onlyKeyDeclaration;
|
|
272
|
-
sections.push({ key: currentKey, headerComments: [...currentComments], body: bodyStr, isEmpty });
|
|
273
|
-
}
|
|
274
|
-
currentComments = [];
|
|
275
|
-
currentKey = null;
|
|
276
|
-
currentBodyLines = [];
|
|
277
|
-
inBody = false;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
for (const line of lines) {
|
|
281
|
-
const trimmed = line.trim();
|
|
282
|
-
if (trimmed === '') { if (inBody) currentBodyLines.push(line); continue; }
|
|
283
|
-
if (trimmed.startsWith('#')) { if (inBody) currentBodyLines.push(line); else currentComments.push(line); continue; }
|
|
284
|
-
const topKeyMatch = line.match(/^(\w[\w_-]*)\s*:(.*)/);
|
|
285
|
-
if (topKeyMatch && !line.startsWith(' ') && !line.startsWith('\t')) {
|
|
286
|
-
flushSection();
|
|
287
|
-
currentKey = topKeyMatch[1];
|
|
288
|
-
currentBodyLines = [line];
|
|
289
|
-
inBody = true;
|
|
290
|
-
continue;
|
|
291
|
-
}
|
|
292
|
-
if (inBody) currentBodyLines.push(line);
|
|
293
|
-
}
|
|
294
|
-
flushSection();
|
|
295
|
-
return sections;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function findSection(sections, key) {
|
|
299
|
-
return sections.find((s) => s.key === key) || null;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function mergeConfigYaml(existingContent, templateContent) {
|
|
303
|
-
const existingSections = parseYamlSections(existingContent);
|
|
304
|
-
const templateSections = parseYamlSections(templateContent);
|
|
305
|
-
const resultLines = [];
|
|
306
|
-
|
|
307
|
-
for (const tmplSec of templateSections) {
|
|
308
|
-
const existSec = findSection(existingSections, tmplSec.key);
|
|
309
|
-
if (existSec) {
|
|
310
|
-
if (!existSec.isEmpty && existSec.body.trim() !== tmplSec.body.trim()) {
|
|
311
|
-
resultLines.push(...tmplSec.headerComments);
|
|
312
|
-
resultLines.push(existSec.body);
|
|
313
|
-
} else {
|
|
314
|
-
resultLines.push(...tmplSec.headerComments);
|
|
315
|
-
resultLines.push(tmplSec.body);
|
|
316
|
-
}
|
|
317
|
-
resultLines.push('');
|
|
318
|
-
} else if (tmplSec.key !== null) {
|
|
319
|
-
resultLines.push('# 🆕 新增配置段 (specline sync)');
|
|
320
|
-
resultLines.push(...tmplSec.headerComments);
|
|
321
|
-
resultLines.push(tmplSec.body);
|
|
322
|
-
resultLines.push('');
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
for (const existSec of existingSections) {
|
|
327
|
-
if (existSec.key === null) continue;
|
|
328
|
-
if (!findSection(templateSections, existSec.key)) {
|
|
329
|
-
resultLines.push(...existSec.headerComments);
|
|
330
|
-
resultLines.push(existSec.body);
|
|
331
|
-
resultLines.push('');
|
|
332
|
-
}
|
|
107
|
+
return false;
|
|
108
|
+
} finally {
|
|
109
|
+
rl.close();
|
|
333
110
|
}
|
|
334
|
-
return resultLines.join('\n');
|
|
335
111
|
}
|
|
336
112
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
return backupPath;
|
|
341
|
-
}
|
|
113
|
+
// ============================================================
|
|
114
|
+
// 命令实现 — init / sync / update(保持原有逻辑,Task 16/18 再拆)
|
|
115
|
+
// ============================================================
|
|
342
116
|
|
|
343
|
-
function cmd_init(targetPath) {
|
|
117
|
+
async function cmd_init(targetPath, rawArgs) {
|
|
344
118
|
const cwd = process.cwd();
|
|
345
119
|
const target = resolve(cwd, targetPath || '.');
|
|
346
120
|
|
|
@@ -350,79 +124,48 @@ function cmd_init(targetPath) {
|
|
|
350
124
|
}
|
|
351
125
|
|
|
352
126
|
const lockFile = join(target, 'specline', '.specline-lock.yaml');
|
|
353
|
-
const forceMode =
|
|
354
|
-
|
|
355
|
-
if (existsSync(lockFile) && !forceMode) {
|
|
356
|
-
warn('Specline 已在此项目中初始化。使用 --force 强制覆盖。');
|
|
357
|
-
process.exit(0);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// 检测 hooks.json 冲突
|
|
361
|
-
const hooksJsonDest = join(target, '.cursor', 'hooks.json');
|
|
362
|
-
if (existsSync(hooksJsonDest)) {
|
|
363
|
-
const backupPath = hooksJsonDest + '.bak';
|
|
364
|
-
copyFileSync(hooksJsonDest, backupPath);
|
|
365
|
-
warn('已备份原有 hooks.json → .cursor/hooks.json.bak');
|
|
366
|
-
}
|
|
127
|
+
const forceMode = rawArgs.includes('--force') || rawArgs.includes('-f');
|
|
128
|
+
const withShellGuard = rawArgs.includes('--with-shell-guard');
|
|
367
129
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
'
|
|
371
|
-
|
|
372
|
-
'.cursor/hooks',
|
|
373
|
-
'specline/changes/archive',
|
|
374
|
-
'specline/specs',
|
|
375
|
-
];
|
|
376
|
-
|
|
377
|
-
for (const dir of dirs) {
|
|
378
|
-
const fullDir = join(target, dir);
|
|
379
|
-
if (!existsSync(fullDir)) {
|
|
380
|
-
mkdirSync(fullDir, { recursive: true });
|
|
130
|
+
let platformArg;
|
|
131
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
132
|
+
if (rawArgs[i] === '--platform' && rawArgs[i + 1]) {
|
|
133
|
+
platformArg = rawArgs[++i];
|
|
381
134
|
}
|
|
382
135
|
}
|
|
383
136
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
process.exit(1);
|
|
137
|
+
if (existsSync(lockFile) && !forceMode && !platformArg) {
|
|
138
|
+
warn('Specline 已在此项目中初始化。使用 --force 强制覆盖,或使用 --platform 追加平台。');
|
|
139
|
+
process.exit(0);
|
|
388
140
|
}
|
|
389
141
|
|
|
390
|
-
|
|
142
|
+
const platforms = await resolvePlatforms(process.stdin.isTTY, platformArg);
|
|
391
143
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
if (entry.isFile()) count++;
|
|
400
|
-
}
|
|
401
|
-
} catch (_) {}
|
|
402
|
-
return count;
|
|
403
|
-
}
|
|
144
|
+
const result = runInit({
|
|
145
|
+
target,
|
|
146
|
+
platforms,
|
|
147
|
+
withShellGuard,
|
|
148
|
+
version: VERSION,
|
|
149
|
+
force: forceMode,
|
|
150
|
+
});
|
|
404
151
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
152
|
+
if (result.appended && result.appended.length > 0) {
|
|
153
|
+
success(`追加平台:${result.appended.join(', ')}。已有平台不受影响。`);
|
|
154
|
+
} else {
|
|
155
|
+
success('Specline 初始化完成');
|
|
156
|
+
}
|
|
157
|
+
log(`📁 文件: ${result.skills} skills, ${result.agents} agents, ${result.hooks} hooks`);
|
|
158
|
+
if (result.platforms.length > 0) {
|
|
159
|
+
log(`🌐 平台: ${result.platforms.join(', ')}`);
|
|
160
|
+
} else {
|
|
161
|
+
log('🌐 平台: 无(仅创建 specline/ 核心目录)');
|
|
162
|
+
}
|
|
408
163
|
|
|
409
|
-
success('Specline 初始化完成');
|
|
410
|
-
log(`📁 文件: ${skillsCount} skills, ${agentsCount} agents, ${hooksCount} hooks`);
|
|
411
164
|
log('');
|
|
412
|
-
log('🚀
|
|
165
|
+
log('🚀 试试输入:');
|
|
413
166
|
log(' /specline-pipeline "你的第一个需求"');
|
|
414
167
|
log(' /specline-explore');
|
|
415
168
|
|
|
416
|
-
// 生成锁文件
|
|
417
|
-
const lockPath = join(target, 'specline', '.specline-lock.yaml');
|
|
418
|
-
if (existsSync(lockPath) && !forceMode) {
|
|
419
|
-
warn('锁文件已存在,跳过');
|
|
420
|
-
} else {
|
|
421
|
-
const lockData = buildLockData(target, target);
|
|
422
|
-
writeLockFile(target, lockData);
|
|
423
|
-
success('已生成锁文件');
|
|
424
|
-
}
|
|
425
|
-
|
|
426
169
|
process.exit(0);
|
|
427
170
|
}
|
|
428
171
|
|
|
@@ -455,26 +198,7 @@ function fetchLatestVersion() {
|
|
|
455
198
|
});
|
|
456
199
|
}
|
|
457
200
|
|
|
458
|
-
/**
|
|
459
|
-
* 交互式确认提问:回车/Y/y/Yes/yes → true, N/n/No/no → false
|
|
460
|
-
* 非 TTY 环境直接返回 true(无人值守模式)
|
|
461
|
-
*/
|
|
462
|
-
async function askConfirm(question) {
|
|
463
|
-
if (!process.stdin.isTTY) return true;
|
|
464
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
465
|
-
try {
|
|
466
|
-
const answer = await rl.question(question + ' ');
|
|
467
|
-
const trimmed = answer.trim().toLowerCase();
|
|
468
|
-
return trimmed === '' || trimmed === 'y' || trimmed === 'yes';
|
|
469
|
-
} catch {
|
|
470
|
-
return false;
|
|
471
|
-
} finally {
|
|
472
|
-
rl.close();
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
|
|
476
201
|
async function cmd_update() {
|
|
477
|
-
// 1. 从 npm registry 获取最新版本
|
|
478
202
|
let latest;
|
|
479
203
|
try {
|
|
480
204
|
latest = await fetchLatestVersion();
|
|
@@ -492,13 +216,11 @@ async function cmd_update() {
|
|
|
492
216
|
process.exit(0);
|
|
493
217
|
}
|
|
494
218
|
|
|
495
|
-
// 2. 版本比较
|
|
496
219
|
if (compareVersions(VERSION, latest) >= 0) {
|
|
497
220
|
success('已是最新版本 (v' + VERSION + ')');
|
|
498
221
|
process.exit(0);
|
|
499
222
|
}
|
|
500
223
|
|
|
501
|
-
// 3. 交互确认
|
|
502
224
|
log('✨ 新版本可用: v' + latest + '(当前: v' + VERSION + ')');
|
|
503
225
|
|
|
504
226
|
if (!process.stdin.isTTY) {
|
|
@@ -512,7 +234,6 @@ async function cmd_update() {
|
|
|
512
234
|
process.exit(0);
|
|
513
235
|
}
|
|
514
236
|
|
|
515
|
-
// 4. 执行 npm install -g specline@latest
|
|
516
237
|
log('正在升级 specline...');
|
|
517
238
|
try {
|
|
518
239
|
execSync('npm install -g specline@latest', { stdio: 'inherit' });
|
|
@@ -530,7 +251,6 @@ async function cmd_update() {
|
|
|
530
251
|
|
|
531
252
|
success('已升级至 v' + latest);
|
|
532
253
|
|
|
533
|
-
// 5. 检测是否为 specline 项目,询问是否同步模板
|
|
534
254
|
const cwd = process.cwd();
|
|
535
255
|
const lockFile = join(cwd, 'specline', '.specline-lock.yaml');
|
|
536
256
|
if (existsSync(lockFile)) {
|
|
@@ -551,14 +271,12 @@ async function cmd_update() {
|
|
|
551
271
|
process.exit(0);
|
|
552
272
|
}
|
|
553
273
|
|
|
554
|
-
function cmd_sync({ dryRun, targetPath }) {
|
|
274
|
+
function cmd_sync({ dryRun, targetPath, platformArg }) {
|
|
555
275
|
const cwd = process.cwd();
|
|
556
276
|
const target = resolve(cwd, targetPath || '.');
|
|
557
277
|
|
|
558
|
-
// 1. 检查项目是否已初始化
|
|
559
278
|
const lockFile = join(target, 'specline', '.specline-lock.yaml');
|
|
560
279
|
if (!existsSync(lockFile)) {
|
|
561
|
-
// 向后兼容:检查旧版 .specline-config.yaml
|
|
562
280
|
const oldMarker = join(target, '.specline-config.yaml');
|
|
563
281
|
if (existsSync(oldMarker)) {
|
|
564
282
|
warn('检测到旧版项目,正在自动迁移...');
|
|
@@ -571,259 +289,116 @@ function cmd_sync({ dryRun, targetPath }) {
|
|
|
571
289
|
}
|
|
572
290
|
}
|
|
573
291
|
|
|
574
|
-
// 2. 构建上游模板哈希映射
|
|
575
|
-
const upstreamData = buildLockData(target);
|
|
576
|
-
const upstreamFiles = upstreamData.files;
|
|
577
|
-
|
|
578
|
-
// 3. 读取锁文件
|
|
579
292
|
const lockData = readLockFile(target);
|
|
580
|
-
|
|
581
|
-
// 4. 版本校验
|
|
582
293
|
if (lockData && compareVersions(lockData.version, VERSION) > 0) {
|
|
583
|
-
|
|
584
|
-
if (!process.stdin.isTTY) {
|
|
585
|
-
error('非交互式环境,已跳过同步');
|
|
586
|
-
process.exit(1);
|
|
587
|
-
}
|
|
588
|
-
error('锁文件版本高于 CLI,请先更新 CLI');
|
|
294
|
+
error('锁文件版本 (v' + lockData.version + ') 高于 CLI 版本 (v' + VERSION + '),请先更新 CLI');
|
|
589
295
|
process.exit(1);
|
|
590
296
|
}
|
|
591
297
|
|
|
592
|
-
|
|
593
|
-
const allPaths = new Set();
|
|
594
|
-
for (const p of upstreamFiles.keys()) allPaths.add(p);
|
|
595
|
-
if (lockData) {
|
|
596
|
-
for (const p of lockData.files.keys()) {
|
|
597
|
-
if (!upstreamFiles.has(p)) allPaths.add(p);
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
// 6. 分类
|
|
602
|
-
const results = [];
|
|
603
|
-
for (const path of allPaths) {
|
|
604
|
-
const templateHash = upstreamFiles.get(path) || null;
|
|
605
|
-
const lockEntry = lockData ? (lockData.files.get(path) || null) : null;
|
|
606
|
-
const projectPath = join(target, path);
|
|
607
|
-
|
|
608
|
-
if (templateHash === null) {
|
|
609
|
-
results.push({ type: 'UPSTREAM_REMOVED', path });
|
|
610
|
-
} else {
|
|
611
|
-
results.push(classifyFile(path, templateHash, lockEntry, projectPath));
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
// 7. 统计
|
|
616
|
-
const stats = { newCount: 0, updated: 0, conflicted: 0, skippedModified: 0, unchanged: 0, upstreamRemoved: 0 };
|
|
617
|
-
for (const r of results) {
|
|
618
|
-
if (r.type === 'NEW') stats.newCount++;
|
|
619
|
-
else if (r.type === 'WILL_UPDATE') stats.updated++;
|
|
620
|
-
else if (r.type === 'CONFLICT' || r.type === 'NO_LOCK_CONFLICT') stats.conflicted++;
|
|
621
|
-
else if (r.type === 'MODIFIED_ONLY') stats.skippedModified++;
|
|
622
|
-
else if (r.type === 'UPSTREAM_REMOVED') stats.upstreamRemoved++;
|
|
623
|
-
else stats.unchanged++;
|
|
624
|
-
}
|
|
298
|
+
const platforms = platformArg ? parsePlatformList(platformArg) : undefined;
|
|
625
299
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
hooksPlan = { customCount: countCustomHooks(existingObj) };
|
|
640
|
-
} catch {}
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
if (r.path === CONFIG_YAML) {
|
|
644
|
-
log('💡 config.yaml: 用户已修改,保留现有配置不变');
|
|
645
|
-
}
|
|
646
|
-
continue;
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
if (r.path === HOOKS_JSON) {
|
|
650
|
-
hooksPlan = hooksPlan || { customCount: 0 };
|
|
651
|
-
// 读取用户现有 hooks.json,计算自定义 hook 数量
|
|
652
|
-
const projPath = join(target, r.path);
|
|
653
|
-
if (existsSync(projPath) && !hooksPlan.readFromUser) {
|
|
654
|
-
try {
|
|
655
|
-
const existingObj = JSON.parse(readFileSync(projPath, 'utf-8'));
|
|
656
|
-
hooksPlan = { customCount: countCustomHooks(existingObj), readFromUser: true };
|
|
657
|
-
} catch {}
|
|
658
|
-
}
|
|
659
|
-
let tplCount = 0;
|
|
660
|
-
try {
|
|
661
|
-
const tpl = JSON.parse(readFileSync(join(TEMPLATES_DIR, r.path), 'utf-8'));
|
|
662
|
-
for (const ev of Object.keys(tpl.hooks || {})) tplCount += (tpl.hooks[ev] || []).length;
|
|
663
|
-
} catch {}
|
|
664
|
-
log(`🔄 hooks.json 语义合并: 保留 ${hooksPlan.customCount >= 0 ? hooksPlan.customCount : '?'} 个自定义 hook, 更新 ${tplCount} 个官方 hook`);
|
|
665
|
-
continue;
|
|
300
|
+
try {
|
|
301
|
+
const result = runSync(target, { dryRun, platforms });
|
|
302
|
+
|
|
303
|
+
if (dryRun) {
|
|
304
|
+
for (const item of result.plan) {
|
|
305
|
+
if (item.type === 'UNCHANGED' || item.type === 'MODIFIED_ONLY') continue;
|
|
306
|
+
const labels = {
|
|
307
|
+
NEW: '➕ 新增',
|
|
308
|
+
WILL_UPDATE: '🔄 更新',
|
|
309
|
+
CONFLICT: '⚠️ 冲突(将备份后覆盖)',
|
|
310
|
+
UPSTREAM_REMOVED: '🗑️ 上游移除',
|
|
311
|
+
};
|
|
312
|
+
log((labels[item.type] || item.type) + ' ' + item.path);
|
|
666
313
|
}
|
|
667
|
-
|
|
668
|
-
if (
|
|
669
|
-
log('
|
|
670
|
-
|
|
314
|
+
const s = result.stats;
|
|
315
|
+
if (s.newCount === 0 && s.updated === 0 && s.conflicted === 0 && s.upstreamRemoved === 0) {
|
|
316
|
+
log('所有模板文件已是最新,无需同步');
|
|
317
|
+
} else {
|
|
318
|
+
log('\n以上为预览,未实际执行。去掉 --dry-run 以执行同步。');
|
|
671
319
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
log(labels[r.type] + ' ' + r.path);
|
|
675
|
-
}
|
|
676
|
-
if (stats.newCount === 0 && stats.updated === 0 && stats.conflicted === 0
|
|
677
|
-
&& stats.skippedModified === 0 && stats.upstreamRemoved === 0) {
|
|
678
|
-
log('所有模板文件已是最新,无需同步');
|
|
679
|
-
} else {
|
|
680
|
-
log('\n以上为预览,未实际执行。去掉 --dry-run 以执行同步。');
|
|
681
|
-
}
|
|
682
|
-
process.exit(0);
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
// 9. 执行写入
|
|
686
|
-
const newFiles = new Map();
|
|
687
|
-
const HOOKS_JSON = '.cursor/hooks.json';
|
|
688
|
-
const CONFIG_YAML = 'specline/config.yaml';
|
|
689
|
-
const mergeStats = { hooksMerged: false, configUpdated: false, backupsCreated: 0 };
|
|
690
|
-
|
|
691
|
-
for (const r of results) {
|
|
692
|
-
if (r.type === 'UNCHANGED' || r.type === 'MODIFIED_ONLY') {
|
|
693
|
-
const projectPath = join(target, r.path);
|
|
694
|
-
if (existsSync(projectPath)) {
|
|
695
|
-
newFiles.set(r.path, computeFileHash(projectPath));
|
|
320
|
+
if (result.migrated) {
|
|
321
|
+
log('📦 Lock file 将从 v1 迁移至 v2');
|
|
696
322
|
}
|
|
697
|
-
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
if (r.type === 'UPSTREAM_REMOVED') {
|
|
701
|
-
warn('上游已移除:' + r.path);
|
|
702
|
-
continue;
|
|
323
|
+
process.exit(0);
|
|
703
324
|
}
|
|
704
325
|
|
|
705
|
-
const
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
326
|
+
const s = result.stats;
|
|
327
|
+
if (s.newCount === 0 && s.updated === 0 && s.conflicted === 0
|
|
328
|
+
&& s.skippedModified === 0 && s.upstreamRemoved === 0) {
|
|
329
|
+
success('项目模板已是最新,无需同步 (v' + VERSION + ')');
|
|
330
|
+
} else {
|
|
331
|
+
log('📊 同步摘要:');
|
|
332
|
+
log(' ✅ 已新增: ' + s.newCount);
|
|
333
|
+
log(' 🔄 已更新: ' + s.updated);
|
|
334
|
+
log(' ⚠️ 已覆盖(冲突): ' + s.conflicted);
|
|
335
|
+
log(' ⏭️ 已跳过(本地修改): ' + s.skippedModified);
|
|
336
|
+
log(' 🗑️ 上游已移除: ' + s.upstreamRemoved);
|
|
337
|
+
log(' ✨ 锁文件已更新至 v' + VERSION);
|
|
710
338
|
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
// 特殊文件:hooks.json 语义合并
|
|
714
|
-
if (r.path === HOOKS_JSON) {
|
|
715
|
-
const existingContent = existsSync(destPath) ? readFileSync(destPath, 'utf-8') : '{}';
|
|
716
|
-
const templateContent = readFileSync(srcPath, 'utf-8');
|
|
717
|
-
try {
|
|
718
|
-
const merged = mergeHooksJson(existingContent, templateContent);
|
|
719
|
-
writeFileSync(destPath, merged, 'utf-8');
|
|
720
|
-
newFiles.set(r.path, sha256(merged));
|
|
721
|
-
mergeStats.hooksMerged = true;
|
|
722
|
-
} catch {
|
|
723
|
-
warn('hooks.json 合并失败,将保留现有文件');
|
|
724
|
-
newFiles.set(r.path, computeFileHash(destPath));
|
|
725
|
-
}
|
|
726
|
-
continue;
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
// 特殊文件:config.yaml 注释合并
|
|
730
|
-
if (r.path === CONFIG_YAML) {
|
|
731
|
-
const existingContent = existsSync(destPath) ? readFileSync(destPath, 'utf-8') : '';
|
|
732
|
-
const templateContent = readFileSync(srcPath, 'utf-8');
|
|
733
|
-
try {
|
|
734
|
-
const merged = mergeConfigYaml(existingContent, templateContent);
|
|
735
|
-
writeFileSync(destPath, merged, 'utf-8');
|
|
736
|
-
newFiles.set(r.path, sha256(merged));
|
|
737
|
-
mergeStats.configUpdated = true;
|
|
738
|
-
} catch {
|
|
739
|
-
warn('config.yaml 合并失败,将保留现有文件');
|
|
740
|
-
newFiles.set(r.path, computeFileHash(destPath));
|
|
741
|
-
}
|
|
742
|
-
continue;
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
// CONFLICT:备份后覆盖
|
|
746
|
-
if (r.type === 'CONFLICT' || r.type === 'NO_LOCK_CONFLICT') {
|
|
747
|
-
if (existsSync(destPath)) {
|
|
748
|
-
const backupPath = backupBeforeOverwrite(destPath);
|
|
749
|
-
mergeStats.backupsCreated++;
|
|
750
|
-
warn('已覆盖(冲突,备份: ' + basename(backupPath) + '): ' + r.path);
|
|
751
|
-
}
|
|
752
|
-
copyFileSync(srcPath, destPath);
|
|
753
|
-
newFiles.set(r.path, computeFileHash(destPath));
|
|
754
|
-
} else {
|
|
755
|
-
copyFileSync(srcPath, destPath);
|
|
756
|
-
newFiles.set(r.path, computeFileHash(destPath));
|
|
757
|
-
}
|
|
758
|
-
} catch (err) {
|
|
759
|
-
warn(r.path + ' 写入失败:' + err.message);
|
|
760
|
-
if (lockData && lockData.files.has(r.path)) {
|
|
761
|
-
newFiles.set(r.path, lockData.files.get(r.path));
|
|
762
|
-
}
|
|
339
|
+
if (result.migrated) {
|
|
340
|
+
success('Lock file 已从 v1 迁移至 v2 格式');
|
|
763
341
|
}
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
writeLockFile(target, {
|
|
768
|
-
version: VERSION,
|
|
769
|
-
synced_at: new Date().toISOString(),
|
|
770
|
-
files: newFiles,
|
|
771
|
-
});
|
|
772
|
-
|
|
773
|
-
// 11. 输出摘要
|
|
774
|
-
if (stats.newCount === 0 && stats.updated === 0 && stats.conflicted === 0
|
|
775
|
-
&& stats.skippedModified === 0 && stats.upstreamRemoved === 0
|
|
776
|
-
&& !mergeStats.hooksMerged && !mergeStats.configUpdated) {
|
|
777
|
-
success('项目模板已是最新,无需同步 (v' + VERSION + ')');
|
|
778
|
-
} else {
|
|
779
|
-
log('📊 同步摘要:');
|
|
780
|
-
log(' 总模板文件: ' + allPaths.size);
|
|
781
|
-
log(' ✅ 已新增: ' + stats.newCount);
|
|
782
|
-
log(' 🔄 已更新: ' + stats.updated);
|
|
783
|
-
log(' ⚠️ 已覆盖(冲突): ' + stats.conflicted);
|
|
784
|
-
log(' ⏭️ 已跳过(本地修改): ' + stats.skippedModified);
|
|
785
|
-
log(' 🗑️ 上游已移除: ' + stats.upstreamRemoved);
|
|
786
|
-
if (mergeStats.hooksMerged) log(' 🔧 hooks.json: 语义合并完成');
|
|
787
|
-
if (mergeStats.configUpdated) log(' 📝 config.yaml: 注释已更新');
|
|
788
|
-
if (mergeStats.backupsCreated > 0) log(' 💾 创建备份: ' + mergeStats.backupsCreated + ' 个 .orig 文件');
|
|
789
|
-
log(' ✨ 锁文件已更新至 v' + VERSION);
|
|
342
|
+
} catch (err) {
|
|
343
|
+
error(err.message);
|
|
344
|
+
process.exit(1);
|
|
790
345
|
}
|
|
791
346
|
|
|
792
347
|
process.exit(0);
|
|
793
348
|
}
|
|
794
349
|
|
|
350
|
+
// ============================================================
|
|
351
|
+
// 命令路由
|
|
352
|
+
// ============================================================
|
|
353
|
+
|
|
795
354
|
function cmd_version() {
|
|
796
355
|
log(`specline v${VERSION}`);
|
|
797
356
|
process.exit(0);
|
|
798
357
|
}
|
|
799
358
|
|
|
800
|
-
function cmd_help() {
|
|
801
|
-
log(`specline v${VERSION} — Spec-driven AI coding pipeline
|
|
359
|
+
function cmd_help(exitCode = 0) {
|
|
360
|
+
log(`specline v${VERSION} — Spec-driven AI coding pipeline
|
|
802
361
|
|
|
803
362
|
用法:
|
|
804
|
-
specline init [path]
|
|
805
|
-
specline init --
|
|
806
|
-
specline
|
|
807
|
-
specline
|
|
808
|
-
specline --
|
|
809
|
-
specline --
|
|
363
|
+
specline init [path] 在指定路径初始化流水线基础设施
|
|
364
|
+
specline init --platform <list> 指定平台 (cursor,claude,codex,opencode,all,none)
|
|
365
|
+
specline init --force 强制覆盖已有配置
|
|
366
|
+
specline init --with-shell-guard 启用 shell 命令安全防护 hook
|
|
367
|
+
specline sync [--dry-run] [path] 同步项目模板文件到最新版本
|
|
368
|
+
specline sync --platform <list> 只同步指定平台
|
|
369
|
+
specline update 检查 CLI 自身更新(npm registry)
|
|
370
|
+
specline gate <sub> [--change <n>] 运行 Gate 检查(spec/build/lint/list …)
|
|
371
|
+
specline hook <sub> [--platform p] 运行 Hook 脚本(session-start …)
|
|
372
|
+
specline platforms 显示已部署平台列表
|
|
373
|
+
specline --version, -v 显示版本号
|
|
374
|
+
specline --help, -h 显示此帮助信息
|
|
810
375
|
|
|
811
376
|
示例:
|
|
812
|
-
specline init
|
|
813
|
-
specline init
|
|
814
|
-
specline
|
|
815
|
-
|
|
377
|
+
specline init 在当前目录初始化(TTY 下交互选择平台)
|
|
378
|
+
specline init --platform cursor 只部署 Cursor 平台
|
|
379
|
+
specline init --platform all 部署全部 4 个平台
|
|
380
|
+
specline init ./my-project 在指定目录初始化
|
|
381
|
+
specline sync --dry-run 预览模板文件更新
|
|
382
|
+
specline gate spec --change feat 运行 spec gate 检查
|
|
383
|
+
specline hook session-start 运行 session-start hook
|
|
384
|
+
specline platforms 列出已部署平台
|
|
385
|
+
npx specline init 无需全局安装即可使用
|
|
816
386
|
`);
|
|
817
|
-
process.exit(
|
|
387
|
+
process.exit(exitCode);
|
|
818
388
|
}
|
|
819
389
|
|
|
820
|
-
// 入口
|
|
821
390
|
const [,, command, ...args] = process.argv;
|
|
822
391
|
|
|
823
392
|
switch (command) {
|
|
824
393
|
case 'init': {
|
|
825
|
-
const
|
|
826
|
-
|
|
394
|
+
const initFlags = ['--force', '-f', '--with-shell-guard', '--platform'];
|
|
395
|
+
const pathArg = args.filter(a => {
|
|
396
|
+
if (initFlags.includes(a)) return false;
|
|
397
|
+
const prevIdx = args.indexOf(a) - 1;
|
|
398
|
+
if (prevIdx >= 0 && args[prevIdx] === '--platform') return false;
|
|
399
|
+
return true;
|
|
400
|
+
})[0];
|
|
401
|
+
await cmd_init(pathArg, args);
|
|
827
402
|
break;
|
|
828
403
|
}
|
|
829
404
|
case 'update': {
|
|
@@ -832,8 +407,31 @@ switch (command) {
|
|
|
832
407
|
}
|
|
833
408
|
case 'sync': {
|
|
834
409
|
const dryRun = args.includes('--dry-run');
|
|
835
|
-
|
|
836
|
-
|
|
410
|
+
let syncPlatformArg;
|
|
411
|
+
const filteredArgs = [];
|
|
412
|
+
for (let i = 0; i < args.length; i++) {
|
|
413
|
+
if (args[i] === '--platform' && args[i + 1]) {
|
|
414
|
+
syncPlatformArg = args[++i];
|
|
415
|
+
} else if (args[i] !== '--dry-run') {
|
|
416
|
+
filteredArgs.push(args[i]);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
cmd_sync({ dryRun, targetPath: filteredArgs[0], platformArg: syncPlatformArg });
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
case 'gate': {
|
|
423
|
+
const exitCode = cliGate(args);
|
|
424
|
+
process.exit(exitCode);
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
case 'hook': {
|
|
428
|
+
const exitCode = cliHook(args);
|
|
429
|
+
process.exit(exitCode);
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
case 'platforms': {
|
|
433
|
+
const exitCode = cliPlatforms();
|
|
434
|
+
process.exit(exitCode);
|
|
837
435
|
break;
|
|
838
436
|
}
|
|
839
437
|
case '--version':
|
|
@@ -842,7 +440,12 @@ switch (command) {
|
|
|
842
440
|
break;
|
|
843
441
|
case '--help':
|
|
844
442
|
case '-h':
|
|
845
|
-
default:
|
|
846
443
|
cmd_help();
|
|
847
444
|
break;
|
|
445
|
+
default:
|
|
446
|
+
if (command) {
|
|
447
|
+
console.error(`\x1b[31m未知命令: ${command}\x1b[0m\n`);
|
|
448
|
+
}
|
|
449
|
+
cmd_help(command ? 1 : 0);
|
|
450
|
+
break;
|
|
848
451
|
}
|