sillyspec 3.7.7 → 3.7.9
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/.sillyspec/changes/dashboard/design.md +219 -0
- package/.sillyspec/plans/2026-04-05-dashboard.md +737 -0
- package/.sillyspec/specs/2026-04-05-dashboard-design.md +206 -0
- package/bin/sillyspec.js +0 -0
- package/package.json +1 -1
- package/packages/dashboard/dist/assets/index-Bh-GPjKY.css +1 -0
- package/packages/dashboard/dist/assets/index-CrCn5Gg6.js +17 -0
- package/packages/dashboard/dist/index.html +16 -0
- package/packages/dashboard/index.html +15 -0
- package/packages/dashboard/package-lock.json +2164 -0
- package/packages/dashboard/package.json +22 -0
- package/packages/dashboard/server/executor.js +86 -0
- package/packages/dashboard/server/index.js +359 -0
- package/packages/dashboard/server/parser.js +154 -0
- package/packages/dashboard/server/watcher.js +277 -0
- package/packages/dashboard/src/App.vue +154 -0
- package/packages/dashboard/src/components/ActionBar.vue +100 -0
- package/packages/dashboard/src/components/CommandPalette.vue +117 -0
- package/packages/dashboard/src/components/DetailPanel.vue +122 -0
- package/packages/dashboard/src/components/LogStream.vue +85 -0
- package/packages/dashboard/src/components/PipelineStage.vue +75 -0
- package/packages/dashboard/src/components/PipelineView.vue +94 -0
- package/packages/dashboard/src/components/ProjectList.vue +152 -0
- package/packages/dashboard/src/components/StageBadge.vue +53 -0
- package/packages/dashboard/src/components/StepCard.vue +89 -0
- package/packages/dashboard/src/composables/useDashboard.js +171 -0
- package/packages/dashboard/src/composables/useKeyboard.js +117 -0
- package/packages/dashboard/src/composables/useWebSocket.js +129 -0
- package/packages/dashboard/src/main.js +5 -0
- package/packages/dashboard/src/style.css +132 -0
- package/packages/dashboard/vite.config.js +18 -0
- package/src/index.js +68 -8
- package/src/init.js +23 -1
- package/src/progress.js +422 -0
- package/src/setup.js +16 -0
- package/templates/archive.md +56 -0
- package/templates/brainstorm.md +82 -26
- package/templates/commit.md +2 -0
- package/templates/execute.md +20 -1
- package/templates/progress-format.md +90 -0
- package/templates/quick.md +36 -3
- package/templates/resume-dialog.md +55 -0
- package/templates/skills/playwright-e2e/SKILL.md +1 -1
package/src/index.js
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* SillySpec CLI — 安装工具
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
6
|
* 只负责两件事:init(安装命令模板)和 setup(安装 MCP 工具)。
|
|
7
7
|
* 状态管理由 AI 直接读文件(STATE.md)完成,不需要 CLI。
|
|
8
8
|
*/
|
|
9
|
-
|
|
10
9
|
import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
|
|
11
10
|
import { join, resolve } from 'path';
|
|
12
11
|
import { cmdInit, getVersion } from './init.js';
|
|
12
|
+
import { ProgressManager } from './progress.js';
|
|
13
13
|
|
|
14
14
|
// ── CLI 入口 ──
|
|
15
15
|
|
|
@@ -25,6 +25,15 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
25
25
|
[--dir <path>] 指定目录
|
|
26
26
|
sillyspec setup [--list] 安装推荐 MCP 工具
|
|
27
27
|
[--list] 查看已安装状态
|
|
28
|
+
sillyspec progress <cmd> 进度恢复管理
|
|
29
|
+
init 初始化进度文件
|
|
30
|
+
status 查看当前进度
|
|
31
|
+
validate 校验并修复进度文件
|
|
32
|
+
reset [--stage X] 重置进度(全部或指定阶段)
|
|
33
|
+
complete --stage X 归档已完成阶段
|
|
34
|
+
sillyspec dashboard 启动 Dashboard Web UI
|
|
35
|
+
[--port <number>] 指定端口(默认 3456)
|
|
36
|
+
[--no-open] 不自动打开浏览器
|
|
28
37
|
|
|
29
38
|
选项:
|
|
30
39
|
--json 输出 JSON(给 AI 程序化读取)
|
|
@@ -36,12 +45,14 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
36
45
|
sillyspec init --workspace
|
|
37
46
|
sillyspec setup
|
|
38
47
|
sillyspec setup --list
|
|
48
|
+
sillyspec dashboard
|
|
49
|
+
sillyspec dashboard --port 8080 --no-open
|
|
39
50
|
`);
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
async function main() {
|
|
43
54
|
const args = process.argv.slice(2);
|
|
44
|
-
|
|
55
|
+
|
|
45
56
|
if (args[0] === '--version' || args[0] === '-v') {
|
|
46
57
|
console.log(getVersion());
|
|
47
58
|
process.exit(0);
|
|
@@ -51,7 +62,7 @@ async function main() {
|
|
|
51
62
|
printUsage();
|
|
52
63
|
process.exit(0);
|
|
53
64
|
}
|
|
54
|
-
|
|
65
|
+
|
|
55
66
|
// 解析全局选项
|
|
56
67
|
let json = false;
|
|
57
68
|
let targetDir = process.cwd();
|
|
@@ -59,7 +70,7 @@ async function main() {
|
|
|
59
70
|
let workspace = false;
|
|
60
71
|
let interactive = false;
|
|
61
72
|
const filteredArgs = [];
|
|
62
|
-
|
|
73
|
+
|
|
63
74
|
for (let i = 0; i < args.length; i++) {
|
|
64
75
|
if (args[i] === '--json') {
|
|
65
76
|
json = true;
|
|
@@ -79,15 +90,15 @@ async function main() {
|
|
|
79
90
|
filteredArgs.push(args[i]);
|
|
80
91
|
}
|
|
81
92
|
}
|
|
82
|
-
|
|
93
|
+
|
|
83
94
|
const command = filteredArgs[0];
|
|
84
95
|
const dir = targetDir;
|
|
85
|
-
|
|
96
|
+
|
|
86
97
|
if (!existsSync(dir)) {
|
|
87
98
|
console.error(`❌ 目录不存在: ${dir}`);
|
|
88
99
|
process.exit(1);
|
|
89
100
|
}
|
|
90
|
-
|
|
101
|
+
|
|
91
102
|
switch (command) {
|
|
92
103
|
case 'init':
|
|
93
104
|
await cmdInit(dir, { tool, workspace, interactive });
|
|
@@ -96,6 +107,55 @@ async function main() {
|
|
|
96
107
|
const setupList = filteredArgs.includes('--list') || filteredArgs.includes('-l');
|
|
97
108
|
await (await import('./setup.js')).cmdSetup(dir, { json, list: setupList });
|
|
98
109
|
break;
|
|
110
|
+
case 'progress': {
|
|
111
|
+
const pm = new ProgressManager();
|
|
112
|
+
const subCommand = filteredArgs[1];
|
|
113
|
+
const stageIdx = args.indexOf('--stage');
|
|
114
|
+
const stage = stageIdx >= 0 && args[stageIdx + 1] ? args[stageIdx + 1] : null;
|
|
115
|
+
|
|
116
|
+
switch (subCommand) {
|
|
117
|
+
case 'init':
|
|
118
|
+
pm.init(dir);
|
|
119
|
+
break;
|
|
120
|
+
case 'status':
|
|
121
|
+
pm.status(dir);
|
|
122
|
+
break;
|
|
123
|
+
case 'validate':
|
|
124
|
+
pm.validate(dir);
|
|
125
|
+
break;
|
|
126
|
+
case 'reset':
|
|
127
|
+
pm.reset(dir, stage);
|
|
128
|
+
break;
|
|
129
|
+
case 'complete':
|
|
130
|
+
pm.complete(dir, stage);
|
|
131
|
+
break;
|
|
132
|
+
default:
|
|
133
|
+
console.log('用法: sillyspec progress <init|status|validate|reset|complete> [--stage <stage>]');
|
|
134
|
+
}
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case 'dashboard': {
|
|
138
|
+
// Parse dashboard options
|
|
139
|
+
let port = 3456;
|
|
140
|
+
let openBrowser = true;
|
|
141
|
+
|
|
142
|
+
for (let i = 1; i < args.length; i++) {
|
|
143
|
+
if (args[i] === '--port' && args[i + 1]) {
|
|
144
|
+
port = parseInt(args[i + 1], 10);
|
|
145
|
+
i++;
|
|
146
|
+
} else if (args[i] === '--no-open') {
|
|
147
|
+
openBrowser = false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Import and start dashboard server
|
|
152
|
+
const { startServer } = await import('../packages/dashboard/server/index.js');
|
|
153
|
+
startServer({ port, open: openBrowser });
|
|
154
|
+
|
|
155
|
+
// Keep process alive
|
|
156
|
+
console.log('按 Ctrl+C 停止服务器');
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
99
159
|
default:
|
|
100
160
|
console.error(`❌ 未知命令: ${command}`);
|
|
101
161
|
printUsage();
|
package/src/init.js
CHANGED
|
@@ -209,6 +209,7 @@ async function doInstall(projectDir, tools, isWorkspace, subprojects = []) {
|
|
|
209
209
|
// .sillyspec/changes/archive/ → archive
|
|
210
210
|
// .sillyspec/quicklog/ → quick
|
|
211
211
|
// .sillyspec/knowledge/ → archive (spec 沉淀)
|
|
212
|
+
// .sillyspec/.runtime/ → progress (gitignored)
|
|
212
213
|
// (plan 内容已合并到 tasks.md)
|
|
213
214
|
if (isWorkspace) {
|
|
214
215
|
mkdirSync(join(projectDir, '.sillyspec', 'shared'), { recursive: true });
|
|
@@ -227,8 +228,29 @@ async function doInstall(projectDir, tools, isWorkspace, subprojects = []) {
|
|
|
227
228
|
writeFileSync(uncatPath, `# 未分类知识\n\n> execute/quick 执行中发现的坑暂存于此,用户审阅后归类到对应文件并更新 INDEX.md。\n`);
|
|
228
229
|
}
|
|
229
230
|
|
|
231
|
+
// 创建 .sillyspec/.runtime/ 目录结构
|
|
232
|
+
const runtimeDir = join(projectDir, '.sillyspec', '.runtime');
|
|
233
|
+
for (const sub of ['artifacts', 'history', 'logs', 'templates']) {
|
|
234
|
+
mkdirSync(join(runtimeDir, sub), { recursive: true });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// 复制 resume-dialog.md 到 .runtime/templates/
|
|
238
|
+
const resumeDialogSrc = join(TEMPLATE_DIR, 'resume-dialog.md');
|
|
239
|
+
if (existsSync(resumeDialogSrc)) {
|
|
240
|
+
const dest = join(runtimeDir, 'templates', 'resume-dialog.md');
|
|
241
|
+
if (!existsSync(dest)) {
|
|
242
|
+
copyFileSync(resumeDialogSrc, dest);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 创建初始 user-inputs.md
|
|
247
|
+
const inputsPath = join(runtimeDir, 'user-inputs.md');
|
|
248
|
+
if (!existsSync(inputsPath)) {
|
|
249
|
+
writeFileSync(inputsPath, '# 用户输入记录\n\n> 每步完成时由 AI 自动追加,记录用户所有原话。\n\n');
|
|
250
|
+
}
|
|
251
|
+
|
|
230
252
|
const gitignorePath = join(projectDir, '.gitignore');
|
|
231
|
-
const ignoreRules = ['.sillyspec/STATE.md', '.sillyspec/codebase/SCAN-RAW.md', '.sillyspec/local.yaml'];
|
|
253
|
+
const ignoreRules = ['.sillyspec/STATE.md', '.sillyspec/codebase/SCAN-RAW.md', '.sillyspec/local.yaml', '.sillyspec/.runtime/'];
|
|
232
254
|
if (existsSync(gitignorePath)) {
|
|
233
255
|
const content = readFileSync(gitignorePath, 'utf8');
|
|
234
256
|
let updated = content.trimEnd();
|
package/src/progress.js
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SillySpec ProgressManager — 进度恢复管理
|
|
3
|
+
*
|
|
4
|
+
* 纯 Node.js,无外部依赖。管理 .sillyspec/.runtime/progress.json。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, readdirSync, unlinkSync, copyFileSync } from 'fs';
|
|
8
|
+
import { join, resolve, dirname } from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = dirname(__filename);
|
|
13
|
+
const RUNTIME_DIR = '.sillyspec/.runtime';
|
|
14
|
+
const PROGRESS_FILE = 'progress.json';
|
|
15
|
+
const BACKUP_FILE = 'progress.json.bak';
|
|
16
|
+
|
|
17
|
+
// ── 默认步骤定义 ──
|
|
18
|
+
|
|
19
|
+
const STEP_DEFINITIONS = {
|
|
20
|
+
brainstorm: [
|
|
21
|
+
{ id: 1, name: '加载项目上下文' },
|
|
22
|
+
{ id: 2, name: '协作与复用检查' },
|
|
23
|
+
{ id: 3, name: '原型/设计图分析' },
|
|
24
|
+
{ id: 4, name: '评估需求范围' },
|
|
25
|
+
{ id: 5, name: '对话式探索' },
|
|
26
|
+
{ id: 6, name: '提出方案并推荐' },
|
|
27
|
+
{ id: 7, name: '分段展示设计' },
|
|
28
|
+
{ id: 8, name: '写设计文档' },
|
|
29
|
+
{ id: 9, name: 'AI 自审' },
|
|
30
|
+
{ id: 10, name: '用户确认设计方案' },
|
|
31
|
+
{ id: 11, name: '输出技术方案' },
|
|
32
|
+
{ id: 12, name: '更新 STATE.md' },
|
|
33
|
+
{ id: 13, name: '保存最终进度' },
|
|
34
|
+
],
|
|
35
|
+
propose: [
|
|
36
|
+
{ id: 1, name: '变更范围' },
|
|
37
|
+
{ id: 2, name: '方案设计' },
|
|
38
|
+
{ id: 3, name: '任务拆分' },
|
|
39
|
+
{ id: 4, name: '优先级排序' },
|
|
40
|
+
{ id: 5, name: '规范输出' },
|
|
41
|
+
],
|
|
42
|
+
plan: [
|
|
43
|
+
{ id: 1, name: '任务分析' },
|
|
44
|
+
{ id: 2, name: '依赖梳理' },
|
|
45
|
+
{ id: 3, name: '实现路径' },
|
|
46
|
+
{ id: 4, name: '风险评估' },
|
|
47
|
+
{ id: 5, name: '计划输出' },
|
|
48
|
+
],
|
|
49
|
+
execute: [
|
|
50
|
+
{ id: 1, name: '环境准备' },
|
|
51
|
+
{ id: 2, name: '编码实现' },
|
|
52
|
+
{ id: 3, name: '单元测试' },
|
|
53
|
+
{ id: 4, name: '代码审查' },
|
|
54
|
+
{ id: 5, name: '集成验证' },
|
|
55
|
+
],
|
|
56
|
+
verify: [
|
|
57
|
+
{ id: 1, name: '规范对照' },
|
|
58
|
+
{ id: 2, name: '功能测试' },
|
|
59
|
+
{ id: 3, name: '边界测试' },
|
|
60
|
+
{ id: 4, name: '回归测试' },
|
|
61
|
+
{ id: 5, name: '验收报告' },
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const EMPTY_PROGRESS = {
|
|
66
|
+
_version: 1,
|
|
67
|
+
schemaVersion: '1.0.0',
|
|
68
|
+
currentStage: 'brainstorm',
|
|
69
|
+
lastActiveAt: new Date().toISOString(),
|
|
70
|
+
resumeCount: 0,
|
|
71
|
+
checkpoint: '',
|
|
72
|
+
stages: {},
|
|
73
|
+
artifacts: [],
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function emptyStage() {
|
|
77
|
+
return {
|
|
78
|
+
status: 'not_started',
|
|
79
|
+
completedSteps: [],
|
|
80
|
+
inProgressStep: null,
|
|
81
|
+
summaries: {},
|
|
82
|
+
artifacts: [],
|
|
83
|
+
stageSummary: null,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── ProgressManager ──
|
|
88
|
+
|
|
89
|
+
export class ProgressManager {
|
|
90
|
+
// ── 公开方法 ──
|
|
91
|
+
|
|
92
|
+
init(cwd) {
|
|
93
|
+
const runtimeDir = join(cwd, RUNTIME_DIR);
|
|
94
|
+
const subdirs = ['artifacts', 'history', 'logs', 'templates'];
|
|
95
|
+
mkdirSync(runtimeDir, { recursive: true });
|
|
96
|
+
for (const d of subdirs) {
|
|
97
|
+
mkdirSync(join(runtimeDir, d), { recursive: true });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const progressPath = join(runtimeDir, PROGRESS_FILE);
|
|
101
|
+
if (!existsSync(progressPath)) {
|
|
102
|
+
// 初始化所有阶段
|
|
103
|
+
const data = { ...EMPTY_PROGRESS, stages: {} };
|
|
104
|
+
for (const stage of Object.keys(STEP_DEFINITIONS)) {
|
|
105
|
+
data.stages[stage] = emptyStage();
|
|
106
|
+
}
|
|
107
|
+
writeFileSync(progressPath, JSON.stringify(data, null, 2) + '\n');
|
|
108
|
+
console.log(`✅ 已创建 ${join(RUNTIME_DIR, PROGRESS_FILE)}`);
|
|
109
|
+
} else {
|
|
110
|
+
console.log(`ℹ️ ${join(RUNTIME_DIR, PROGRESS_FILE)} 已存在,跳过`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 复制 resume-dialog.md 模板
|
|
114
|
+
const templateDir = resolve(__dirname, '..', 'templates');
|
|
115
|
+
const resumeSrc = join(templateDir, 'resume-dialog.md');
|
|
116
|
+
const resumeDest = join(runtimeDir, 'templates', 'resume-dialog.md');
|
|
117
|
+
if (existsSync(resumeSrc) && !existsSync(resumeDest)) {
|
|
118
|
+
copyFileSync(resumeSrc, resumeDest);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 创建 user-inputs.md
|
|
122
|
+
const inputsPath = join(runtimeDir, 'user-inputs.md');
|
|
123
|
+
if (!existsSync(inputsPath)) {
|
|
124
|
+
writeFileSync(inputsPath, '# 用户输入记录\n\n> 每步完成时由 AI 自动追加,记录用户所有原话。\n\n');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// .gitignore
|
|
128
|
+
this._ensureGitignore(cwd);
|
|
129
|
+
|
|
130
|
+
return this.read(cwd);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
read(cwd) {
|
|
134
|
+
const progressPath = join(cwd, RUNTIME_DIR, PROGRESS_FILE);
|
|
135
|
+
const backupPath = join(cwd, RUNTIME_DIR, BACKUP_FILE);
|
|
136
|
+
|
|
137
|
+
// 三层容错:正常解析 → 修复 → 读 .bak
|
|
138
|
+
if (existsSync(progressPath)) {
|
|
139
|
+
const raw = readFileSync(progressPath, 'utf8');
|
|
140
|
+
const parsed = this._parseWithRecovery(raw);
|
|
141
|
+
if (parsed) return parsed;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (existsSync(backupPath)) {
|
|
145
|
+
const raw = readFileSync(backupPath, 'utf8');
|
|
146
|
+
const parsed = this._parseWithRecovery(raw);
|
|
147
|
+
if (parsed) {
|
|
148
|
+
console.log('⚠️ progress.json 损坏,已从备份恢复');
|
|
149
|
+
writeFileSync(progressPath, JSON.stringify(parsed, null, 2) + '\n');
|
|
150
|
+
return parsed;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
validate(cwd) {
|
|
158
|
+
const data = this.read(cwd);
|
|
159
|
+
if (!data) {
|
|
160
|
+
console.log('❌ 无法读取 progress.json');
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const errors = this._validate(data);
|
|
165
|
+
if (errors.length === 0) {
|
|
166
|
+
console.log('✅ progress.json 格式正确');
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
console.log(`⚠️ 发现 ${errors.length} 个问题,尝试修复...`);
|
|
171
|
+
|
|
172
|
+
// 自动修复:补全缺失的阶段
|
|
173
|
+
let fixed = { ...data };
|
|
174
|
+
let changed = false;
|
|
175
|
+
for (const stage of Object.keys(STEP_DEFINITIONS)) {
|
|
176
|
+
if (!fixed.stages[stage]) {
|
|
177
|
+
fixed.stages[stage] = emptyStage();
|
|
178
|
+
changed = true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (!fixed._version) { fixed._version = 1; changed = true; }
|
|
182
|
+
if (!fixed.schemaVersion) { fixed.schemaVersion = '1.0.0'; changed = true; }
|
|
183
|
+
if (!fixed.artifacts) { fixed.artifacts = []; changed = true; }
|
|
184
|
+
if (!fixed.lastActiveAt) { fixed.lastActiveAt = new Date().toISOString(); changed = true; }
|
|
185
|
+
|
|
186
|
+
if (changed) {
|
|
187
|
+
this._backup(cwd);
|
|
188
|
+
const progressPath = join(cwd, RUNTIME_DIR, PROGRESS_FILE);
|
|
189
|
+
writeFileSync(progressPath, JSON.stringify(fixed, null, 2) + '\n');
|
|
190
|
+
console.log('✅ 已修复并备份');
|
|
191
|
+
} else {
|
|
192
|
+
console.log('❌ 无法自动修复:');
|
|
193
|
+
errors.forEach(e => console.log(` - ${e}`));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return errors.length === 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
status(cwd) {
|
|
200
|
+
const data = this.read(cwd);
|
|
201
|
+
if (!data) {
|
|
202
|
+
console.log('❌ 未找到 progress.json,请先运行 sillyspec progress init');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const stageLabels = {
|
|
207
|
+
brainstorm: '🧠 需求探索',
|
|
208
|
+
propose: '📋 方案设计',
|
|
209
|
+
plan: '📐 实现计划',
|
|
210
|
+
execute: '⚡ 波次执行',
|
|
211
|
+
verify: '🔍 验证确认',
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
console.log('');
|
|
215
|
+
console.log(' ═══════════════════════════════════════');
|
|
216
|
+
console.log(` 当前阶段: ${(stageLabels[data.currentStage] || data.currentStage)}`);
|
|
217
|
+
console.log(` 最近活跃: ${data.lastActiveAt ? this._timeAgo(data.lastActiveAt) : '未知'}`);
|
|
218
|
+
console.log(` 恢复次数: ${data.resumeCount ?? 0}`);
|
|
219
|
+
if (data.checkpoint) {
|
|
220
|
+
console.log(` 检查点: ${data.checkpoint}`);
|
|
221
|
+
}
|
|
222
|
+
console.log(' ═══════════════════════════════════════');
|
|
223
|
+
console.log('');
|
|
224
|
+
|
|
225
|
+
for (const [stage, def] of Object.entries(STEP_DEFINITIONS)) {
|
|
226
|
+
const stageData = data.stages[stage] || emptyStage();
|
|
227
|
+
const label = stageLabels[stage] || stage;
|
|
228
|
+
const statusIcons = { not_started: '⬜', in_progress: '🔵', completed: '✅' };
|
|
229
|
+
const icon = statusIcons[stageData.status] || '⬜';
|
|
230
|
+
|
|
231
|
+
let steps = '';
|
|
232
|
+
for (const step of def) {
|
|
233
|
+
if (stageData.completedSteps.includes(step.id)) {
|
|
234
|
+
steps += '●';
|
|
235
|
+
} else if (stageData.inProgressStep && stageData.inProgressStep.id === step.id) {
|
|
236
|
+
steps += '◐';
|
|
237
|
+
} else {
|
|
238
|
+
steps += '○';
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const completed = (stageData.completedSteps || []).length;
|
|
243
|
+
const total = def.length;
|
|
244
|
+
console.log(` ${icon} ${label} [${steps}] ${completed}/${total}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
console.log('');
|
|
248
|
+
|
|
249
|
+
// 产出文件
|
|
250
|
+
if (data.artifacts && data.artifacts.length > 0) {
|
|
251
|
+
console.log(' 📦 产出文件:');
|
|
252
|
+
for (const a of data.artifacts) {
|
|
253
|
+
console.log(` - ${a.name} (${a.stage}) → ${a.path}`);
|
|
254
|
+
}
|
|
255
|
+
console.log('');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
reset(cwd, stage) {
|
|
260
|
+
this._ensureDir(cwd);
|
|
261
|
+
this._backup(cwd);
|
|
262
|
+
|
|
263
|
+
const progressPath = join(cwd, RUNTIME_DIR, PROGRESS_FILE);
|
|
264
|
+
|
|
265
|
+
if (stage) {
|
|
266
|
+
// 只重置指定阶段
|
|
267
|
+
const data = this.read(cwd);
|
|
268
|
+
if (!data) { console.log('❌ 无法读取 progress.json'); return; }
|
|
269
|
+
if (!data.stages[stage]) { console.log(`❌ 未知阶段: ${stage}`); return; }
|
|
270
|
+
|
|
271
|
+
data.stages[stage] = emptyStage();
|
|
272
|
+
data.lastActiveAt = new Date().toISOString();
|
|
273
|
+
writeFileSync(progressPath, JSON.stringify(data, null, 2) + '\n');
|
|
274
|
+
console.log(`✅ 已重置阶段: ${stage}`);
|
|
275
|
+
} else {
|
|
276
|
+
// 全部重置:备份后删除
|
|
277
|
+
if (existsSync(progressPath)) {
|
|
278
|
+
unlinkSync(progressPath);
|
|
279
|
+
console.log('✅ 已重置所有进度(备份已保留)');
|
|
280
|
+
} else {
|
|
281
|
+
console.log('ℹ️ 无进度文件可重置');
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
complete(cwd, stage) {
|
|
287
|
+
if (!stage) {
|
|
288
|
+
console.log('❌ 请指定阶段: --stage <stage>');
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const data = this.read(cwd);
|
|
293
|
+
if (!data) { console.log('❌ 无法读取 progress.json'); return; }
|
|
294
|
+
|
|
295
|
+
const stageData = data.stages[stage];
|
|
296
|
+
if (!stageData) { console.log(`❌ 未知阶段: ${stage}`); return; }
|
|
297
|
+
|
|
298
|
+
// 归档到 history/
|
|
299
|
+
const historyDir = join(cwd, RUNTIME_DIR, 'history');
|
|
300
|
+
mkdirSync(historyDir, { recursive: true });
|
|
301
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
302
|
+
const archiveName = `${stage}-${ts}.json`;
|
|
303
|
+
writeFileSync(join(historyDir, archiveName), JSON.stringify({ stage, data: stageData, completedAt: new Date().toISOString() }, null, 2) + '\n');
|
|
304
|
+
|
|
305
|
+
console.log(`✅ 已归档 ${stage} → ${join(RUNTIME_DIR, 'history', archiveName)}`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ── 内部方法 ──
|
|
309
|
+
|
|
310
|
+
_ensureDir(cwd) {
|
|
311
|
+
const runtimeDir = join(cwd, RUNTIME_DIR);
|
|
312
|
+
if (!existsSync(runtimeDir)) {
|
|
313
|
+
mkdirSync(runtimeDir, { recursive: true });
|
|
314
|
+
for (const d of ['artifacts', 'history', 'logs', 'templates']) {
|
|
315
|
+
mkdirSync(join(runtimeDir, d), { recursive: true });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
_backup(cwd) {
|
|
321
|
+
const progressPath = join(cwd, RUNTIME_DIR, PROGRESS_FILE);
|
|
322
|
+
const backupPath = join(cwd, RUNTIME_DIR, BACKUP_FILE);
|
|
323
|
+
if (existsSync(progressPath)) {
|
|
324
|
+
renameSync(progressPath, backupPath);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
_parseWithRecovery(jsonString) {
|
|
329
|
+
// 第一层:直接解析
|
|
330
|
+
try {
|
|
331
|
+
return JSON.parse(jsonString);
|
|
332
|
+
} catch {}
|
|
333
|
+
|
|
334
|
+
// 第二层:修复常见问题
|
|
335
|
+
let fixed = jsonString;
|
|
336
|
+
|
|
337
|
+
// 去尾随逗号(对象和数组中的 ,} 和 ,])
|
|
338
|
+
fixed = fixed.replace(/,\s*([}\]])/g, '$1');
|
|
339
|
+
|
|
340
|
+
// 单引号转双引号(简易处理)
|
|
341
|
+
// 只处理 key 和简单字符串值
|
|
342
|
+
fixed = fixed.replace(/([{,]\s*)'([^']+)'(\s*:)/g, '$1"$2"$3');
|
|
343
|
+
fixed = fixed.replace(/:\s*'([^']*)'([,}\]])/g, ':"$1"$2');
|
|
344
|
+
|
|
345
|
+
try {
|
|
346
|
+
return JSON.parse(fixed);
|
|
347
|
+
} catch {}
|
|
348
|
+
|
|
349
|
+
// 第三层:尝试截断到最后一个完整对象
|
|
350
|
+
const lastBrace = fixed.lastIndexOf('}');
|
|
351
|
+
if (lastBrace > 0) {
|
|
352
|
+
const truncated = fixed.substring(0, lastBrace + 1);
|
|
353
|
+
// 补全外层括号
|
|
354
|
+
let open = 0;
|
|
355
|
+
for (const ch of truncated) {
|
|
356
|
+
if (ch === '{') open++;
|
|
357
|
+
if (ch === '}') open--;
|
|
358
|
+
}
|
|
359
|
+
const repaired = truncated + '}'.repeat(Math.max(0, open));
|
|
360
|
+
try {
|
|
361
|
+
return JSON.parse(repaired);
|
|
362
|
+
} catch {}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
_validate(data) {
|
|
369
|
+
const errors = [];
|
|
370
|
+
if (!data || typeof data !== 'object') { errors.push('数据不是有效对象'); return errors; }
|
|
371
|
+
if (!data.stages || typeof data.stages !== 'object') { errors.push('缺少 stages 字段'); }
|
|
372
|
+
if (!data.currentStage || typeof data.currentStage !== 'string') { errors.push('缺少 currentStage 字段'); }
|
|
373
|
+
if (!data.schemaVersion) { errors.push('缺少 schemaVersion 字段'); }
|
|
374
|
+
if (typeof data._version !== 'number' || data._version < 1) { errors.push('_version 应为正整数'); }
|
|
375
|
+
|
|
376
|
+
// 校验阶段数据
|
|
377
|
+
if (data.stages) {
|
|
378
|
+
for (const [name, stage] of Object.entries(data.stages)) {
|
|
379
|
+
if (!stage.status) errors.push(`阶段 ${name} 缺少 status`);
|
|
380
|
+
if (stage.completedSteps && !Array.isArray(stage.completedSteps)) {
|
|
381
|
+
errors.push(`阶段 ${name} 的 completedSteps 不是数组`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return errors;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
_timeAgo(dateStr) {
|
|
390
|
+
if (!dateStr) return '未知';
|
|
391
|
+
const now = Date.now();
|
|
392
|
+
const then = new Date(dateStr).getTime();
|
|
393
|
+
const diff = now - then;
|
|
394
|
+
|
|
395
|
+
const seconds = Math.floor(diff / 1000);
|
|
396
|
+
const minutes = Math.floor(seconds / 60);
|
|
397
|
+
const hours = Math.floor(minutes / 60);
|
|
398
|
+
const days = Math.floor(hours / 24);
|
|
399
|
+
|
|
400
|
+
if (days > 0) return `${days} 天前`;
|
|
401
|
+
if (hours > 0) return `${hours} 小时前`;
|
|
402
|
+
if (minutes > 0) return `${minutes} 分钟前`;
|
|
403
|
+
return '刚刚';
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
_getStepDefinitions(stage) {
|
|
407
|
+
return STEP_DEFINITIONS[stage] || [];
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
_ensureGitignore(cwd) {
|
|
411
|
+
const gitignorePath = join(cwd, '.gitignore');
|
|
412
|
+
const rule = '.sillyspec/.runtime/';
|
|
413
|
+
|
|
414
|
+
if (existsSync(gitignorePath)) {
|
|
415
|
+
const content = readFileSync(gitignorePath, 'utf8');
|
|
416
|
+
if (content.includes(rule)) return;
|
|
417
|
+
writeFileSync(gitignorePath, content.trimEnd() + '\n' + rule + '\n');
|
|
418
|
+
} else {
|
|
419
|
+
writeFileSync(gitignorePath, rule + '\n');
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
package/src/setup.js
CHANGED
|
@@ -40,6 +40,22 @@ const MCP_TOOLS = [
|
|
|
40
40
|
args: ['chrome-devtools-mcp@latest'],
|
|
41
41
|
url: 'https://github.com/ChromeDevTools/chrome-devtools-mcp',
|
|
42
42
|
},
|
|
43
|
+
{
|
|
44
|
+
id: 'agent-browser',
|
|
45
|
+
name: 'Agent Browser (Vercel)',
|
|
46
|
+
description: 'Rust 原生浏览器 CLI,token 消耗极低,50+ 命令覆盖导航/表单/截图/网络',
|
|
47
|
+
command: 'npx',
|
|
48
|
+
args: ['@anthropic-ai/agent-browser@latest'],
|
|
49
|
+
url: 'https://github.com/vercel-labs/agent-browser',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: 'pinchtab',
|
|
53
|
+
name: 'PinchTab',
|
|
54
|
+
description: '12MB Go 二进制,零依赖,accessibility tree 极省 token,有 MCP 支持',
|
|
55
|
+
command: 'npx',
|
|
56
|
+
args: ['pinchtab-mcp@latest'],
|
|
57
|
+
url: 'https://github.com/pinchtab/pinchtab',
|
|
58
|
+
},
|
|
43
59
|
];
|
|
44
60
|
|
|
45
61
|
// ── 数据库 MCP 定义(需要连接信息)──
|
package/templates/archive.md
CHANGED
|
@@ -32,6 +32,7 @@ $ARGUMENTS
|
|
|
32
32
|
- 包含的文件列表
|
|
33
33
|
- 任务完成统计(✅ 已完成 / ⬜ 未完成)
|
|
34
34
|
- 一句话总结本次变更
|
|
35
|
+
- quicklog 修改记录(如有 `.sillyspec/changes/<change-name>/quicklog/` 目录)
|
|
35
36
|
|
|
36
37
|
### 3. Spec 沉淀
|
|
37
38
|
|
|
@@ -43,6 +44,61 @@ $ARGUMENTS
|
|
|
43
44
|
- ① 确认归档
|
|
44
45
|
- ② 取消
|
|
45
46
|
|
|
47
|
+
### 4.5 生成归档摘要
|
|
48
|
+
|
|
49
|
+
在变更目录下自动生成 `SUMMARY.md`:
|
|
50
|
+
|
|
51
|
+
```markdown
|
|
52
|
+
# <变更名> 归档
|
|
53
|
+
|
|
54
|
+
- 创建:YYYY-MM-DD
|
|
55
|
+
- 完成:YYYY-MM-DD
|
|
56
|
+
- 涉及阶段:brainstorm → plan → execute → verify
|
|
57
|
+
|
|
58
|
+
## 关键决策
|
|
59
|
+
- (从 design.md 提取 3-5 条核心决策)
|
|
60
|
+
|
|
61
|
+
## 产出文件
|
|
62
|
+
- design.md — 设计文档
|
|
63
|
+
- tasks.md — 任务清单
|
|
64
|
+
- quicklog/ — 关联 quick 修改(N次)
|
|
65
|
+
- quick1: 描述
|
|
66
|
+
- quick2: 描述
|
|
67
|
+
|
|
68
|
+
## 代码变更统计
|
|
69
|
+
- 新增 X 文件,修改 Y 文件,删除 Z 文件
|
|
70
|
+
- 详见 CHANGELOG.md
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
在变更目录下自动生成 `CHANGELOG.md`:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# 收集该变更相关的 git commit(按变更名过滤或按时间范围)
|
|
77
|
+
git log --oneline --no-merges -- .sillyspec/changes/<change-name>/ 2>/dev/null
|
|
78
|
+
# 以及变更目录创建后的所有 commit
|
|
79
|
+
git log --oneline --no-merges --since="<创建时间>" -- "*.ts" "*.js" "*.vue" "*.java" 2>/dev/null
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
写入 CHANGELOG.md,格式:
|
|
83
|
+
```markdown
|
|
84
|
+
# <变更名> 变更日志
|
|
85
|
+
|
|
86
|
+
## brainstorm 阶段
|
|
87
|
+
- (相关 commit)
|
|
88
|
+
|
|
89
|
+
## plan 阶段
|
|
90
|
+
- (相关 commit)
|
|
91
|
+
|
|
92
|
+
## execute 阶段
|
|
93
|
+
- (相关 commit)
|
|
94
|
+
|
|
95
|
+
## quick 修改
|
|
96
|
+
- (相关 commit)
|
|
97
|
+
|
|
98
|
+
## verify 阶段
|
|
99
|
+
- (相关 commit)
|
|
100
|
+
```
|
|
101
|
+
|
|
46
102
|
### 5. 执行归档
|
|
47
103
|
|
|
48
104
|
- 目标路径:`.sillyspec/changes/archive/YYYY-MM-DD-<change-name>/`
|