sillyspec 3.7.14 → 3.7.16
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/skills/sillyspec-archive/SKILL.md +77 -0
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +591 -0
- package/.claude/skills/sillyspec-continue/SKILL.md +44 -0
- package/.claude/skills/sillyspec-execute/SKILL.md +233 -0
- package/.claude/skills/sillyspec-explore/SKILL.md +96 -0
- package/.claude/skills/sillyspec-export/SKILL.md +53 -0
- package/.claude/skills/sillyspec-init/SKILL.md +171 -0
- package/.claude/skills/sillyspec-plan/SKILL.md +263 -0
- package/.claude/skills/sillyspec-propose/SKILL.md +248 -0
- package/.claude/skills/sillyspec-quick/SKILL.md +102 -0
- package/.claude/skills/sillyspec-resume/SKILL.md +111 -0
- package/{templates/scan.md → .claude/skills/sillyspec-scan/SKILL.md} +22 -60
- package/.claude/skills/sillyspec-state/SKILL.md +54 -0
- package/.claude/skills/sillyspec-status/SKILL.md +131 -0
- package/.claude/skills/sillyspec-verify/SKILL.md +146 -0
- package/.claude/skills/sillyspec-workspace/SKILL.md +149 -0
- package/.sillyspec/changes/run-command-design/design.md +1230 -0
- package/.sillyspec/docs/sillyspec/scan/.gitkeep +0 -0
- package/.sillyspec/knowledge/INDEX.md +8 -0
- package/.sillyspec/knowledge/uncategorized.md +3 -0
- package/.sillyspec/projects/sillyspec.yaml +3 -0
- package/package.json +1 -1
- package/packages/dashboard/dist/assets/index-Bx0cgoK_.js +7446 -0
- package/packages/dashboard/dist/assets/index-DbkUSsNO.css +1 -0
- package/packages/dashboard/dist/index.html +2 -2
- package/packages/dashboard/package-lock.json +220 -0
- package/packages/dashboard/package.json +8 -5
- package/packages/dashboard/server/index.js +91 -3
- package/packages/dashboard/server/parser.js +252 -28
- package/packages/dashboard/src/App.vue +54 -8
- package/packages/dashboard/src/components/ActionBar.vue +23 -39
- package/packages/dashboard/src/components/CommandPalette.vue +40 -65
- package/packages/dashboard/src/components/DetailPanel.vue +68 -53
- package/packages/dashboard/src/components/DocPreview.vue +137 -20
- package/packages/dashboard/src/components/DocTree.vue +48 -26
- package/packages/dashboard/src/components/LogStream.vue +12 -32
- package/packages/dashboard/src/components/PipelineStage.vue +8 -8
- package/packages/dashboard/src/components/PipelineView.vue +35 -43
- package/packages/dashboard/src/components/ProjectList.vue +51 -77
- package/packages/dashboard/src/components/ProjectOverview.vue +178 -0
- package/packages/dashboard/src/components/StageBadge.vue +13 -13
- package/packages/dashboard/src/components/StepCard.vue +11 -11
- package/packages/dashboard/src/components/detail/DocsDetail.vue +48 -0
- package/packages/dashboard/src/components/detail/GitDetail.vue +61 -0
- package/packages/dashboard/src/components/detail/TechDetail.vue +43 -0
- package/packages/dashboard/src/main.js +4 -1
- package/packages/dashboard/src/style.css +14 -14
- package/src/index.js +55 -8
- package/src/init.js +69 -196
- package/src/migrate.js +1 -18
- package/src/progress.js +279 -281
- package/src/run.js +320 -0
- package/src/setup.js +1 -9
- package/src/stages/brainstorm.js +210 -0
- package/src/stages/execute.js +190 -0
- package/src/stages/index.js +22 -0
- package/src/stages/plan.js +118 -0
- package/src/stages/propose.js +115 -0
- package/src/stages/verify.js +98 -0
- package/packages/dashboard/dist/assets/index-hNnQCobe.css +0 -1
- package/packages/dashboard/dist/assets/index-qgPzQGjk.js +0 -17
- package/templates/archive.md +0 -121
- package/templates/brainstorm.md +0 -246
- package/templates/commit.md +0 -123
- package/templates/continue.md +0 -32
- package/templates/execute.md +0 -314
- package/templates/explore.md +0 -60
- package/templates/export.md +0 -21
- package/templates/init.md +0 -61
- package/templates/plan.md +0 -157
- package/templates/progress-format.md +0 -90
- package/templates/propose.md +0 -73
- package/templates/quick.md +0 -135
- package/templates/resume-dialog.md +0 -55
- package/templates/resume.md +0 -53
- package/templates/scan-quick.md +0 -49
- package/templates/skills/playwright-e2e/SKILL.md +0 -340
- package/templates/status.md +0 -72
- package/templates/verify.md +0 -253
- package/templates/workspace-sync.md +0 -89
- package/templates/workspace.md +0 -67
- /package/.sillyspec/{docs/sillyspec/brainstorm → changes/brainstorm-archive}/2026-04-05-dashboard-design.md +0 -0
- /package/.sillyspec/{docs/sillyspec/brainstorm → changes/brainstorm-archive}/2026-04-05-unified-docs-design.md +0 -0
- /package/.sillyspec/{specs/2026-04-05-dashboard-design.md → changes/dashboard/design.md.braindraft} +0 -0
- /package/.sillyspec/{specs/2026-04-05-unified-docs-design.md → changes/unified-docs-design/design.md} +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { readFileSync, existsSync, readdirSync } from 'fs'
|
|
1
|
+
import { readFileSync, existsSync, readdirSync, statSync } from 'fs'
|
|
2
|
+
import { execSync } from 'child_process'
|
|
2
3
|
import { join } from 'path'
|
|
3
4
|
import { fileURLToPath } from 'url'
|
|
4
5
|
import { dirname } from 'path'
|
|
@@ -10,6 +11,243 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
|
10
11
|
* @param {string} projectPath - Path to the project directory
|
|
11
12
|
* @returns {object} Docs tree grouped by type
|
|
12
13
|
*/
|
|
14
|
+
// Known framework detection keywords in package.json dependencies
|
|
15
|
+
const FRAMEWORK_PATTERNS = [
|
|
16
|
+
{ keys: ['react', 'react-dom'], name: 'React' },
|
|
17
|
+
{ keys: ['vue'], name: 'Vue' },
|
|
18
|
+
{ keys: ['next'], name: 'Next.js' },
|
|
19
|
+
{ keys: ['nuxt'], name: 'Nuxt' },
|
|
20
|
+
{ keys: ['express'], name: 'Express' },
|
|
21
|
+
{ keys: ['koa'], name: 'Koa' },
|
|
22
|
+
{ keys: ['fastify'], name: 'Fastify' },
|
|
23
|
+
{ keys: ['nestjs', '@nestjs/core'], name: 'NestJS' },
|
|
24
|
+
{ keys: ['svelte'], name: 'Svelte' },
|
|
25
|
+
{ keys: ['astro'], name: 'Astro' },
|
|
26
|
+
{ keys: ['vite'], name: 'Vite' },
|
|
27
|
+
{ keys: ['webpack'], name: 'Webpack' },
|
|
28
|
+
{ keys: ['typescript'], name: 'TypeScript' },
|
|
29
|
+
{ keys: ['tailwindcss'], name: 'Tailwind' },
|
|
30
|
+
{ keys: ['prisma'], name: 'Prisma' },
|
|
31
|
+
{ keys: ['drizzle-orm'], name: 'Drizzle' },
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse project overview info
|
|
36
|
+
* @param {string} projectPath
|
|
37
|
+
* @returns {object} Overview data
|
|
38
|
+
*/
|
|
39
|
+
export function parseProjectOverview(projectPath) {
|
|
40
|
+
const result = {
|
|
41
|
+
techStack: [],
|
|
42
|
+
lastActive: null,
|
|
43
|
+
docStats: { design: 0, plan: 0, archive: 0, changes: 0, scan: 0, quicklog: 0, total: 0 },
|
|
44
|
+
git: { branch: '', lastCommit: '', dirtyCount: 0 }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// --- Tech stack ---
|
|
48
|
+
const pkgPath = join(projectPath, 'package.json')
|
|
49
|
+
if (existsSync(pkgPath)) {
|
|
50
|
+
try {
|
|
51
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
52
|
+
const deps = Object.keys({ ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) })
|
|
53
|
+
for (const pattern of FRAMEWORK_PATTERNS) {
|
|
54
|
+
if (pattern.keys.some(k => deps.includes(k))) {
|
|
55
|
+
result.techStack.push(pattern.name)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch {}
|
|
59
|
+
}
|
|
60
|
+
if (existsSync(join(projectPath, 'pom.xml'))) {
|
|
61
|
+
result.techStack.push('Java')
|
|
62
|
+
try {
|
|
63
|
+
const content = readFileSync(join(projectPath, 'pom.xml'), 'utf-8')
|
|
64
|
+
if (content.includes('spring-boot')) result.techStack.push('Spring Boot')
|
|
65
|
+
} catch {}
|
|
66
|
+
}
|
|
67
|
+
if (existsSync(join(projectPath, 'build.gradle')) || existsSync(join(projectPath, 'build.gradle.kts'))) {
|
|
68
|
+
if (!result.techStack.includes('Java')) result.techStack.push('Gradle')
|
|
69
|
+
}
|
|
70
|
+
if (existsSync(join(projectPath, 'requirements.txt')) || existsSync(join(projectPath, 'pyproject.toml'))) {
|
|
71
|
+
result.techStack.push('Python')
|
|
72
|
+
}
|
|
73
|
+
if (existsSync(join(projectPath, 'go.mod'))) {
|
|
74
|
+
result.techStack.push('Go')
|
|
75
|
+
}
|
|
76
|
+
if (result.techStack.length === 0) result.techStack = []
|
|
77
|
+
|
|
78
|
+
// --- Last active ---
|
|
79
|
+
const sillyspecDir = join(projectPath, '.sillyspec')
|
|
80
|
+
const progressPath = join(sillyspecDir, '.runtime', 'progress.json')
|
|
81
|
+
if (existsSync(progressPath)) {
|
|
82
|
+
try {
|
|
83
|
+
const progress = JSON.parse(readFileSync(progressPath, 'utf-8'))
|
|
84
|
+
if (progress.stages) {
|
|
85
|
+
for (const stageData of Object.values(progress.stages)) {
|
|
86
|
+
if (stageData.lastActive && (!result.lastActive || new Date(stageData.lastActive) > new Date(result.lastActive))) {
|
|
87
|
+
result.lastActive = stageData.lastActive
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (progress.lastActive) result.lastActive = progress.lastActive
|
|
92
|
+
} catch {}
|
|
93
|
+
}
|
|
94
|
+
if (!result.lastActive) {
|
|
95
|
+
// Fallback: most recently modified file in .sillyspec
|
|
96
|
+
try {
|
|
97
|
+
const findRecent = (dir) => {
|
|
98
|
+
let latest = null
|
|
99
|
+
if (!existsSync(dir)) return latest
|
|
100
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
101
|
+
const p = join(dir, entry.name)
|
|
102
|
+
try {
|
|
103
|
+
const s = statSync(p)
|
|
104
|
+
if (entry.isDirectory()) {
|
|
105
|
+
const sub = findRecent(p)
|
|
106
|
+
if (sub && (!latest || sub > latest)) latest = sub
|
|
107
|
+
} else if (!latest || s.mtimeMs > latest) {
|
|
108
|
+
latest = s.mtimeMs
|
|
109
|
+
}
|
|
110
|
+
} catch {}
|
|
111
|
+
}
|
|
112
|
+
return latest
|
|
113
|
+
}
|
|
114
|
+
const ts = findRecent(sillyspecDir)
|
|
115
|
+
if (ts) result.lastActive = new Date(ts).toISOString()
|
|
116
|
+
} catch {}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// --- Doc stats ---
|
|
120
|
+
const docsDir = join(sillyspecDir, 'docs')
|
|
121
|
+
if (existsSync(docsDir)) {
|
|
122
|
+
const typeMap = { brainstorm: 'design', plan: 'plan', archive: 'archive', changes: 'changes', scan: 'scan', quicklog: 'quicklog' }
|
|
123
|
+
try {
|
|
124
|
+
for (const projDir of readdirSync(docsDir, { withFileTypes: true }).filter(d => d.isDirectory())) {
|
|
125
|
+
for (const typeDir of readdirSync(join(docsDir, projDir.name), { withFileTypes: true }).filter(d => d.isDirectory())) {
|
|
126
|
+
const key = typeMap[typeDir.name]
|
|
127
|
+
if (!key) continue
|
|
128
|
+
const count = countMdFiles(join(docsDir, projDir.name, typeDir.name))
|
|
129
|
+
result.docStats[key] += count
|
|
130
|
+
result.docStats.total += count
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} catch {}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// --- Git info ---
|
|
137
|
+
try {
|
|
138
|
+
result.git.lastCommit = execSync('git log -1 --format=%s', { cwd: projectPath, encoding: 'utf-8' }).trim()
|
|
139
|
+
} catch {}
|
|
140
|
+
try {
|
|
141
|
+
result.git.branch = execSync('git branch --show-current', { cwd: projectPath, encoding: 'utf-8' }).trim()
|
|
142
|
+
} catch {}
|
|
143
|
+
try {
|
|
144
|
+
result.git.dirtyCount = parseInt(execSync('git status --porcelain', { cwd: projectPath, encoding: 'utf-8' }).trim().split('\n').filter(Boolean).length, 10) || 0
|
|
145
|
+
} catch {}
|
|
146
|
+
|
|
147
|
+
return result
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function parseGitDetail(projectPath) {
|
|
151
|
+
const result = { branch: '', commits: [], untracked: [] }
|
|
152
|
+
try {
|
|
153
|
+
result.branch = execSync('git branch --show-current', { cwd: projectPath, encoding: 'utf-8' }).trim()
|
|
154
|
+
} catch {}
|
|
155
|
+
try {
|
|
156
|
+
const log = execSync('git log -5 --format=%h|%s|%an|%aI', { cwd: projectPath, encoding: 'utf-8' }).trim()
|
|
157
|
+
result.commits = log.split('\n').filter(Boolean).map(line => {
|
|
158
|
+
const [hash, message, author, date] = line.split('|')
|
|
159
|
+
return { hash, message, author, date }
|
|
160
|
+
})
|
|
161
|
+
} catch {}
|
|
162
|
+
try {
|
|
163
|
+
const status = execSync('git status --porcelain', { cwd: projectPath, encoding: 'utf-8' }).trim()
|
|
164
|
+
result.untracked = status.split('\n').filter(Boolean).map(line => ({
|
|
165
|
+
status: line.slice(0, 2).trim(),
|
|
166
|
+
file: line.slice(3)
|
|
167
|
+
}))
|
|
168
|
+
} catch {}
|
|
169
|
+
return result
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function parseTechStackDetail(projectPath) {
|
|
173
|
+
const result = { frameworks: [], dependencies: {}, devDependencies: {} }
|
|
174
|
+
const pkgPath = join(projectPath, 'package.json')
|
|
175
|
+
if (existsSync(pkgPath)) {
|
|
176
|
+
try {
|
|
177
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
178
|
+
result.dependencies = pkg.dependencies || {}
|
|
179
|
+
result.devDependencies = pkg.devDependencies || {}
|
|
180
|
+
const allDeps = Object.keys({ ...result.dependencies, ...result.devDependencies })
|
|
181
|
+
for (const pattern of FRAMEWORK_PATTERNS) {
|
|
182
|
+
if (pattern.keys.some(k => allDeps.includes(k))) {
|
|
183
|
+
result.frameworks.push(pattern.name)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} catch {}
|
|
187
|
+
}
|
|
188
|
+
return result
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function parseDocsList(projectPath) {
|
|
192
|
+
const sillyspecDir = join(projectPath, '.sillyspec')
|
|
193
|
+
const docsDir = join(sillyspecDir, 'docs')
|
|
194
|
+
const groups = []
|
|
195
|
+
if (!existsSync(docsDir)) return groups
|
|
196
|
+
const typeMap = {
|
|
197
|
+
brainstorm: { label: '设计文档', icon: '📋' },
|
|
198
|
+
plan: { label: '实现计划', icon: '📐' },
|
|
199
|
+
archive: { label: '已归档', icon: '📦' },
|
|
200
|
+
changes: { label: '当前变更', icon: '⚙️' },
|
|
201
|
+
scan: { label: '架构文档', icon: '🔍' },
|
|
202
|
+
quicklog: { label: '快速修复', icon: '⚡' }
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
for (const projDir of readdirSync(docsDir, { withFileTypes: true }).filter(d => d.isDirectory())) {
|
|
206
|
+
for (const typeDir of readdirSync(join(docsDir, projDir.name), { withFileTypes: true }).filter(d => d.isDirectory())) {
|
|
207
|
+
const cfg = typeMap[typeDir.name]
|
|
208
|
+
if (!cfg) continue
|
|
209
|
+
const files = listFilesRecursive(join(docsDir, projDir.name, typeDir.name))
|
|
210
|
+
if (files.length) {
|
|
211
|
+
groups.push({ key: typeDir.name, label: cfg.label, icon: cfg.icon, files })
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
} catch {}
|
|
216
|
+
return groups
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function listFilesRecursive(dir) {
|
|
220
|
+
const files = []
|
|
221
|
+
try {
|
|
222
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
223
|
+
const p = join(dir, entry.name)
|
|
224
|
+
if (entry.isDirectory()) {
|
|
225
|
+
const sub = listFilesRecursive(p)
|
|
226
|
+
for (const f of sub) f.name = entry.name + '/' + f.name
|
|
227
|
+
files.push(...sub)
|
|
228
|
+
} else if (entry.name.endsWith('.md')) {
|
|
229
|
+
const s = statSync(p)
|
|
230
|
+
files.push({ name: entry.name, path: p, size: s.size, mtime: s.mtime.toISOString() })
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
} catch {}
|
|
234
|
+
return files
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function countMdFiles(dir) {
|
|
238
|
+
let count = 0
|
|
239
|
+
try {
|
|
240
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
241
|
+
if (entry.isDirectory()) {
|
|
242
|
+
count += countMdFiles(join(dir, entry.name))
|
|
243
|
+
} else if (entry.name.endsWith('.md')) {
|
|
244
|
+
count++
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
} catch {}
|
|
248
|
+
return count
|
|
249
|
+
}
|
|
250
|
+
|
|
13
251
|
export function parseDocsTree(projectPath) {
|
|
14
252
|
const sillyspecDir = join(projectPath, '.sillyspec')
|
|
15
253
|
const docsDir = join(sillyspecDir, 'docs')
|
|
@@ -97,43 +335,29 @@ export function parseProjectState(projectPath) {
|
|
|
97
335
|
return null
|
|
98
336
|
}
|
|
99
337
|
|
|
100
|
-
let currentStage = '
|
|
338
|
+
let currentStage = ''
|
|
101
339
|
let nextStep = null
|
|
102
340
|
let progress = { stages: {} }
|
|
103
341
|
let stages = []
|
|
104
342
|
let specs = []
|
|
105
343
|
let lastActive = null
|
|
106
344
|
|
|
107
|
-
// Read
|
|
108
|
-
const statePath = join(sillyspecDir, 'STATE.md')
|
|
109
|
-
if (existsSync(statePath)) {
|
|
110
|
-
try {
|
|
111
|
-
const stateContent = readFileSync(statePath, 'utf-8')
|
|
112
|
-
const stageMatch = stateContent.match(/当前阶段[::]\s*(\w+)/) || stateContent.match(/current_stage:\s*(\w+)/i)
|
|
113
|
-
const stepMatch = stateContent.match(/下一步[::]\s*(.+)/) || stateContent.match(/next_step:\s*(.+)/i)
|
|
114
|
-
|
|
115
|
-
if (stageMatch) currentStage = stageMatch[1]
|
|
116
|
-
if (stepMatch) nextStep = stepMatch[1].trim()
|
|
117
|
-
} catch (err) {
|
|
118
|
-
// State file exists but couldn't be read
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Read progress.json from .runtime directory
|
|
345
|
+
// Read progress.json for current stage
|
|
123
346
|
const progressPath = join(sillyspecDir, '.runtime', 'progress.json')
|
|
124
347
|
if (existsSync(progressPath)) {
|
|
125
348
|
try {
|
|
126
|
-
const
|
|
127
|
-
progress =
|
|
128
|
-
|
|
349
|
+
const progressData = JSON.parse(readFileSync(progressPath, 'utf-8'))
|
|
350
|
+
progress = progressData
|
|
351
|
+
currentStage = progressData.currentStage || ''
|
|
352
|
+
stages = Object.keys(progressData.stages || {})
|
|
129
353
|
|
|
130
|
-
// Find last active
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
354
|
+
// Find last active
|
|
355
|
+
if (progressData.lastActive) lastActive = progressData.lastActive
|
|
356
|
+
if (progressData.stages) {
|
|
357
|
+
for (const [stageName, stageData] of Object.entries(progressData.stages)) {
|
|
358
|
+
if (stageData.lastActive || stageData.startedAt) {
|
|
359
|
+
const t = stageData.lastActive || stageData.startedAt
|
|
360
|
+
if (!lastActive || new Date(t) > new Date(lastActive)) lastActive = t
|
|
137
361
|
}
|
|
138
362
|
}
|
|
139
363
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
2
|
+
<n-config-provider :theme-overrides="themeOverrides">
|
|
3
|
+
<div class="h-screen w-screen flex flex-col overflow-hidden font-[DM_Sans,sans-serif] relative" style="background-color: #F5F5F7;">
|
|
3
4
|
<!-- Ambient background -->
|
|
4
5
|
<div class="absolute inset-0 pointer-events-none" style="background: radial-gradient(ellipse 60% 40% at 10% 20%, rgba(251,191,36,0.04) 0%, transparent 70%), radial-gradient(ellipse 50% 50% at 90% 80%, rgba(251,191,36,0.02) 0%, transparent 70%);" />
|
|
5
6
|
|
|
@@ -11,7 +12,7 @@
|
|
|
11
12
|
<!-- Left: Project List -->
|
|
12
13
|
<aside
|
|
13
14
|
class="flex-shrink-0 relative overflow-hidden"
|
|
14
|
-
:style="`width: ${leftWidth}px; background: #
|
|
15
|
+
:style="`width: ${leftWidth}px; background: #FFFFFF; border-right: none;`"
|
|
15
16
|
>
|
|
16
17
|
<ProjectList
|
|
17
18
|
:projects="dashboard.state.projects"
|
|
@@ -26,13 +27,14 @@
|
|
|
26
27
|
|
|
27
28
|
<!-- Left ↔ Center resize handle -->
|
|
28
29
|
<div
|
|
29
|
-
class="w-[2px] flex-shrink-0 cursor-col-resize hover:bg-[#
|
|
30
|
+
class="w-[2px] flex-shrink-0 cursor-col-resize hover:bg-[#D97706] active:bg-[#D97706] relative z-20"
|
|
30
31
|
style="background: #2A3040;"
|
|
31
32
|
@mousedown="startDragLeft"
|
|
32
33
|
/>
|
|
33
34
|
|
|
34
35
|
<!-- Center: Pipeline View -->
|
|
35
|
-
<main class="flex-1 overflow-hidden accent-stripe" style="min-width: 300px;">
|
|
36
|
+
<main class="flex-1 overflow-hidden accent-stripe flex flex-col" style="min-width: 300px;">
|
|
37
|
+
<ProjectOverview :project="dashboard.state.activeProject" @show-detail="handleShowDetail" />
|
|
36
38
|
<PipelineView
|
|
37
39
|
:project="dashboard.state.activeProject"
|
|
38
40
|
:active-step="dashboard.state.activeStep"
|
|
@@ -50,7 +52,7 @@
|
|
|
50
52
|
<!-- Center ↔ Right resize handle -->
|
|
51
53
|
<div
|
|
52
54
|
v-if="dashboard.state.isPanelOpen"
|
|
53
|
-
class="w-[2px] flex-shrink-0 cursor-col-resize hover:bg-[#
|
|
55
|
+
class="w-[2px] flex-shrink-0 cursor-col-resize hover:bg-[#D97706] active:bg-[#D97706] relative z-20"
|
|
54
56
|
style="background: #2A3040;"
|
|
55
57
|
@mousedown="startDragRight"
|
|
56
58
|
/>
|
|
@@ -61,14 +63,17 @@
|
|
|
61
63
|
'flex-shrink-0 relative overflow-hidden',
|
|
62
64
|
dashboard.state.isPanelOpen ? '' : 'w-0'
|
|
63
65
|
]"
|
|
64
|
-
:style="dashboard.state.isPanelOpen ? `width: ${rightWidth}px; background: #
|
|
66
|
+
:style="dashboard.state.isPanelOpen ? `width: ${rightWidth}px; background: #FFFFFF; transition: none;` : 'background: #FFFFFF;'"
|
|
65
67
|
>
|
|
66
68
|
<DetailPanel
|
|
67
69
|
:is-open="dashboard.state.isPanelOpen"
|
|
68
70
|
:active-step="dashboard.state.activeStep"
|
|
69
71
|
:logs="dashboard.state.logs"
|
|
70
|
-
|
|
72
|
+
:detail-type="detailType"
|
|
73
|
+
:detail-data="detailData"
|
|
74
|
+
@close="handleDetailClose"
|
|
71
75
|
@clear-logs="dashboard.clearLogs"
|
|
76
|
+
@open-doc-file="handleOpenDocFromDetail"
|
|
72
77
|
/>
|
|
73
78
|
</aside>
|
|
74
79
|
</div>
|
|
@@ -94,10 +99,11 @@
|
|
|
94
99
|
@select-stage="handleSelectStage"
|
|
95
100
|
/>
|
|
96
101
|
</div>
|
|
102
|
+
</n-config-provider>
|
|
97
103
|
</template>
|
|
98
104
|
|
|
99
105
|
<script setup>
|
|
100
|
-
import { ref, onMounted } from 'vue'
|
|
106
|
+
import { ref, onMounted, readonly } from 'vue'
|
|
101
107
|
import { useWebSocket } from './composables/useWebSocket.js'
|
|
102
108
|
import { useDashboard } from './composables/useDashboard.js'
|
|
103
109
|
import { useDashboardKeyboard } from './composables/useKeyboard.js'
|
|
@@ -105,6 +111,7 @@ import ProjectList from './components/ProjectList.vue'
|
|
|
105
111
|
import PipelineView from './components/PipelineView.vue'
|
|
106
112
|
import DetailPanel from './components/DetailPanel.vue'
|
|
107
113
|
import ActionBar from './components/ActionBar.vue'
|
|
114
|
+
import ProjectOverview from './components/ProjectOverview.vue'
|
|
108
115
|
import CommandPalette from './components/CommandPalette.vue'
|
|
109
116
|
|
|
110
117
|
// Composables
|
|
@@ -113,6 +120,8 @@ const dashboard = useDashboard()
|
|
|
113
120
|
const isCommandPaletteOpen = ref(false)
|
|
114
121
|
const executionResult = ref(null)
|
|
115
122
|
const scanPaths = ref([])
|
|
123
|
+
const detailType = ref(null)
|
|
124
|
+
const detailData = ref(null)
|
|
116
125
|
|
|
117
126
|
// Panel resize state
|
|
118
127
|
const STORAGE_KEY = 'dashboard-panel-widths'
|
|
@@ -264,6 +273,43 @@ function handleAddScanPath(path) {
|
|
|
264
273
|
function handleRemoveScanPath(path) {
|
|
265
274
|
ws.send({ type: 'scan:remove-path', data: { path } })
|
|
266
275
|
}
|
|
276
|
+
|
|
277
|
+
async function handleShowDetail(type) {
|
|
278
|
+
const project = dashboard.state.activeProject
|
|
279
|
+
if (!project?.path) return
|
|
280
|
+
detailType.value = type
|
|
281
|
+
detailData.value = null
|
|
282
|
+
dashboard.openPanel()
|
|
283
|
+
try {
|
|
284
|
+
const url = `/api/projects/${encodeURIComponent(project.path)}/detail?type=${type}`
|
|
285
|
+
const res = await fetch(url)
|
|
286
|
+
if (res.ok) detailData.value = await res.json()
|
|
287
|
+
} catch {}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function handleDetailClose() {
|
|
291
|
+
detailType.value = null
|
|
292
|
+
detailData.value = null
|
|
293
|
+
dashboard.closePanel()
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function handleOpenDocFromDetail(file) {
|
|
297
|
+
detailType.value = null
|
|
298
|
+
detailData.value = null
|
|
299
|
+
dashboard.setActiveTab('docs')
|
|
300
|
+
handleSelectDocFile(file)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const themeOverrides = {
|
|
304
|
+
common: {
|
|
305
|
+
primaryColor: '#D97706',
|
|
306
|
+
primaryColorHover: '#F59E0B',
|
|
307
|
+
primaryColorPressed: '#B45309',
|
|
308
|
+
borderRadius: '6px',
|
|
309
|
+
fontFamily: 'DM Sans, sans-serif',
|
|
310
|
+
fontFamilyMono: 'JetBrains Mono, monospace'
|
|
311
|
+
}
|
|
312
|
+
}
|
|
267
313
|
</script>
|
|
268
314
|
|
|
269
315
|
<style>
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="h-12 flex items-center justify-between px-5 relative" style="background: rgba(17,17,19,0.9); backdrop-filter: blur(20px); border-top: 1px solid #
|
|
2
|
+
<div class="h-12 flex items-center justify-between px-5 relative" style="background: rgba(17,17,19,0.9); backdrop-filter: blur(20px); border-top: 1px solid #F0F0F3;">
|
|
3
3
|
<!-- Ambient top glow -->
|
|
4
4
|
<div class="absolute inset-x-0 top-0 h-px" style="background: linear-gradient(90deg, transparent, rgba(251,191,36,0.1), transparent);"></div>
|
|
5
5
|
|
|
6
6
|
<!-- Left: Status -->
|
|
7
7
|
<div class="flex items-center gap-3">
|
|
8
8
|
<div v-if="project" class="flex items-center gap-2">
|
|
9
|
-
<span class="text-[11px] font-[JetBrains_Mono,monospace]" style="color: #
|
|
10
|
-
<span style="color: #
|
|
9
|
+
<span class="text-[11px] font-[JetBrains_Mono,monospace]" style="color: #6B7280;">{{ project.name }}</span>
|
|
10
|
+
<span style="color: #F0F0F3;">|</span>
|
|
11
11
|
<StageBadge v-if="project.state?.currentStage" :status="getProjectStatus()" :label="stageLabel()" />
|
|
12
12
|
</div>
|
|
13
|
-
<div v-else class="text-[10px] font-[JetBrains_Mono,monospace]" style="color: #
|
|
13
|
+
<div v-else class="text-[10px] font-[JetBrains_Mono,monospace]" style="color: #D1D1D6;">
|
|
14
14
|
未选择项目
|
|
15
15
|
</div>
|
|
16
16
|
</div>
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
<!-- Center: Execution -->
|
|
19
19
|
<div class="flex items-center gap-2">
|
|
20
20
|
<div v-if="isExecuting" class="flex items-center gap-2">
|
|
21
|
-
<div class="w-1 h-1 rounded-full animate-pulse-dot" style="background: #
|
|
22
|
-
<span class="text-[10px] font-[JetBrains_Mono,monospace]" style="color: #
|
|
21
|
+
<div class="w-1 h-1 rounded-full animate-pulse-dot" style="background: #D97706;" />
|
|
22
|
+
<span class="text-[10px] font-[JetBrains_Mono,monospace]" style="color: #D97706;">执行中...</span>
|
|
23
23
|
</div>
|
|
24
24
|
<div v-else-if="executionResult" class="flex items-center gap-1.5">
|
|
25
|
-
<span class="text-[10px] font-[JetBrains_Mono,monospace]" :style="{ color: executionResult.exitCode === 0 ? '#
|
|
25
|
+
<span class="text-[10px] font-[JetBrains_Mono,monospace]" :style="{ color: executionResult.exitCode === 0 ? '#16A34A' : '#DC2626' }">
|
|
26
26
|
{{ executionResult.exitCode === 0 ? '● 完成' : `● 失败 (${executionResult.exitCode})` }}
|
|
27
27
|
</span>
|
|
28
28
|
</div>
|
|
@@ -30,46 +30,30 @@
|
|
|
30
30
|
|
|
31
31
|
<!-- Right: Actions -->
|
|
32
32
|
<div class="flex items-center gap-1">
|
|
33
|
-
<button
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
>
|
|
41
|
-
<svg :class="['w-3.5 h-3.5 transition-transform duration-200', { 'rotate-180': !isPanelOpen }]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
42
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
|
43
|
-
</svg>
|
|
44
|
-
</button>
|
|
33
|
+
<n-button quaternary size="tiny" @click="$emit('toggle-panel')" title="切换详情面板">
|
|
34
|
+
<template #icon>
|
|
35
|
+
<svg :class="['w-3.5 h-3.5 transition-transform duration-200', { 'rotate-180': !isPanelOpen }]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
36
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
|
37
|
+
</svg>
|
|
38
|
+
</template>
|
|
39
|
+
</n-button>
|
|
45
40
|
|
|
46
|
-
<button
|
|
47
|
-
v-if="isExecuting"
|
|
48
|
-
@click="$emit('kill')"
|
|
49
|
-
class="px-2.5 py-1 rounded-sm text-[10px] font-[JetBrains_Mono,monospace] transition-colors duration-100"
|
|
50
|
-
style="background: rgba(239,68,68,0.08); border: 1px solid rgba(239,68,68,0.2); color: #EF4444;"
|
|
51
|
-
>
|
|
41
|
+
<n-button v-if="isExecuting" size="tiny" type="error" @click="$emit('kill')">
|
|
52
42
|
停止
|
|
53
|
-
</button>
|
|
43
|
+
</n-button>
|
|
54
44
|
|
|
55
|
-
<button
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
>
|
|
63
|
-
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
64
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
65
|
-
</svg>
|
|
66
|
-
</button>
|
|
45
|
+
<n-button quaternary size="tiny" @click="$emit('open-palette')" title="命令面板 (⌘K)">
|
|
46
|
+
<template #icon>
|
|
47
|
+
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
48
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
49
|
+
</svg>
|
|
50
|
+
</template>
|
|
51
|
+
</n-button>
|
|
67
52
|
</div>
|
|
68
53
|
</div>
|
|
69
54
|
</template>
|
|
70
55
|
|
|
71
56
|
<script setup>
|
|
72
|
-
import { computed } from 'vue'
|
|
73
57
|
import StageBadge from './StageBadge.vue'
|
|
74
58
|
|
|
75
59
|
const props = defineProps({
|
|
@@ -1,67 +1,49 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
|
15
|
-
</svg>
|
|
16
|
-
<input
|
|
17
|
-
ref="searchInput"
|
|
18
|
-
v-model="searchQuery"
|
|
19
|
-
type="text"
|
|
20
|
-
placeholder="搜索项目或命令..."
|
|
21
|
-
class="flex-1 bg-transparent border-none outline-none text-[12px] font-[JetBrains_Mono,monospace]"
|
|
22
|
-
style="color: #E4E4E7;"
|
|
23
|
-
@keydown="handleKeydown"
|
|
24
|
-
/>
|
|
25
|
-
<kbd v-if="searchQuery" class="text-[9px] px-1 py-0.5 rounded-sm font-mono-log" style="color: #8B8FA3; background: #0A0A0B; border: 1px solid #2A2A2D;">ESC</kbd>
|
|
26
|
-
</div>
|
|
27
|
-
</div>
|
|
2
|
+
<n-modal :show="isOpen" @update:show="$emit('close')" :mask-closable="true" transform-origin="center" style="max-width: 480px;">
|
|
3
|
+
<div class="overflow-hidden rounded-md" style="background: #FFFFFF; border: 1px solid #E5E5EA; box-shadow: 0 25px 60px rgba(0,0,0,0.1);">
|
|
4
|
+
<!-- Search -->
|
|
5
|
+
<div class="px-4 py-3" style="border-bottom: 1px solid #F0F0F3;">
|
|
6
|
+
<n-input v-model:value="searchQuery" placeholder="搜索项目或命令..." ref="searchInput" @keydown="handleKeydown" size="small">
|
|
7
|
+
<template #prefix>
|
|
8
|
+
<svg class="w-3.5 h-3.5" style="color: #6B7280;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
9
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
|
10
|
+
</svg>
|
|
11
|
+
</template>
|
|
12
|
+
</n-input>
|
|
13
|
+
</div>
|
|
28
14
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
15
|
+
<!-- Results -->
|
|
16
|
+
<div class="max-h-72 overflow-y-auto">
|
|
17
|
+
<n-empty v-if="filteredItems.length === 0" description="无结果" style="padding: 40px 0;" />
|
|
18
|
+
<div v-else class="py-0.5">
|
|
19
|
+
<div
|
|
20
|
+
v-for="(item, index) in filteredItems"
|
|
21
|
+
:key="item.id"
|
|
22
|
+
:class="['px-4 py-2.5 cursor-pointer transition-colors duration-100 flex items-center gap-3']"
|
|
23
|
+
:style="{ background: selectedIndex === index ? 'rgba(217,119,6,0.06)' : 'transparent' }"
|
|
24
|
+
@click="selectItem(item)"
|
|
25
|
+
@mouseenter="selectedIndex = index"
|
|
26
|
+
>
|
|
27
|
+
<div class="w-6 h-6 rounded-sm flex items-center justify-center text-[10px] font-[JetBrains_Mono,monospace] flex-shrink-0" style="background: #F0F0F3; border: 1px solid #F0F0F3; color: #6B7280;">
|
|
28
|
+
{{ item.type === 'project' ? '□' : '◇' }}
|
|
33
29
|
</div>
|
|
34
|
-
<div
|
|
35
|
-
<div
|
|
36
|
-
|
|
37
|
-
:key="item.id"
|
|
38
|
-
:class="['px-4 py-2.5 cursor-pointer transition-colors duration-100 flex items-center gap-3']"
|
|
39
|
-
:style="{ background: selectedIndex === index ? 'rgba(251,191,36,0.06)' : 'transparent' }"
|
|
40
|
-
@click="selectItem(item)"
|
|
41
|
-
@mouseenter="selectedIndex = index"
|
|
42
|
-
>
|
|
43
|
-
<div class="w-6 h-6 rounded-sm flex items-center justify-center text-[10px] font-[JetBrains_Mono,monospace] flex-shrink-0" style="background: #0A0A0B; border: 1px solid #1F1F22; color: #8B8FA3;">
|
|
44
|
-
{{ item.type === 'project' ? '□' : '◇' }}
|
|
45
|
-
</div>
|
|
46
|
-
<div class="flex-1 min-w-0">
|
|
47
|
-
<div class="text-[12px] font-medium truncate font-[JetBrains_Mono,monospace]" style="color: #E4E4E7;">{{ item.title }}</div>
|
|
48
|
-
<div class="text-[10px] truncate" style="color: #8B8FA3;">{{ item.subtitle }}</div>
|
|
49
|
-
</div>
|
|
50
|
-
<StageBadge v-if="item.status" :status="item.status" size="sm" />
|
|
51
|
-
</div>
|
|
30
|
+
<div class="flex-1 min-w-0">
|
|
31
|
+
<div class="text-[12px] font-medium truncate font-[JetBrains_Mono,monospace]" style="color: #1C1C1E;">{{ item.title }}</div>
|
|
32
|
+
<div class="text-[10px] truncate" style="color: #6B7280;">{{ item.subtitle }}</div>
|
|
52
33
|
</div>
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
<!-- Footer -->
|
|
56
|
-
<div class="px-4 py-2 flex items-center gap-4 text-[9px] font-mono-log" style="border-top: 1px solid #1F1F22; color: #3A3A3D;">
|
|
57
|
-
<span><kbd class="px-1 rounded-sm" style="background: #0A0A0B; border: 1px solid #1F1F22;">↑↓</kbd> 导航</span>
|
|
58
|
-
<span><kbd class="px-1 rounded-sm" style="background: #0A0A0B; border: 1px solid #1F1F22;">↵</kbd> 打开</span>
|
|
59
|
-
<span><kbd class="px-1 rounded-sm" style="background: #0A0A0B; border: 1px solid #1F1F22;">esc</kbd> 关闭</span>
|
|
34
|
+
<StageBadge v-if="item.status" :status="item.status" size="sm" />
|
|
60
35
|
</div>
|
|
61
36
|
</div>
|
|
62
37
|
</div>
|
|
63
|
-
|
|
64
|
-
|
|
38
|
+
|
|
39
|
+
<!-- Footer -->
|
|
40
|
+
<div class="px-4 py-2 flex items-center gap-4 text-[9px] font-mono-log" style="border-top: 1px solid #F0F0F3; color: #D1D1D6;">
|
|
41
|
+
<span><kbd class="px-1 rounded-sm" style="background: #F0F0F3; border: 1px solid #F0F0F3;">↑↓</kbd> 导航</span>
|
|
42
|
+
<span><kbd class="px-1 rounded-sm" style="background: #F0F0F3; border: 1px solid #F0F0F3;">↵</kbd> 打开</span>
|
|
43
|
+
<span><kbd class="px-1 rounded-sm" style="background: #F0F0F3; border: 1px solid #F0F0F3;">esc</kbd> 关闭</span>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</n-modal>
|
|
65
47
|
</template>
|
|
66
48
|
|
|
67
49
|
<script setup>
|
|
@@ -106,12 +88,5 @@ function handleKeydown(e) {
|
|
|
106
88
|
else if (e.key === 'Escape') { e.preventDefault(); close() }
|
|
107
89
|
}
|
|
108
90
|
watch(filteredItems, () => { selectedIndex.value = 0 })
|
|
109
|
-
watch(() => props.isOpen, (v) => { if (v) nextTick(() => { searchInput.value?.focus() }) })
|
|
91
|
+
watch(() => props.isOpen, (v) => { if (v) { nextTick(() => { searchInput.value?.focus() }) } })
|
|
110
92
|
</script>
|
|
111
|
-
|
|
112
|
-
<style scoped>
|
|
113
|
-
.backdrop-enter-active, .backdrop-leave-active { transition: opacity 150ms ease; }
|
|
114
|
-
.backdrop-enter-from, .backdrop-leave-to { opacity: 0; }
|
|
115
|
-
.palette-enter-active, .palette-leave-active { transition: all 200ms cubic-bezier(0.16, 1, 0.3, 1); }
|
|
116
|
-
.palette-enter-from, .palette-leave-to { opacity: 0; transform: translate(-50%, -8px) scale(0.98); }
|
|
117
|
-
</style>
|