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
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
# SillySpec Dashboard — 实现计划
|
|
2
|
+
|
|
3
|
+
## 锚定确认
|
|
4
|
+
|
|
5
|
+
- [x] design.md — 技术方案和文件变更清单
|
|
6
|
+
- [x] src/index.js — CLI 入口,理解现有命令结构
|
|
7
|
+
- [x] package.json — ES Module,Node >=18
|
|
8
|
+
|
|
9
|
+
## 执行顺序
|
|
10
|
+
|
|
11
|
+
**Wave 1**(基础骨架,并行):
|
|
12
|
+
- Task 1: 项目脚手架 + 依赖安装
|
|
13
|
+
- Task 2: 后端 HTTP + WebSocket 服务
|
|
14
|
+
|
|
15
|
+
**Wave 2**(依赖 Wave 1):
|
|
16
|
+
- Task 3: 文件监听 + 数据解析
|
|
17
|
+
- Task 4: REST API
|
|
18
|
+
|
|
19
|
+
**Wave 3**(依赖 Wave 2):
|
|
20
|
+
- Task 5: 前端三栏布局 + 状态管理
|
|
21
|
+
- Task 6: Pipeline 视图 + StepCard 组件
|
|
22
|
+
|
|
23
|
+
**Wave 4**(依赖 Wave 3):
|
|
24
|
+
- Task 7: 日志流 + 命令面板
|
|
25
|
+
- Task 8: CLI 集成(dashboard 命令)
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Task 1: 项目脚手架 + 依赖安装
|
|
30
|
+
|
|
31
|
+
**文件:**
|
|
32
|
+
- 新建:`packages/dashboard/package.json`
|
|
33
|
+
- 新建:`packages/dashboard/vite.config.js`
|
|
34
|
+
- 新建:`packages/dashboard/index.html`
|
|
35
|
+
- 新建:`packages/dashboard/tailwind.config.js`
|
|
36
|
+
- 新建:`packages/dashboard/postcss.config.js`
|
|
37
|
+
- 新建:`packages/dashboard/src/main.js`
|
|
38
|
+
- 新建:`packages/dashboard/src/App.vue`
|
|
39
|
+
- 新建:`packages/dashboard/src/style.css`
|
|
40
|
+
|
|
41
|
+
**步骤:**
|
|
42
|
+
|
|
43
|
+
- [ ] 创建 `packages/dashboard/package.json`:
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"name": "@sillyspec/dashboard",
|
|
47
|
+
"version": "1.0.0",
|
|
48
|
+
"type": "module",
|
|
49
|
+
"private": true,
|
|
50
|
+
"scripts": {
|
|
51
|
+
"dev": "vite",
|
|
52
|
+
"build": "vite build",
|
|
53
|
+
"preview": "vite preview"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"vue": "^3.5.0",
|
|
57
|
+
"ws": "^8.18.0",
|
|
58
|
+
"chokidar": "^4.0.0",
|
|
59
|
+
"open": "^10.1.0"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@vitejs/plugin-vue": "^5.2.0",
|
|
63
|
+
"vite": "^6.0.0",
|
|
64
|
+
"tailwindcss": "^4.0.0",
|
|
65
|
+
"@tailwindcss/vite": "^4.0.0",
|
|
66
|
+
"autoprefixer": "^10.4.0"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
- [ ] 创建 `packages/dashboard/vite.config.js`:
|
|
72
|
+
```javascript
|
|
73
|
+
import { defineConfig } from 'vite'
|
|
74
|
+
import vue from '@vitejs/plugin-vue'
|
|
75
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
76
|
+
|
|
77
|
+
export default defineConfig({
|
|
78
|
+
plugins: [vue(), tailwindcss()],
|
|
79
|
+
build: {
|
|
80
|
+
outDir: 'dist',
|
|
81
|
+
emptyOutDir: true
|
|
82
|
+
},
|
|
83
|
+
server: {
|
|
84
|
+
port: 3456
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- [ ] 创建 `packages/dashboard/index.html`:
|
|
90
|
+
```html
|
|
91
|
+
<!DOCTYPE html>
|
|
92
|
+
<html lang="zh-CN" class="dark">
|
|
93
|
+
<head>
|
|
94
|
+
<meta charset="UTF-8">
|
|
95
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
96
|
+
<title>SillySpec Dashboard</title>
|
|
97
|
+
</head>
|
|
98
|
+
<body class="bg-[#0D1117] text-gray-100">
|
|
99
|
+
<div id="app"></div>
|
|
100
|
+
<script type="module" src="/src/main.js"></script>
|
|
101
|
+
</body>
|
|
102
|
+
</html>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
- [ ] 创建 `packages/dashboard/src/style.css`:
|
|
106
|
+
```css
|
|
107
|
+
@import "tailwindcss";
|
|
108
|
+
|
|
109
|
+
@theme {
|
|
110
|
+
--color-bg: #0D1117;
|
|
111
|
+
--color-primary: #00D4AA;
|
|
112
|
+
--color-warning: #F59E0B;
|
|
113
|
+
--color-danger: #F87171;
|
|
114
|
+
--color-muted: #6B7280;
|
|
115
|
+
--color-surface: #161B22;
|
|
116
|
+
--color-border: #30363D;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
body {
|
|
120
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
121
|
+
overflow: hidden;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* 动效 */
|
|
125
|
+
@keyframes pulse-glow {
|
|
126
|
+
0% { box-shadow: 0 0 0 0 rgba(0, 212, 170, 0.4); }
|
|
127
|
+
70% { box-shadow: 0 0 0 8px rgba(0, 212, 170, 0); }
|
|
128
|
+
100% { box-shadow: 0 0 0 0 rgba(0, 212, 170, 0); }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.pulse-complete { animation: pulse-glow 200ms ease-out; }
|
|
132
|
+
|
|
133
|
+
.fade-enter-active, .fade-leave-active { transition: opacity 100ms; }
|
|
134
|
+
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
|
135
|
+
|
|
136
|
+
/* 日志字体 */
|
|
137
|
+
.font-mono-log { font-family: 'JetBrains Mono', 'Fira Code', monospace; }
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
- [ ] 创建 `packages/dashboard/src/main.js`:
|
|
141
|
+
```javascript
|
|
142
|
+
import { createApp } from 'vue'
|
|
143
|
+
import App from './App.vue'
|
|
144
|
+
import './style.css'
|
|
145
|
+
|
|
146
|
+
createApp(App).mount('#app')
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
- [ ] 创建 `packages/dashboard/src/App.vue`(骨架,后续 Task 填充):
|
|
150
|
+
```vue
|
|
151
|
+
<template>
|
|
152
|
+
<div class="h-screen flex flex-col">
|
|
153
|
+
<div class="h-full flex">
|
|
154
|
+
<!-- 三栏布局占位 -->
|
|
155
|
+
<div class="w-[200px] bg-[#161B22] border-r border-[#30363D] flex-shrink-0">
|
|
156
|
+
<div class="p-4 text-sm text-[#00D4AA] font-semibold">项目列表</div>
|
|
157
|
+
</div>
|
|
158
|
+
<div class="flex-1 overflow-auto p-6">
|
|
159
|
+
<div class="text-[#6B7280]">选择左侧项目查看详情</div>
|
|
160
|
+
</div>
|
|
161
|
+
<div class="w-[320px] bg-[#161B22] border-l border-[#30363D] flex-shrink-0">
|
|
162
|
+
<div class="p-4 text-sm text-[#6B7280]">详情面板</div>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
</template>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
- [ ] 安装依赖:`cd packages/dashboard && npm install`
|
|
170
|
+
- [ ] 验证:`cd packages/dashboard && npm run dev` → 浏览器打开 `http://localhost:3456` 看到三栏骨架
|
|
171
|
+
- [ ] git commit -m "feat(dashboard): project scaffold with Vue 3 + Vite + Tailwind"
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Task 2: 后端 HTTP + WebSocket 服务
|
|
176
|
+
|
|
177
|
+
**文件:**
|
|
178
|
+
- 新建:`packages/dashboard/server/index.js`
|
|
179
|
+
|
|
180
|
+
**步骤:**
|
|
181
|
+
|
|
182
|
+
- [ ] 创建 `packages/dashboard/server/index.js`:
|
|
183
|
+
```javascript
|
|
184
|
+
import { createServer } from 'http'
|
|
185
|
+
import { WebSocketServer } from 'ws'
|
|
186
|
+
import { existsSync, statSync, readFileSync, readdirSync } from 'fs'
|
|
187
|
+
import { join, resolve } from 'path'
|
|
188
|
+
|
|
189
|
+
let wss
|
|
190
|
+
|
|
191
|
+
function broadcast(data) {
|
|
192
|
+
const msg = JSON.stringify(data)
|
|
193
|
+
for (const client of wss.clients) {
|
|
194
|
+
if (client.readyState === 1) client.send(msg)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function startServer({ port = 3456, open: shouldOpen = true } = {}) {
|
|
199
|
+
const server = createServer((req, res) => {
|
|
200
|
+
// API 路由
|
|
201
|
+
if (req.url?.startsWith('/api/')) {
|
|
202
|
+
return handleApi(req, res)
|
|
203
|
+
}
|
|
204
|
+
// SPA fallback: serve dist/ 静态文件
|
|
205
|
+
res.writeHead(404)
|
|
206
|
+
res.end('Not found. Run `npm run build` first.')
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
wss = new WebSocketServer({ server })
|
|
210
|
+
wss.on('connection', (ws) => {
|
|
211
|
+
ws.on('message', (data) => handleMessage(ws, data.toString()))
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
// 导入并启动 watcher(Task 3 实现)
|
|
215
|
+
try {
|
|
216
|
+
const { startWatcher } = await import('./watcher.js')
|
|
217
|
+
startWatcher((event) => broadcast(event))
|
|
218
|
+
} catch {
|
|
219
|
+
console.warn('⚠️ Watcher not available yet')
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
server.listen(port, () => {
|
|
223
|
+
console.log(`🚀 SillySpec Dashboard: http://localhost:${port}`)
|
|
224
|
+
if (shouldOpen) {
|
|
225
|
+
import('open').then(m => m.default(`http://localhost:${port}`)).catch(() => {})
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
return server
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function handleApi(req, res) {
|
|
233
|
+
const url = new URL(req.url, 'http://localhost')
|
|
234
|
+
res.setHeader('Content-Type', 'application/json')
|
|
235
|
+
|
|
236
|
+
if (url.pathname === '/api/projects') {
|
|
237
|
+
const projects = discoverProjects()
|
|
238
|
+
res.writeHead(200)
|
|
239
|
+
res.end(JSON.stringify(projects))
|
|
240
|
+
} else if (url.pathname.startsWith('/api/project/')) {
|
|
241
|
+
const name = decodeURIComponent(url.pathname.split('/api/project/')[1])
|
|
242
|
+
const project = getProjectStatus(name)
|
|
243
|
+
if (!project) {
|
|
244
|
+
res.writeHead(404)
|
|
245
|
+
res.end(JSON.stringify({ error: 'Project not found' }))
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
res.writeHead(200)
|
|
249
|
+
res.end(JSON.stringify(project))
|
|
250
|
+
} else {
|
|
251
|
+
res.writeHead(404)
|
|
252
|
+
res.end(JSON.stringify({ error: 'Not found' }))
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function handleMessage(ws, raw) {
|
|
257
|
+
try {
|
|
258
|
+
const msg = JSON.parse(raw)
|
|
259
|
+
if (msg.type === 'cli:execute') {
|
|
260
|
+
const { spawn } = await import('child_process')
|
|
261
|
+
// Task 4 实现 executor
|
|
262
|
+
}
|
|
263
|
+
} catch {}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function discoverProjects() {
|
|
267
|
+
const cwd = process.cwd()
|
|
268
|
+
const home = process.env.HOME || '/root'
|
|
269
|
+
const dirs = [cwd]
|
|
270
|
+
try {
|
|
271
|
+
for (const entry of readdirSync(home)) {
|
|
272
|
+
const p = join(home, entry)
|
|
273
|
+
if (statSync(p).isDirectory() && existsSync(join(p, '.sillyspec'))) {
|
|
274
|
+
dirs.push(p)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
} catch {}
|
|
278
|
+
// 去重
|
|
279
|
+
const unique = [...new Set(dirs)]
|
|
280
|
+
return unique.map(p => ({
|
|
281
|
+
name: p.split('/').pop(),
|
|
282
|
+
path: p
|
|
283
|
+
}))
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function getProjectStatus(name) {
|
|
287
|
+
// 简单实现:读取 STATE.md
|
|
288
|
+
// Task 3 完善
|
|
289
|
+
return null
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export { startServer, broadcast }
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
- [ ] 验证:`node packages/dashboard/server/index.js --port 3456` → 输出 `🚀 SillySpec Dashboard: http://localhost:3456`
|
|
296
|
+
- [ ] git commit -m "feat(dashboard): HTTP + WebSocket server"
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Task 3: 文件监听 + 数据解析
|
|
301
|
+
|
|
302
|
+
**文件:**
|
|
303
|
+
- 新建:`packages/dashboard/server/watcher.js`
|
|
304
|
+
- 新建:`packages/dashboard/server/parser.js`
|
|
305
|
+
|
|
306
|
+
**步骤:**
|
|
307
|
+
|
|
308
|
+
- [ ] 创建 `packages/dashboard/server/parser.js`:
|
|
309
|
+
```javascript
|
|
310
|
+
import { existsSync, readFileSync } from 'fs'
|
|
311
|
+
import { join } from 'path'
|
|
312
|
+
|
|
313
|
+
export function parseProjectState(projectPath) {
|
|
314
|
+
const sillyDir = join(projectPath, '.sillyspec')
|
|
315
|
+
if (!existsSync(sillyDir)) return null
|
|
316
|
+
|
|
317
|
+
const state = {}
|
|
318
|
+
|
|
319
|
+
// 1. 解析 STATE.md
|
|
320
|
+
const stateFile = join(sillyDir, 'STATE.md')
|
|
321
|
+
if (existsSync(stateFile)) {
|
|
322
|
+
const content = readFileSync(stateFile, 'utf8')
|
|
323
|
+
state.currentStage = extractField(content, '当前阶段') || 'unknown'
|
|
324
|
+
state.nextStep = extractField(content, '下一步') || ''
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// 2. 解析 progress.json
|
|
328
|
+
const progressFile = join(sillyDir, '.runtime', 'progress.json')
|
|
329
|
+
if (existsSync(progressFile)) {
|
|
330
|
+
try {
|
|
331
|
+
const data = JSON.parse(readFileSync(progressFile, 'utf8'))
|
|
332
|
+
state.progress = data
|
|
333
|
+
state.stages = data.stages || {}
|
|
334
|
+
} catch {}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// 3. 列出 specs
|
|
338
|
+
const specsDir = join(sillyDir, 'specs')
|
|
339
|
+
if (existsSync(specsDir)) {
|
|
340
|
+
const { readdirSync } = await import('fs')
|
|
341
|
+
state.specs = readdirSync(specsDir).filter(f => f.endsWith('.md'))
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
state.lastActive = state.progress?.lastActiveAt || state.progress?._meta?.updatedAt || null
|
|
345
|
+
|
|
346
|
+
return state
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function extractField(content, fieldName) {
|
|
350
|
+
const match = content.match(new RegExp(`${fieldName}[::]\\s*(.+)`))
|
|
351
|
+
return match ? match[1].trim() : null
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
- [ ] 创建 `packages/dashboard/server/watcher.js`:
|
|
356
|
+
```javascript
|
|
357
|
+
import { watch } from 'chokidar'
|
|
358
|
+
import { parseProjectState } from './parser.js'
|
|
359
|
+
|
|
360
|
+
let watcher = null
|
|
361
|
+
|
|
362
|
+
export function startWatcher(onChange) {
|
|
363
|
+
const home = process.env.HOME || '/root'
|
|
364
|
+
const pattern = `${home}/*/.sillyspec/**/*`
|
|
365
|
+
|
|
366
|
+
watcher = watch(pattern, {
|
|
367
|
+
ignored: /node_modules/,
|
|
368
|
+
persistent: true,
|
|
369
|
+
ignoreInitial: true,
|
|
370
|
+
awaitWriteFinish: { stabilityThreshold: 300, pollInterval: 100 }
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
watcher.on('all', (event, filePath) => {
|
|
374
|
+
// 从文件路径反推项目名
|
|
375
|
+
const match = filePath.match(/\/([^/]+)\/\.sillyspec\//)
|
|
376
|
+
if (!match) return
|
|
377
|
+
const projectName = match[1]
|
|
378
|
+
const projectPath = filePath.split('.sillyspec')[0]
|
|
379
|
+
|
|
380
|
+
const state = parseProjectState(projectPath)
|
|
381
|
+
if (state) {
|
|
382
|
+
onChange({ type: 'project:updated', project: { name: projectName, path: projectPath, ...state } })
|
|
383
|
+
}
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
return watcher
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function stopWatcher() {
|
|
390
|
+
watcher?.close()
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
- [ ] 验证:在另一个终端执行 `sillyspec init --dir /tmp/test-project`,确认 WebSocket 推送了 `project:updated` 事件
|
|
395
|
+
- [ ] git commit -m "feat(dashboard): file watcher + state parser"
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
## Task 4: REST API + CLI 执行器
|
|
400
|
+
|
|
401
|
+
**文件:**
|
|
402
|
+
- 修改:`packages/dashboard/server/index.js`(完善 API 路由)
|
|
403
|
+
- 新建:`packages/dashboard/server/executor.js`
|
|
404
|
+
|
|
405
|
+
**步骤:**
|
|
406
|
+
|
|
407
|
+
- [ ] 创建 `packages/dashboard/server/executor.js`:
|
|
408
|
+
```javascript
|
|
409
|
+
import { spawn } from 'child_process'
|
|
410
|
+
|
|
411
|
+
const runningProcesses = new Map()
|
|
412
|
+
|
|
413
|
+
export function executeCommand(projectPath, command, onOutput, onComplete) {
|
|
414
|
+
const key = `${projectPath}:${command}`
|
|
415
|
+
if (runningProcesses.has(key)) {
|
|
416
|
+
onOutput(`⚠️ Command already running: ${command}`)
|
|
417
|
+
return
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const child = spawn('npx', ['sillyspec', ...command.split(' ')], {
|
|
421
|
+
cwd: projectPath,
|
|
422
|
+
shell: true,
|
|
423
|
+
env: { ...process.env, FORCE_COLOR: '1' }
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
runningProcesses.set(key, child)
|
|
427
|
+
|
|
428
|
+
child.stdout.on('data', (data) => {
|
|
429
|
+
onOutput(data.toString())
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
child.stderr.on('data', (data) => {
|
|
433
|
+
onOutput(data.toString())
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
child.on('close', (code) => {
|
|
437
|
+
runningProcesses.delete(key)
|
|
438
|
+
onComplete(code)
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
return () => child.kill()
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
- [ ] 完善 `handleApi` 中的 `/api/project/:name` 路由,调用 `parseProjectState` 返回完整数据
|
|
446
|
+
- [ ] 完善 `handleMessage`,处理 `cli:execute` 消息,调用 `executeCommand` 并通过 `broadcast` 推送输出
|
|
447
|
+
- [ ] 验证:通过 WebSocket 发送 `{ type: 'cli:execute', project: 'test', command: 'progress status' }`,收到 `cli:output` 和 `cli:complete`
|
|
448
|
+
- [ ] git commit -m "feat(dashboard): REST API + CLI executor"
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
## Task 5: 前端三栏布局 + 状态管理
|
|
453
|
+
|
|
454
|
+
**文件:**
|
|
455
|
+
- 新建:`packages/dashboard/src/composables/useDashboard.js`
|
|
456
|
+
- 新建:`packages/dashboard/src/composables/useWebSocket.js`
|
|
457
|
+
- 新建:`packages/dashboard/src/composables/useKeyboard.js`
|
|
458
|
+
- 修改:`packages/dashboard/src/App.vue`
|
|
459
|
+
- 新建:`packages/dashboard/src/components/ProjectList.vue`
|
|
460
|
+
|
|
461
|
+
**步骤:**
|
|
462
|
+
|
|
463
|
+
- [ ] 创建 `packages/dashboard/src/composables/useWebSocket.js`:
|
|
464
|
+
```javascript
|
|
465
|
+
import { ref, onMounted, onUnmounted } from 'vue'
|
|
466
|
+
|
|
467
|
+
export function useWebSocket(url = `ws://${location.host}`) {
|
|
468
|
+
const connected = ref(false)
|
|
469
|
+
let ws = null
|
|
470
|
+
const handlers = new Map()
|
|
471
|
+
|
|
472
|
+
function connect() {
|
|
473
|
+
ws = new WebSocket(url)
|
|
474
|
+
ws.onopen = () => { connected.value = true }
|
|
475
|
+
ws.onclose = () => { connected.value = false; setTimeout(connect, 3000) }
|
|
476
|
+
ws.onmessage = (e) => {
|
|
477
|
+
const data = JSON.parse(e.data)
|
|
478
|
+
handlers.get(data.type)?.forEach(fn => fn(data))
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function on(type, handler) {
|
|
483
|
+
if (!handlers.has(type)) handlers.set(type, [])
|
|
484
|
+
handlers.get(type).push(handler)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function send(data) {
|
|
488
|
+
if (ws?.readyState === 1) ws.send(JSON.stringify(data))
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
onMounted(connect)
|
|
492
|
+
onUnmounted(() => ws?.close())
|
|
493
|
+
|
|
494
|
+
return { connected, on, send }
|
|
495
|
+
}
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
- [ ] 创建 `packages/dashboard/src/composables/useDashboard.js`:
|
|
499
|
+
```javascript
|
|
500
|
+
import { ref, reactive } from 'vue'
|
|
501
|
+
|
|
502
|
+
const state = reactive({
|
|
503
|
+
projects: [],
|
|
504
|
+
activeProject: null,
|
|
505
|
+
activeStep: null,
|
|
506
|
+
logs: [],
|
|
507
|
+
isPanelOpen: true
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
export function useDashboard() {
|
|
511
|
+
function selectProject(project) {
|
|
512
|
+
state.activeProject = project
|
|
513
|
+
state.activeStep = null
|
|
514
|
+
state.logs = []
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function appendLog(lines) {
|
|
518
|
+
state.logs.push(...lines)
|
|
519
|
+
// 保留最近 500 行
|
|
520
|
+
if (state.logs.length > 500) state.logs.splice(0, state.logs.length - 500)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return { state, selectProject, appendLog }
|
|
524
|
+
}
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
- [ ] 创建 `packages/dashboard/src/composables/useKeyboard.js`:
|
|
528
|
+
```javascript
|
|
529
|
+
import { onMounted, onUnmounted } from 'vue'
|
|
530
|
+
|
|
531
|
+
export function useKeyboard(handlers) {
|
|
532
|
+
function onKeyDown(e) {
|
|
533
|
+
// Cmd/Ctrl+K
|
|
534
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
535
|
+
e.preventDefault()
|
|
536
|
+
handlers['cmd+k']?.()
|
|
537
|
+
}
|
|
538
|
+
// j/k 导航
|
|
539
|
+
if (!e.metaKey && !e.ctrlKey && !e.altKey) {
|
|
540
|
+
if (e.key === 'j') handlers['j']?.()
|
|
541
|
+
if (e.key === 'k') handlers['k']?.()
|
|
542
|
+
if (e.key === 'Escape') handlers['escape']?.()
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
onMounted(() => document.addEventListener('keydown', onKeyDown))
|
|
547
|
+
onUnmounted(() => document.removeEventListener('keydown', onKeyDown))
|
|
548
|
+
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
- [ ] 创建 `packages/dashboard/src/components/ProjectList.vue`:
|
|
552
|
+
```vue
|
|
553
|
+
<template>
|
|
554
|
+
<div class="h-full flex flex-col">
|
|
555
|
+
<div class="p-4 text-sm text-[#00D4AA] font-semibold border-b border-[#30363D]">
|
|
556
|
+
📊 SillySpec
|
|
557
|
+
</div>
|
|
558
|
+
<div class="flex-1 overflow-auto">
|
|
559
|
+
<div v-for="project in projects" :key="project.path"
|
|
560
|
+
@click="$emit('select', project)"
|
|
561
|
+
:class="[
|
|
562
|
+
'px-4 py-3 cursor-pointer text-sm border-b border-[#30363D] hover:bg-[#1C2128] transition-colors duration-100',
|
|
563
|
+
isActive(project) ? 'bg-[#1C2128] text-[#00D4AA]' : 'text-gray-300'
|
|
564
|
+
]">
|
|
565
|
+
<div class="font-medium truncate">{{ project.name }}</div>
|
|
566
|
+
<div v-if="project.currentStage" class="text-xs text-[#6B7280] mt-1">
|
|
567
|
+
{{ project.currentStage }}
|
|
568
|
+
</div>
|
|
569
|
+
</div>
|
|
570
|
+
<div v-if="!projects.length" class="p-4 text-sm text-[#6B7280]">
|
|
571
|
+
未发现 SillySpec 项目
|
|
572
|
+
</div>
|
|
573
|
+
</div>
|
|
574
|
+
</div>
|
|
575
|
+
</template>
|
|
576
|
+
|
|
577
|
+
<script setup>
|
|
578
|
+
const props = defineProps({ projects: Array, activeName: String })
|
|
579
|
+
defineEmits(['select'])
|
|
580
|
+
|
|
581
|
+
function isActive(project) {
|
|
582
|
+
return project.name === props.activeName
|
|
583
|
+
}
|
|
584
|
+
</script>
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
- [ ] 更新 `App.vue`,集成 WebSocket + 状态管理 + 三栏布局
|
|
588
|
+
- [ ] 验证:启动后端,浏览器中左侧显示项目列表,点击项目高亮
|
|
589
|
+
- [ ] git commit -m "feat(dashboard): three-panel layout + state management"
|
|
590
|
+
|
|
591
|
+
---
|
|
592
|
+
|
|
593
|
+
## Task 6: Pipeline 视图 + StepCard 组件
|
|
594
|
+
|
|
595
|
+
**文件:**
|
|
596
|
+
- 新建:`packages/dashboard/src/components/PipelineView.vue`
|
|
597
|
+
- 新建:`packages/dashboard/src/components/StepCard.vue`
|
|
598
|
+
- 新建:`packages/dashboard/src/components/StageBadge.vue`
|
|
599
|
+
|
|
600
|
+
**步骤:**
|
|
601
|
+
|
|
602
|
+
- [ ] 创建 `packages/dashboard/src/components/StageBadge.vue`:
|
|
603
|
+
```vue
|
|
604
|
+
<template>
|
|
605
|
+
<span :class="badgeClass">
|
|
606
|
+
{{ icon }} {{ label }}
|
|
607
|
+
</span>
|
|
608
|
+
</template>
|
|
609
|
+
|
|
610
|
+
<script setup>
|
|
611
|
+
import { computed } from 'vue'
|
|
612
|
+
|
|
613
|
+
const props = defineProps({ status: String })
|
|
614
|
+
|
|
615
|
+
const configs = {
|
|
616
|
+
completed: { icon: '✅', label: '已完成', class: 'bg-green-900/30 text-green-400' },
|
|
617
|
+
'in-progress': { icon: '⏳', label: '进行中', class: 'bg-[#00D4AA]/20 text-[#00D4AA]' },
|
|
618
|
+
blocked: { icon: '🟡', label: '阻塞', class: 'bg-yellow-900/30 text-yellow-400' },
|
|
619
|
+
failed: { icon: '🔴', label: '失败', class: 'bg-red-900/30 text-red-400' },
|
|
620
|
+
pending: { icon: '⬜', label: '未开始', class: 'bg-gray-800 text-gray-500' }
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const config = computed(() => configs[props.status] || configs.pending)
|
|
624
|
+
const icon = computed(() => config.value.icon)
|
|
625
|
+
const label = computed(() => config.value.label)
|
|
626
|
+
const badgeClass = computed(() => `inline-flex items-center gap-1 px-2 py-1 rounded text-xs ${config.value.class}`)
|
|
627
|
+
</script>
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
- [ ] 创建 `packages/dashboard/src/components/StepCard.vue`:
|
|
631
|
+
三级信息密度实现:默认显示标题,hover 显示摘要,点击 emit select 事件。参考 design.md 中的模板。
|
|
632
|
+
|
|
633
|
+
- [ ] 创建 `packages/dashboard/src/components/PipelineView.vue`:
|
|
634
|
+
纵向 pipeline,四个阶段(brainstorm/plan/execute/verify),每个阶段内显示步骤卡片。下方显示时间线(步骤耗时列表)。
|
|
635
|
+
|
|
636
|
+
- [ ] 验证:选择一个有 STATE.md 的项目,pipeline 正确显示四个阶段状态
|
|
637
|
+
- [ ] git commit -m "feat(dashboard): pipeline view + step cards"
|
|
638
|
+
|
|
639
|
+
---
|
|
640
|
+
|
|
641
|
+
## Task 7: 日志流 + 命令面板
|
|
642
|
+
|
|
643
|
+
**文件:**
|
|
644
|
+
- 新建:`packages/dashboard/src/components/LogStream.vue`
|
|
645
|
+
- 新建:`packages/dashboard/src/components/CommandPalette.vue`
|
|
646
|
+
- 新建:`packages/dashboard/src/components/DetailPanel.vue`
|
|
647
|
+
- 新建:`packages/dashboard/src/components/ActionBar.vue`
|
|
648
|
+
|
|
649
|
+
**步骤:**
|
|
650
|
+
|
|
651
|
+
- [ ] 创建 `packages/dashboard/src/components/LogStream.vue`:
|
|
652
|
+
- 等宽字体(font-mono-log)
|
|
653
|
+
- 新行淡入动画
|
|
654
|
+
- 顶部搜索框,实时过滤
|
|
655
|
+
- 自动滚动到底部,用户上翻时暂停
|
|
656
|
+
- 最大 500 行,超出裁剪
|
|
657
|
+
|
|
658
|
+
- [ ] 创建 `packages/dashboard/src/components/CommandPalette.vue`:
|
|
659
|
+
- `Cmd+K` 打开,`Escape` 关闭
|
|
660
|
+
- 模糊搜索项目名、阶段名
|
|
661
|
+
- 列表用上下键选择,Enter 确认
|
|
662
|
+
- 遮罩层 + 居中弹窗
|
|
663
|
+
|
|
664
|
+
- [ ] 创建 `packages/dashboard/src/components/DetailPanel.vue`:
|
|
665
|
+
- 右侧面板,可收起
|
|
666
|
+
- 上半部分:步骤详情(结论、决策、用户原话)
|
|
667
|
+
- 下半部分:LogStream 组件
|
|
668
|
+
|
|
669
|
+
- [ ] 创建 `packages/dashboard/src/components/ActionBar.vue`:
|
|
670
|
+
- "下一步" 按钮 → 发送 `cli:execute` 消息
|
|
671
|
+
- 显示当前执行状态(空闲/执行中/完成)
|
|
672
|
+
|
|
673
|
+
- [ ] 验证:搜索日志、命令面板跳转项目、点击下一步按钮
|
|
674
|
+
- [ ] git commit -m "feat(dashboard): log stream + command palette + detail panel"
|
|
675
|
+
|
|
676
|
+
---
|
|
677
|
+
|
|
678
|
+
## Task 8: CLI 集成 + 构建优化
|
|
679
|
+
|
|
680
|
+
**文件:**
|
|
681
|
+
- 修改:`src/index.js`(新增 dashboard 命令)
|
|
682
|
+
- 修改:`printUsage`(添加 dashboard 用法)
|
|
683
|
+
- 修改:`packages/dashboard/vite.config.js`(生产构建优化)
|
|
684
|
+
|
|
685
|
+
**步骤:**
|
|
686
|
+
|
|
687
|
+
- [ ] 在 `src/index.js` 的 `switch (command)` 中新增:
|
|
688
|
+
```javascript
|
|
689
|
+
case 'dashboard': {
|
|
690
|
+
const portIdx = args.indexOf('--port')
|
|
691
|
+
const port = portIdx >= 0 && args[portIdx + 1] ? parseInt(args[portIdx + 1]) : 3456
|
|
692
|
+
const noOpen = args.includes('--no-open')
|
|
693
|
+
const { startServer } = await import('../packages/dashboard/server/index.js')
|
|
694
|
+
await startServer({ port, open: !noOpen })
|
|
695
|
+
break
|
|
696
|
+
}
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
- [ ] 在 `printUsage` 中添加:
|
|
700
|
+
```
|
|
701
|
+
sillyspec dashboard 启动可视化仪表盘
|
|
702
|
+
[--port <number>] 端口号(默认 3456)
|
|
703
|
+
[--no-open] 不自动打开浏览器
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
- [ ] 执行 `cd packages/dashboard && npm run build`,确认 dist/ 生成
|
|
707
|
+
- [ ] 修改 `server/index.js`,生产模式下 serve `dist/` 静态文件:
|
|
708
|
+
```javascript
|
|
709
|
+
import { readFile } from 'fs/promises'
|
|
710
|
+
|
|
711
|
+
// 在 createServer handler 中:
|
|
712
|
+
if (!req.url?.startsWith('/api/')) {
|
|
713
|
+
try {
|
|
714
|
+
const html = await readFile(join(__dirname, '../dist/index.html'), 'utf8')
|
|
715
|
+
res.writeHead(200, { 'Content-Type': 'text/html' })
|
|
716
|
+
res.end(html)
|
|
717
|
+
return
|
|
718
|
+
} catch {
|
|
719
|
+
res.writeHead(404)
|
|
720
|
+
res.end('Run `npm run build` first.')
|
|
721
|
+
return
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
- [ ] 验证:全局安装后执行 `sillyspec dashboard`,浏览器自动打开,显示完整仪表盘
|
|
727
|
+
- [ ] git commit -m "feat(dashboard): CLI integration + production build"
|
|
728
|
+
|
|
729
|
+
---
|
|
730
|
+
|
|
731
|
+
## 自检门控
|
|
732
|
+
|
|
733
|
+
- [x] 每个 task 包含具体文件路径
|
|
734
|
+
- [x] 每个 task 包含验证命令和预期输出
|
|
735
|
+
- [x] 标注了 Wave 和执行顺序(Wave 1-4)
|
|
736
|
+
- [x] plan 与 design.md 的文件变更清单一致(21 个新增文件 + 2 个修改文件全部覆盖)
|
|
737
|
+
- [x] 代码示例包含完整可运行内容
|