sillyspec 2.4.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/.claude/commands/sillyspec/archive.md +63 -0
- package/.claude/commands/sillyspec/brainstorm.md +463 -0
- package/.claude/commands/sillyspec/continue.md +44 -0
- package/.claude/commands/sillyspec/execute.md +255 -0
- package/.claude/commands/sillyspec/explore.md +88 -0
- package/.claude/commands/sillyspec/export.md +53 -0
- package/.claude/commands/sillyspec/init.md +166 -0
- package/.claude/commands/sillyspec/plan.md +238 -0
- package/.claude/commands/sillyspec/propose.md +234 -0
- package/.claude/commands/sillyspec/quick.md +62 -0
- package/.claude/commands/sillyspec/resume.md +100 -0
- package/.claude/commands/sillyspec/scan.md +672 -0
- package/.claude/commands/sillyspec/status.md +122 -0
- package/.claude/commands/sillyspec/verify.md +141 -0
- package/.claude/commands/sillyspec/workspace.md +122 -0
- package/README.md +158 -0
- package/SKILL.md +46 -0
- package/adapters/adapters.sh +172 -0
- package/bin/sillyspec.js +2 -0
- package/commands/sillyspec/archive.md +62 -0
- package/commands/sillyspec/brainstorm.md +462 -0
- package/commands/sillyspec/continue.md +41 -0
- package/commands/sillyspec/execute.md +254 -0
- package/commands/sillyspec/explore.md +85 -0
- package/commands/sillyspec/export.md +51 -0
- package/commands/sillyspec/init.md +163 -0
- package/commands/sillyspec/plan.md +237 -0
- package/commands/sillyspec/propose.md +233 -0
- package/commands/sillyspec/quick.md +59 -0
- package/commands/sillyspec/resume.md +99 -0
- package/commands/sillyspec/scan.md +671 -0
- package/commands/sillyspec/status.md +119 -0
- package/commands/sillyspec/verify.md +140 -0
- package/commands/sillyspec/workspace.md +120 -0
- package/package.json +14 -0
- package/scripts/init.sh +2 -0
- package/scripts/install.ps1 +316 -0
- package/scripts/scan-preprocess.sh +378 -0
- package/scripts/validate-all.sh +50 -0
- package/scripts/validate-plan.sh +44 -0
- package/scripts/validate-proposal.sh +87 -0
- package/scripts/validate-scan.sh +90 -0
- package/src/index.js +560 -0
- package/src/init.js +269 -0
- package/templates/archive.md +58 -0
- package/templates/brainstorm.md +458 -0
- package/templates/continue.md +39 -0
- package/templates/execute.md +250 -0
- package/templates/explore.md +83 -0
- package/templates/export.md +48 -0
- package/templates/init.md +161 -0
- package/templates/plan.md +233 -0
- package/templates/propose.md +229 -0
- package/templates/quick.md +57 -0
- package/templates/resume.md +95 -0
- package/templates/scan.md +667 -0
- package/templates/status.md +117 -0
- package/templates/verify.md +136 -0
- package/templates/workspace.md +117 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SillySpec CLI — 流程状态机
|
|
5
|
+
*
|
|
6
|
+
* 核心理念:AI 不自己推断下一步该做什么,而是调用 CLI 获取状态和指令。
|
|
7
|
+
* CLI 是调度器,AI 是执行者。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
|
|
11
|
+
import { join, resolve } from 'path';
|
|
12
|
+
import { cmdInit } from './init.js';
|
|
13
|
+
|
|
14
|
+
const SILLYSPEC_DIR = '.sillyspec';
|
|
15
|
+
const CODEBASE_DIR = `${SILLYSPEC_DIR}/codebase`;
|
|
16
|
+
const CHANGES_DIR = `${SILLYSPEC_DIR}/changes`;
|
|
17
|
+
|
|
18
|
+
// ── SillySpec 生命周期定义 ──
|
|
19
|
+
// 每个阶段需要的文件,只有全部齐全才算该阶段完成
|
|
20
|
+
|
|
21
|
+
const LIFECYCLE = {
|
|
22
|
+
scan: {
|
|
23
|
+
label: '扫描',
|
|
24
|
+
files: [
|
|
25
|
+
{ path: `${CODEBASE_DIR}/STACK.md`, label: '技术栈' },
|
|
26
|
+
{ path: `${CODEBASE_DIR}/STRUCTURE.md`, label: '目录结构' },
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
'scan:deep': {
|
|
30
|
+
label: '深度扫描',
|
|
31
|
+
files: [
|
|
32
|
+
{ path: `${CODEBASE_DIR}/STACK.md`, label: '技术栈' },
|
|
33
|
+
{ path: `${CODEBASE_DIR}/ARCHITECTURE.md`, label: '架构' },
|
|
34
|
+
{ path: `${CODEBASE_DIR}/STRUCTURE.md`, label: '目录结构' },
|
|
35
|
+
{ path: `${CODEBASE_DIR}/CONVENTIONS.md`, label: '代码约定' },
|
|
36
|
+
{ path: `${CODEBASE_DIR}/INTEGRATIONS.md`, label: '集成' },
|
|
37
|
+
{ path: `${CODEBASE_DIR}/TESTING.md`, label: '测试' },
|
|
38
|
+
{ path: `${CODEBASE_DIR}/CONCERNS.md`, label: '技术债务' },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
brainstorm: {
|
|
42
|
+
label: '需求探索',
|
|
43
|
+
// brainstorm 生成的设计文档在 specs/ 下,文件名不固定
|
|
44
|
+
detect: (dir) => {
|
|
45
|
+
const specsDir = `${dir}/${SILLYSPEC_DIR}/specs`;
|
|
46
|
+
if (!existsSync(specsDir)) return false;
|
|
47
|
+
const files = readdirSync(specsDir).filter(f => f.endsWith('.md'));
|
|
48
|
+
return files.length > 0;
|
|
49
|
+
},
|
|
50
|
+
detectLabel: 'specs/ 下的设计文档',
|
|
51
|
+
},
|
|
52
|
+
propose: {
|
|
53
|
+
label: '生成规范',
|
|
54
|
+
// 需要 changes/<name>/proposal.md
|
|
55
|
+
detect: (dir) => {
|
|
56
|
+
const changesDir = `${dir}/${CHANGES_DIR}`;
|
|
57
|
+
if (!existsSync(changesDir)) return false;
|
|
58
|
+
const changes = readdirSync(changesDir).filter(d => {
|
|
59
|
+
const p = join(changesDir, d);
|
|
60
|
+
return statSync(p).isDirectory() && d !== 'archive';
|
|
61
|
+
});
|
|
62
|
+
return changes.some(c => existsSync(join(changesDir, c, 'proposal.md')));
|
|
63
|
+
},
|
|
64
|
+
detectLabel: 'changes/<name>/proposal.md',
|
|
65
|
+
},
|
|
66
|
+
plan: {
|
|
67
|
+
label: '编写计划',
|
|
68
|
+
detect: (dir) => {
|
|
69
|
+
const changesDir = `${dir}/${CHANGES_DIR}`;
|
|
70
|
+
if (!existsSync(changesDir)) return false;
|
|
71
|
+
const changes = readdirSync(changesDir).filter(d => {
|
|
72
|
+
const p = join(changesDir, d);
|
|
73
|
+
return statSync(p).isDirectory() && d !== 'archive';
|
|
74
|
+
});
|
|
75
|
+
return changes.some(c => existsSync(join(changesDir, c, 'tasks.md')));
|
|
76
|
+
},
|
|
77
|
+
detectLabel: 'changes/<name>/tasks.md',
|
|
78
|
+
},
|
|
79
|
+
execute: {
|
|
80
|
+
label: '执行实现',
|
|
81
|
+
// 有 tasks.md 且 checkbox 未全部完成
|
|
82
|
+
detect: (dir) => {
|
|
83
|
+
const changesDir = `${dir}/${CHANGES_DIR}`;
|
|
84
|
+
if (!existsSync(changesDir)) return false;
|
|
85
|
+
const changes = readdirSync(changesDir).filter(d => {
|
|
86
|
+
const p = join(changesDir, d);
|
|
87
|
+
return statSync(p).isDirectory() && d !== 'archive';
|
|
88
|
+
});
|
|
89
|
+
return changes.some(c => {
|
|
90
|
+
const tasksPath = join(changesDir, c, 'tasks.md');
|
|
91
|
+
if (!existsSync(tasksPath)) return false;
|
|
92
|
+
const content = readFileSync(tasksPath, 'utf8');
|
|
93
|
+
// 有未完成的 checkbox (- [ ])
|
|
94
|
+
const unchecked = (content.match(/- \[ \]/g) || []).length;
|
|
95
|
+
return unchecked > 0;
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
detectLabel: 'tasks.md 有未完成项',
|
|
99
|
+
},
|
|
100
|
+
verify: {
|
|
101
|
+
label: '验证实现',
|
|
102
|
+
// tasks.md 全部完成但没有归档
|
|
103
|
+
detect: (dir) => {
|
|
104
|
+
const changesDir = `${dir}/${CHANGES_DIR}`;
|
|
105
|
+
if (!existsSync(changesDir)) return false;
|
|
106
|
+
const changes = readdirSync(changesDir).filter(d => {
|
|
107
|
+
const p = join(changesDir, d);
|
|
108
|
+
return statSync(p).isDirectory() && d !== 'archive';
|
|
109
|
+
});
|
|
110
|
+
return changes.some(c => {
|
|
111
|
+
const tasksPath = join(changesDir, c, 'tasks.md');
|
|
112
|
+
if (!existsSync(tasksPath)) return false;
|
|
113
|
+
const content = readFileSync(tasksPath, 'utf8');
|
|
114
|
+
const unchecked = (content.match(/- \[ \]/g) || []).length;
|
|
115
|
+
const checked = (content.match(/- \[x\]/gi) || []).length;
|
|
116
|
+
return unchecked === 0 && checked > 0;
|
|
117
|
+
});
|
|
118
|
+
},
|
|
119
|
+
detectLabel: 'tasks.md 全部完成',
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// ── 工具函数 ──
|
|
124
|
+
|
|
125
|
+
function getLatestChange(dir) {
|
|
126
|
+
const changesDir = join(dir, CHANGES_DIR);
|
|
127
|
+
if (!existsSync(changesDir)) return null;
|
|
128
|
+
|
|
129
|
+
const changes = readdirSync(changesDir)
|
|
130
|
+
.filter(d => {
|
|
131
|
+
const p = join(changesDir, d);
|
|
132
|
+
return statSync(p).isDirectory() && d !== 'archive';
|
|
133
|
+
})
|
|
134
|
+
.sort((a, b) => {
|
|
135
|
+
const sa = statSync(join(changesDir, a));
|
|
136
|
+
const sb = statSync(join(changesDir, b));
|
|
137
|
+
return sb.mtimeMs - sa.mtimeMs;
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return changes[0] || null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function checkChangeFiles(dir, changeName) {
|
|
144
|
+
const changeDir = join(dir, CHANGES_DIR, changeName);
|
|
145
|
+
if (!existsSync(changeDir)) return {};
|
|
146
|
+
|
|
147
|
+
const checks = {
|
|
148
|
+
proposal: existsSync(join(changeDir, 'proposal.md')),
|
|
149
|
+
design: existsSync(join(changeDir, 'design.md')),
|
|
150
|
+
tasks: existsSync(join(changeDir, 'tasks.md')),
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// 统计 tasks 完成情况
|
|
154
|
+
const tasksPath = join(changeDir, 'tasks.md');
|
|
155
|
+
if (checks.tasks) {
|
|
156
|
+
const content = readFileSync(tasksPath, 'utf8');
|
|
157
|
+
checks.totalTasks = (content.match(/- \[[ x]\]/gi) || []).length;
|
|
158
|
+
checks.completedTasks = (content.match(/- \[x\]/gi) || []).length;
|
|
159
|
+
checks.pendingTasks = (content.match(/- \[ \]/g) || []).length;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return checks;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function checkCodebaseFiles(dir) {
|
|
166
|
+
const files = {};
|
|
167
|
+
const allFiles = [
|
|
168
|
+
'STACK.md', 'ARCHITECTURE.md', 'STRUCTURE.md',
|
|
169
|
+
'CONVENTIONS.md', 'INTEGRATIONS.md', 'TESTING.md',
|
|
170
|
+
'CONCERNS.md', 'PROJECT.md', 'SCAN-RAW.md',
|
|
171
|
+
];
|
|
172
|
+
for (const f of allFiles) {
|
|
173
|
+
const p = join(dir, CODEBASE_DIR, f);
|
|
174
|
+
files[f] = existsSync(p);
|
|
175
|
+
}
|
|
176
|
+
return files;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── 确定当前阶段 ──
|
|
180
|
+
|
|
181
|
+
function determinePhase(dir) {
|
|
182
|
+
const codebase = checkCodebaseFiles(dir);
|
|
183
|
+
const latestChange = getLatestChange(dir);
|
|
184
|
+
const changeFiles = latestChange ? checkChangeFiles(dir, latestChange) : {};
|
|
185
|
+
|
|
186
|
+
// 检查大模块模式(MASTER.md)
|
|
187
|
+
let masterMode = false;
|
|
188
|
+
let masterChange = null;
|
|
189
|
+
let stages = {};
|
|
190
|
+
if (latestChange) {
|
|
191
|
+
const masterPath = join(dir, CHANGES_DIR, latestChange, 'MASTER.md');
|
|
192
|
+
if (existsSync(masterPath)) {
|
|
193
|
+
masterMode = true;
|
|
194
|
+
masterChange = latestChange;
|
|
195
|
+
const stagesDir = join(dir, CHANGES_DIR, latestChange, 'stages');
|
|
196
|
+
if (existsSync(stagesDir)) {
|
|
197
|
+
for (const stage of readdirSync(stagesDir)) {
|
|
198
|
+
const stageDir = join(stagesDir, stage);
|
|
199
|
+
if (statSync(stageDir).isDirectory()) {
|
|
200
|
+
const stageFiles = checkChangeFiles(dir, join(latestChange, 'stages', stage));
|
|
201
|
+
stages[stage] = stageFiles;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 检查深度扫描是否做过
|
|
209
|
+
const hasDeepScan = codebase['ARCHITECTURE.md'] && codebase['CONVENTIONS.md'];
|
|
210
|
+
|
|
211
|
+
// 检查快速扫描
|
|
212
|
+
const hasQuickScan = codebase['STACK.md'] && codebase['STRUCTURE.md'];
|
|
213
|
+
|
|
214
|
+
// 检查扫描中断
|
|
215
|
+
if (existsSync(join(dir, CODEBASE_DIR, 'SCAN-RAW.md')) && !hasDeepScan) {
|
|
216
|
+
// 有预处理数据但文档不全,说明深度扫描中断
|
|
217
|
+
const missingDeep = Object.entries(codebase)
|
|
218
|
+
.filter(([f, exists]) => ['ARCHITECTURE.md','CONVENTIONS.md','INTEGRATIONS.md','TESTING.md','CONCERNS.md'].includes(f) && !exists)
|
|
219
|
+
.map(([f]) => f);
|
|
220
|
+
return {
|
|
221
|
+
phase: 'scan:resume',
|
|
222
|
+
label: '深度扫描(中断恢复)',
|
|
223
|
+
command: '/sillyspec:scan --deep',
|
|
224
|
+
details: { missingDocs: missingDeep },
|
|
225
|
+
masterMode: false,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (hasQuickScan && !hasDeepScan) {
|
|
230
|
+
return {
|
|
231
|
+
phase: 'scan:quick_done',
|
|
232
|
+
label: '快速扫描完成',
|
|
233
|
+
command: '/sillyspec:scan --deep',
|
|
234
|
+
suggestion: '快速扫描已完成,可以用深度扫描获取更详细的分析',
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!hasQuickScan) {
|
|
239
|
+
return {
|
|
240
|
+
phase: 'init',
|
|
241
|
+
label: '未开始',
|
|
242
|
+
command: '/sillyspec:scan',
|
|
243
|
+
suggestion: '棕地项目从这里开始',
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 已扫描,检查是否做过 brainstorm
|
|
248
|
+
const specsDir = join(dir, SILLYSPEC_DIR, 'specs');
|
|
249
|
+
let hasBrainstorm = false;
|
|
250
|
+
let designDocs = [];
|
|
251
|
+
if (existsSync(specsDir)) {
|
|
252
|
+
designDocs = readdirSync(specsDir).filter(f => f.endsWith('.md'));
|
|
253
|
+
hasBrainstorm = designDocs.length > 0;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (!hasBrainstorm) {
|
|
257
|
+
return {
|
|
258
|
+
phase: 'brainstorm',
|
|
259
|
+
label: '需求探索',
|
|
260
|
+
command: '/sillyspec:brainstorm',
|
|
261
|
+
suggestion: '扫描已完成,开始头脑风暴',
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// 有 brainstorm,检查变更
|
|
266
|
+
if (!latestChange) {
|
|
267
|
+
return {
|
|
268
|
+
phase: 'propose',
|
|
269
|
+
label: '生成规范',
|
|
270
|
+
command: '/sillyspec:propose',
|
|
271
|
+
suggestion: '头脑风暴完成,开始生成结构化规范',
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 大模块模式
|
|
276
|
+
if (masterMode) {
|
|
277
|
+
return {
|
|
278
|
+
phase: 'master',
|
|
279
|
+
label: '大模块执行中',
|
|
280
|
+
masterChange,
|
|
281
|
+
stages,
|
|
282
|
+
command: '/sillyspec:resume',
|
|
283
|
+
suggestion: '大模块模式,各阶段独立生命周期',
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// 普通变更
|
|
288
|
+
if (!changeFiles.proposal) {
|
|
289
|
+
return {
|
|
290
|
+
phase: 'propose',
|
|
291
|
+
label: '生成规范',
|
|
292
|
+
latestChange,
|
|
293
|
+
command: `/sillyspec:propose ${latestChange}`,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (!changeFiles.design) {
|
|
298
|
+
return {
|
|
299
|
+
phase: 'plan',
|
|
300
|
+
label: '编写计划',
|
|
301
|
+
latestChange,
|
|
302
|
+
command: `/sillyspec:plan ${latestChange}`,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!changeFiles.tasks) {
|
|
307
|
+
return {
|
|
308
|
+
phase: 'plan',
|
|
309
|
+
label: '编写计划(生成任务)',
|
|
310
|
+
latestChange,
|
|
311
|
+
command: `/sillyspec:plan ${latestChange}`,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// 有 tasks,检查完成情况
|
|
316
|
+
if (changeFiles.pendingTasks > 0) {
|
|
317
|
+
return {
|
|
318
|
+
phase: 'execute',
|
|
319
|
+
label: '执行实现',
|
|
320
|
+
latestChange,
|
|
321
|
+
progress: {
|
|
322
|
+
completed: changeFiles.completedTasks,
|
|
323
|
+
total: changeFiles.totalTasks,
|
|
324
|
+
pending: changeFiles.pendingTasks,
|
|
325
|
+
},
|
|
326
|
+
command: `/sillyspec:execute ${latestChange}`,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// tasks 全部完成
|
|
331
|
+
return {
|
|
332
|
+
phase: 'verify',
|
|
333
|
+
label: '验证实现',
|
|
334
|
+
latestChange,
|
|
335
|
+
command: `/sillyspec:verify ${latestChange}`,
|
|
336
|
+
suggestion: '所有任务已完成,验证后可归档',
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ── 命令实现 ──
|
|
341
|
+
|
|
342
|
+
function cmdStatus(dir, options = {}) {
|
|
343
|
+
const phase = determinePhase(dir);
|
|
344
|
+
const codebase = checkCodebaseFiles(dir);
|
|
345
|
+
const latestChange = getLatestChange(dir);
|
|
346
|
+
|
|
347
|
+
if (options.json) {
|
|
348
|
+
const output = {
|
|
349
|
+
phase: phase.phase,
|
|
350
|
+
label: phase.label,
|
|
351
|
+
command: phase.command,
|
|
352
|
+
suggestion: phase.suggestion || null,
|
|
353
|
+
currentChange: phase.latestChange || null,
|
|
354
|
+
codebase: codebase,
|
|
355
|
+
masterMode: phase.masterMode || false,
|
|
356
|
+
...(phase.masterMode ? { masterChange: phase.masterChange, stages: phase.stages } : {}),
|
|
357
|
+
...(phase.progress ? { progress: phase.progress } : {}),
|
|
358
|
+
...(phase.details ? { details: phase.details } : {}),
|
|
359
|
+
};
|
|
360
|
+
console.log(JSON.stringify(output, null, 2));
|
|
361
|
+
} else {
|
|
362
|
+
// 人类可读
|
|
363
|
+
console.log('');
|
|
364
|
+
console.log(`📋 当前阶段: ${phase.label}`);
|
|
365
|
+
console.log(`📦 当前变更: ${phase.latestChange || '无'}`);
|
|
366
|
+
|
|
367
|
+
if (phase.progress) {
|
|
368
|
+
const pct = Math.round(phase.progress.completed / phase.progress.total * 100);
|
|
369
|
+
console.log(`📊 进度: ${phase.progress.completed}/${phase.progress.total} (${pct}%)`);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
console.log(`➡️ 下一步: ${phase.command}`);
|
|
373
|
+
|
|
374
|
+
if (phase.suggestion) {
|
|
375
|
+
console.log(`💡 ${phase.suggestion}`);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// codebase 文件概览
|
|
379
|
+
const codebaseCount = Object.values(codebase).filter(Boolean).length;
|
|
380
|
+
console.log(``);
|
|
381
|
+
console.log(`🗂️ codebase 文档: ${codebaseCount}/9`);
|
|
382
|
+
|
|
383
|
+
// 大模块信息
|
|
384
|
+
if (phase.masterMode && phase.stages) {
|
|
385
|
+
console.log('');
|
|
386
|
+
console.log('🏗️ 大模块阶段:');
|
|
387
|
+
for (const [name, files] of Object.entries(phase.stages)) {
|
|
388
|
+
const hasTasks = files.tasks;
|
|
389
|
+
const pending = files.pendingTasks || 0;
|
|
390
|
+
const total = files.totalTasks || 0;
|
|
391
|
+
const status = !hasTasks ? '⬜' : pending > 0 ? '🔄' : '✅';
|
|
392
|
+
console.log(` ${status} ${name}: ${hasTasks ? `${total - pending}/${total}` : '未计划'}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function cmdNext(dir, options = {}) {
|
|
399
|
+
const phase = determinePhase(dir);
|
|
400
|
+
|
|
401
|
+
if (options.json) {
|
|
402
|
+
const output = {
|
|
403
|
+
phase: phase.phase,
|
|
404
|
+
command: phase.command,
|
|
405
|
+
reason: phase.suggestion || `当前处于 ${phase.label} 阶段`,
|
|
406
|
+
...(phase.details ? { missingDocs: phase.details.missingDocs } : {}),
|
|
407
|
+
};
|
|
408
|
+
console.log(JSON.stringify(output, null, 2));
|
|
409
|
+
} else {
|
|
410
|
+
console.log(phase.command);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function cmdCheck(dir, options = {}) {
|
|
415
|
+
// 只检查文件存在性,用于断点续扫
|
|
416
|
+
const codebase = checkCodebaseFiles(dir);
|
|
417
|
+
const deepFiles = ['ARCHITECTURE.md','CONVENTIONS.md','INTEGRATIONS.md','TESTING.md','CONCERNS.md'];
|
|
418
|
+
|
|
419
|
+
const result = {
|
|
420
|
+
quickScan: {
|
|
421
|
+
STACK: codebase['STACK.md'],
|
|
422
|
+
STRUCTURE: codebase['STRUCTURE.md'],
|
|
423
|
+
done: codebase['STACK.md'] && codebase['STRUCTURE.md'],
|
|
424
|
+
},
|
|
425
|
+
deepScan: {},
|
|
426
|
+
allDone: true,
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
for (const f of deepFiles) {
|
|
430
|
+
const name = f.replace('.md', '');
|
|
431
|
+
result.deepScan[name] = codebase[f];
|
|
432
|
+
if (!codebase[f]) result.allDone = false;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (!result.quickScan.done) result.allDone = false;
|
|
436
|
+
|
|
437
|
+
// 检查误放文件
|
|
438
|
+
const misplaced = [];
|
|
439
|
+
const docNames = ['ARCHITECTURE.md','STACK.md','STRUCTURE.md','CONVENTIONS.md','INTEGRATIONS.md','TESTING.md','CONCERNS.md','PROJECT.md','SCAN-RAW.md'];
|
|
440
|
+
for (const name of docNames) {
|
|
441
|
+
// 检查项目根目录
|
|
442
|
+
if (existsSync(join(dir, name))) {
|
|
443
|
+
misplaced.push({ file: name, location: './', shouldBe: `${CODEBASE_DIR}/` });
|
|
444
|
+
}
|
|
445
|
+
// 检查 .sillyspec/ 但不在 codebase/ 下
|
|
446
|
+
if (existsSync(join(dir, SILLYSPEC_DIR, name))) {
|
|
447
|
+
misplaced.push({ file: name, location: `${SILLYSPEC_DIR}/`, shouldBe: `${CODEBASE_DIR}/` });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
result.misplaced = misplaced;
|
|
451
|
+
|
|
452
|
+
if (options.json) {
|
|
453
|
+
console.log(JSON.stringify(result, null, 2));
|
|
454
|
+
} else {
|
|
455
|
+
console.log('');
|
|
456
|
+
console.log('📊 快速扫描:');
|
|
457
|
+
console.log(` ${result.quickScan.STACK ? '✅' : '⬜'} STACK.md`);
|
|
458
|
+
console.log(` ${result.quickScan.STRUCTURE ? '✅' : '⬜'} STRUCTURE.md`);
|
|
459
|
+
console.log('');
|
|
460
|
+
console.log('📊 深度扫描:');
|
|
461
|
+
for (const [name, exists] of Object.entries(result.deepScan)) {
|
|
462
|
+
console.log(` ${exists ? '✅' : '⬜'} ${name}.md`);
|
|
463
|
+
}
|
|
464
|
+
if (result.misplaced.length > 0) {
|
|
465
|
+
console.log('');
|
|
466
|
+
console.log('❌ 误放文件:');
|
|
467
|
+
for (const m of result.misplaced) {
|
|
468
|
+
console.log(` ${m.file} 在 ${m.location},应在 ${m.shouldBe}`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ── CLI 入口 ──
|
|
475
|
+
|
|
476
|
+
function printUsage() {
|
|
477
|
+
console.log(`
|
|
478
|
+
SillySpec CLI — 流程状态机
|
|
479
|
+
|
|
480
|
+
用法:
|
|
481
|
+
sillyspec status [--json] 显示当前项目状态
|
|
482
|
+
sillyspec next [--json] 显示下一步该执行的命令
|
|
483
|
+
sillyspec check [--json] 检查文档完整性和路径
|
|
484
|
+
sillyspec init 初始化 SillySpec(安装到各工具)
|
|
485
|
+
[--tool <name>] 只安装指定工具
|
|
486
|
+
[--workspace] 工作区模式
|
|
487
|
+
[--dir <path>] 指定目录
|
|
488
|
+
|
|
489
|
+
选项:
|
|
490
|
+
--json 输出 JSON(给 AI 程序化读取)
|
|
491
|
+
--dir <path> 指定项目目录(默认当前目录)
|
|
492
|
+
|
|
493
|
+
示例:
|
|
494
|
+
sillyspec status
|
|
495
|
+
sillyspec status --json
|
|
496
|
+
sillyspec next --json
|
|
497
|
+
sillyspec check
|
|
498
|
+
`);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function main() {
|
|
502
|
+
const args = process.argv.slice(2);
|
|
503
|
+
|
|
504
|
+
if (args.length === 0 || args[0] === 'help' || args[0] === '--help' || args[0] === '-h') {
|
|
505
|
+
printUsage();
|
|
506
|
+
process.exit(0);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// 解析全局选项
|
|
510
|
+
let json = false;
|
|
511
|
+
let targetDir = process.cwd();
|
|
512
|
+
let tool = null;
|
|
513
|
+
let workspace = false;
|
|
514
|
+
const filteredArgs = [];
|
|
515
|
+
|
|
516
|
+
for (let i = 0; i < args.length; i++) {
|
|
517
|
+
if (args[i] === '--json') {
|
|
518
|
+
json = true;
|
|
519
|
+
} else if (args[i] === '--dir' && args[i + 1]) {
|
|
520
|
+
targetDir = resolve(args[i + 1]);
|
|
521
|
+
i++;
|
|
522
|
+
} else if (args[i] === '--tool' && args[i + 1]) {
|
|
523
|
+
tool = args[i + 1];
|
|
524
|
+
i++;
|
|
525
|
+
} else if (args[i] === '--workspace' || args[i] === '-w') {
|
|
526
|
+
workspace = true;
|
|
527
|
+
} else {
|
|
528
|
+
filteredArgs.push(args[i]);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const command = filteredArgs[0];
|
|
533
|
+
const dir = targetDir;
|
|
534
|
+
|
|
535
|
+
if (!existsSync(dir)) {
|
|
536
|
+
console.error(`❌ 目录不存在: ${dir}`);
|
|
537
|
+
process.exit(1);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
switch (command) {
|
|
541
|
+
case 'status':
|
|
542
|
+
cmdStatus(dir, { json });
|
|
543
|
+
break;
|
|
544
|
+
case 'next':
|
|
545
|
+
cmdNext(dir, { json });
|
|
546
|
+
break;
|
|
547
|
+
case 'check':
|
|
548
|
+
cmdCheck(dir, { json });
|
|
549
|
+
break;
|
|
550
|
+
case 'init':
|
|
551
|
+
cmdInit(dir, { tool, workspace });
|
|
552
|
+
break;
|
|
553
|
+
default:
|
|
554
|
+
console.error(`❌ 未知命令: ${command}`);
|
|
555
|
+
printUsage();
|
|
556
|
+
process.exit(1);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
main();
|