sillyspec 3.8.7 → 3.9.1
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 +17 -0
- package/.claude/skills/sillyspec-auto/SKILL.md +78 -0
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +17 -0
- package/{templates/commit.md → .claude/skills/sillyspec-commit/SKILL.md} +32 -47
- package/.claude/skills/sillyspec-continue/SKILL.md +45 -0
- package/.claude/skills/sillyspec-doctor/SKILL.md +27 -0
- package/.claude/skills/sillyspec-execute/SKILL.md +17 -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 +170 -0
- package/.claude/skills/sillyspec-plan/SKILL.md +52 -0
- package/.claude/skills/sillyspec-propose/SKILL.md +17 -0
- package/.claude/skills/sillyspec-quick/SKILL.md +17 -0
- package/.claude/skills/sillyspec-resume/SKILL.md +111 -0
- package/.claude/skills/sillyspec-scan/SKILL.md +17 -0
- package/.claude/skills/sillyspec-state/SKILL.md +54 -0
- package/.claude/skills/sillyspec-status/SKILL.md +17 -0
- package/.claude/skills/sillyspec-verify/SKILL.md +17 -0
- package/.claude/skills/sillyspec-workspace/SKILL.md +149 -0
- package/.sillyspec/changes/archive/2026-04-08-derive-state/design.md +97 -0
- package/.sillyspec/changes/archive/2026-04-08-derive-state/plan.md +51 -0
- package/.sillyspec/changes/archive/2026-04-08-derive-state/proposal.md +29 -0
- package/.sillyspec/changes/archive/2026-04-08-derive-state/requirements.md +34 -0
- package/.sillyspec/changes/archive/2026-04-08-derive-state/tasks.md +13 -0
- package/.sillyspec/changes/archive/2026-04-08-derive-state/verify-result.md +43 -0
- package/.sillyspec/changes/auto-mode/design.md +50 -0
- package/.sillyspec/changes/auto-mode/proposal.md +19 -0
- package/.sillyspec/changes/auto-mode/requirements.md +21 -0
- package/.sillyspec/changes/auto-mode/tasks.md +7 -0
- package/.sillyspec/changes/brainstorm-archive/2026-04-05-unified-docs-design.md +199 -0
- package/.sillyspec/changes/dashboard/design.md.braindraft +206 -0
- package/.sillyspec/changes/run-command-design/design.md +1230 -0
- package/.sillyspec/changes/unified-docs-design/design.md +199 -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/README.md +12 -5
- package/package.json +9 -11
- package/packages/dashboard/dist/assets/index-CntACGUN.css +1 -0
- package/packages/dashboard/dist/assets/index-RsLVPAy7.js +7446 -0
- package/packages/dashboard/dist/index.html +3 -2
- package/packages/dashboard/package-lock.json +226 -6
- package/packages/dashboard/package.json +8 -5
- package/packages/dashboard/public/logo.jpg +0 -0
- package/packages/dashboard/server/executor.js +1 -1
- package/packages/dashboard/server/index.js +336 -113
- package/packages/dashboard/server/parser.js +333 -29
- package/packages/dashboard/server/watcher.js +203 -131
- package/packages/dashboard/src/App.vue +187 -11
- package/packages/dashboard/src/components/ActionBar.vue +26 -42
- 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 +160 -0
- package/packages/dashboard/src/components/DocTree.vue +58 -0
- package/packages/dashboard/src/components/LogStream.vue +13 -33
- package/packages/dashboard/src/components/PipelineStage.vue +8 -8
- package/packages/dashboard/src/components/PipelineView.vue +80 -45
- package/packages/dashboard/src/components/ProjectList.vue +103 -45
- 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 +15 -15
- 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/composables/useDashboard.js +20 -6
- package/packages/dashboard/src/composables/useKeyboard.js +6 -4
- package/packages/dashboard/src/main.js +4 -1
- package/packages/dashboard/src/style.css +17 -17
- package/src/index.js +136 -14
- package/src/init.js +83 -228
- package/src/migrate.js +117 -0
- package/src/progress.js +459 -0
- package/src/run.js +624 -0
- package/src/setup.js +2 -72
- package/src/stages/archive.js +54 -0
- package/src/stages/brainstorm.js +239 -0
- package/src/stages/doctor.js +303 -0
- package/src/stages/execute.js +262 -0
- package/src/stages/index.js +26 -0
- package/src/stages/plan.js +282 -0
- package/src/stages/propose.js +115 -0
- package/src/stages/quick.js +64 -0
- package/src/stages/scan.js +141 -0
- package/src/stages/status.js +65 -0
- package/src/stages/verify.js +135 -0
- package/docs/.vitepress/config.mts +0 -45
- package/docs/.vitepress/dist/404.html +0 -25
- package/docs/.vitepress/dist/assets/app.YytxICdd.js +0 -1
- package/docs/.vitepress/dist/assets/chunks/framework.Czhw_PXq.js +0 -19
- package/docs/.vitepress/dist/assets/chunks/theme.DusTRZQk.js +0 -1
- package/docs/.vitepress/dist/assets/index.md.C3VCvtQA.js +0 -1
- package/docs/.vitepress/dist/assets/index.md.C3VCvtQA.lean.js +0 -1
- package/docs/.vitepress/dist/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-cyrillic.By2_1cv3.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-greek-ext.1u6EdAuj.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-greek.DJ8dCoTZ.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-latin-ext.CN1xVJS-.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-latin.C2AdPX0b.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-vietnamese.BSbpV94h.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-greek.BBVDIX6e.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-latin.Di8DUHzh.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-vietnamese.BjW4sHH5.woff2 +0 -0
- package/docs/.vitepress/dist/assets/sillyspec_commands.md.CXFFsj08.js +0 -15
- package/docs/.vitepress/dist/assets/sillyspec_commands.md.CXFFsj08.lean.js +0 -1
- package/docs/.vitepress/dist/assets/sillyspec_dashboard.md.BuPXHqjX.js +0 -4
- package/docs/.vitepress/dist/assets/sillyspec_dashboard.md.BuPXHqjX.lean.js +0 -1
- package/docs/.vitepress/dist/assets/sillyspec_file-io.md.Cz3x7llx.js +0 -1
- package/docs/.vitepress/dist/assets/sillyspec_file-io.md.Cz3x7llx.lean.js +0 -1
- package/docs/.vitepress/dist/assets/sillyspec_getting-started.md.ClcvV8k3.js +0 -4
- package/docs/.vitepress/dist/assets/sillyspec_getting-started.md.ClcvV8k3.lean.js +0 -1
- package/docs/.vitepress/dist/assets/sillyspec_install.md.CKuR2tiT.js +0 -5
- package/docs/.vitepress/dist/assets/sillyspec_install.md.CKuR2tiT.lean.js +0 -1
- package/docs/.vitepress/dist/assets/sillyspec_lifecycle.md.DY293cR1.js +0 -28
- package/docs/.vitepress/dist/assets/sillyspec_lifecycle.md.DY293cR1.lean.js +0 -1
- package/docs/.vitepress/dist/assets/sillyspec_structure.md.sVYS4zPs.js +0 -30
- package/docs/.vitepress/dist/assets/sillyspec_structure.md.sVYS4zPs.lean.js +0 -1
- package/docs/.vitepress/dist/assets/style.DFTx90Kk.css +0 -1
- package/docs/.vitepress/dist/hashmap.json +0 -1
- package/docs/.vitepress/dist/index.html +0 -28
- package/docs/.vitepress/dist/sillyspec/commands.html +0 -42
- package/docs/.vitepress/dist/sillyspec/dashboard.html +0 -31
- package/docs/.vitepress/dist/sillyspec/file-io.html +0 -28
- package/docs/.vitepress/dist/sillyspec/getting-started.html +0 -31
- package/docs/.vitepress/dist/sillyspec/install.html +0 -32
- package/docs/.vitepress/dist/sillyspec/lifecycle.html +0 -55
- package/docs/.vitepress/dist/sillyspec/structure.html +0 -57
- package/docs/.vitepress/dist/vp-icons.css +0 -1
- package/docs/index.md +0 -34
- package/docs/sillyspec/commands.md +0 -218
- package/docs/sillyspec/dashboard.md +0 -51
- package/docs/sillyspec/file-io.md +0 -34
- package/docs/sillyspec/getting-started.md +0 -61
- package/docs/sillyspec/install.md +0 -51
- package/docs/sillyspec/lifecycle.md +0 -146
- package/docs/sillyspec/structure.md +0 -62
- package/packages/dashboard/dist/assets/index-Bh-GPjKY.css +0 -1
- package/packages/dashboard/dist/assets/index-CrCn5Gg6.js +0 -17
- package/templates/archive.md +0 -120
- package/templates/brainstorm.md +0 -170
- package/templates/continue.md +0 -32
- package/templates/execute.md +0 -304
- package/templates/explore.md +0 -59
- package/templates/export.md +0 -21
- package/templates/init.md +0 -61
- package/templates/plan.md +0 -146
- package/templates/quick.md +0 -135
- package/templates/scan-quick.md +0 -49
- package/templates/scan.md +0 -156
- package/templates/skills/playwright-e2e/SKILL.md +0 -340
- package/templates/status.md +0 -75
- package/templates/verify.md +0 -236
- package/templates/workspace-sync.md +0 -99
- package/templates/workspace.md +0 -70
- /package/.sillyspec/{specs → changes/brainstorm-archive}/2026-04-05-dashboard-design.md +0 -0
- /package/{docs/.vitepress/dist/logo.jpg → logo.jpg} +0 -0
- /package/{docs/.vitepress → packages/dashboard}/dist/favicon.jpg +0 -0
- /package/{docs/public → packages/dashboard/dist}/logo.jpg +0 -0
- /package/{docs → packages/dashboard}/public/favicon.jpg +0 -0
|
@@ -1,100 +1,202 @@
|
|
|
1
1
|
import chokidar from 'chokidar'
|
|
2
|
-
import { join } from 'path'
|
|
2
|
+
import { join, basename, dirname, sep } from 'path'
|
|
3
3
|
import { homedir } from 'os'
|
|
4
|
-
import { existsSync } from 'fs'
|
|
4
|
+
import { existsSync, readdirSync, realpathSync } from 'fs'
|
|
5
5
|
import { parseProjectState } from './parser.js'
|
|
6
6
|
|
|
7
7
|
let watcher = null
|
|
8
8
|
let updateCallback = null
|
|
9
9
|
let projectStates = new Map()
|
|
10
|
+
export const customScanPaths = new Set()
|
|
11
|
+
|
|
12
|
+
// Directories to exclude (system junk, cache, etc.)
|
|
13
|
+
const excludeDirs = new Set([
|
|
14
|
+
'.Trash', '.cache', '.npm', '.local', '.vscode', 'Library',
|
|
15
|
+
'.git', 'node_modules', '.Trash-*', '.DS_Store', '.config',
|
|
16
|
+
'.cocoapods', '.gem', '.rvm', '.nvm', '.asdf', '.brew',
|
|
17
|
+
'AppData', 'Application Data', '.cargo', '.rustup',
|
|
18
|
+
'.nuget', '.android', '.gradle', '.m2', '.vscode-server'
|
|
19
|
+
])
|
|
10
20
|
|
|
11
21
|
/**
|
|
12
|
-
*
|
|
13
|
-
* @param {
|
|
14
|
-
* @returns {
|
|
22
|
+
* Check if directory should be excluded from scanning
|
|
23
|
+
* @param {string} name - Directory name
|
|
24
|
+
* @returns {boolean}
|
|
15
25
|
*/
|
|
16
|
-
|
|
17
|
-
if (
|
|
18
|
-
|
|
26
|
+
function shouldExclude(name, cwd) {
|
|
27
|
+
if (excludeDirs.has(name)) return true
|
|
28
|
+
// Check wildcard patterns (like .Trash-*)
|
|
29
|
+
for (const pattern of excludeDirs) {
|
|
30
|
+
if (pattern.includes('*')) {
|
|
31
|
+
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$')
|
|
32
|
+
if (regex.test(name)) return true
|
|
33
|
+
}
|
|
19
34
|
}
|
|
35
|
+
// Exclude hidden directories unless it's the cwd basename
|
|
36
|
+
const cwdName = cwd.split(sep).pop() || cwd.split('/').pop() || ''
|
|
37
|
+
if (name.startsWith('.') && name !== cwdName) {
|
|
38
|
+
return true
|
|
39
|
+
}
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
20
42
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Build list of directories to scan
|
|
45
|
+
* @returns {string[]}
|
|
46
|
+
*/
|
|
47
|
+
function buildScanDirs() {
|
|
24
48
|
const home = homedir()
|
|
25
49
|
const cwd = process.cwd()
|
|
26
50
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
])
|
|
33
|
-
|
|
34
|
-
// Helper to check if directory should be excluded
|
|
35
|
-
const shouldExclude = (name) => {
|
|
36
|
-
// Check exact matches
|
|
37
|
-
if (excludeDirs.has(name)) return true
|
|
38
|
-
// Check wildcard patterns (like .Trash-*)
|
|
39
|
-
for (const pattern of excludeDirs) {
|
|
40
|
-
if (pattern.includes('*')) {
|
|
41
|
-
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$')
|
|
42
|
-
if (regex.test(name)) return true
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
// Exclude hidden directories (starting with .) unless it's the cwd basename
|
|
46
|
-
if (name.startsWith('.') && name !== cwd.split('/').pop()) {
|
|
47
|
-
return true
|
|
48
|
-
}
|
|
49
|
-
return false
|
|
50
|
-
}
|
|
51
|
+
const scanDirs = new Set()
|
|
52
|
+
|
|
53
|
+
// Always scan cwd and its parent
|
|
54
|
+
scanDirs.add(cwd)
|
|
55
|
+
scanDirs.add(dirname(cwd))
|
|
51
56
|
|
|
52
|
-
//
|
|
53
|
-
const
|
|
54
|
-
|
|
57
|
+
// Scan parent of parent (2 levels up from cwd) to discover sibling projects
|
|
58
|
+
const parentParent = dirname(dirname(cwd))
|
|
59
|
+
scanDirs.add(parentParent)
|
|
60
|
+
|
|
61
|
+
// Home directory
|
|
62
|
+
scanDirs.add(home)
|
|
63
|
+
|
|
64
|
+
// Common project directories - check both English and Chinese names
|
|
65
|
+
const extraDirs = [
|
|
66
|
+
'Desktop', '桌面',
|
|
67
|
+
'Documents', '文档',
|
|
68
|
+
'Downloads', '下载',
|
|
69
|
+
'Projects', '项目',
|
|
70
|
+
'Work', '工作',
|
|
71
|
+
'Repos', 'Code', 'src', 'dev',
|
|
72
|
+
'workspace', '工作区'
|
|
73
|
+
]
|
|
55
74
|
|
|
56
75
|
for (const extra of extraDirs) {
|
|
57
76
|
const extraPath = join(home, extra)
|
|
58
77
|
if (existsSync(extraPath)) {
|
|
59
|
-
scanDirs.
|
|
78
|
+
scanDirs.add(extraPath)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Add custom scan paths
|
|
83
|
+
for (const customPath of customScanPaths) {
|
|
84
|
+
if (existsSync(customPath)) {
|
|
85
|
+
scanDirs.add(customPath)
|
|
60
86
|
}
|
|
61
87
|
}
|
|
62
88
|
|
|
63
|
-
|
|
89
|
+
return Array.from(scanDirs)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Recursively scan a directory for .sillyspec projects
|
|
94
|
+
* @param {string} baseDir - Directory to scan
|
|
95
|
+
* @param {Set} seen - Already seen paths
|
|
96
|
+
* @param {number} maxDepth - Maximum recursion depth
|
|
97
|
+
* @param {number} currentDepth - Current depth
|
|
98
|
+
* @returns {Array} Found projects
|
|
99
|
+
*/
|
|
100
|
+
function scanDirectory(baseDir, seen, maxDepth = 2, currentDepth = 0) {
|
|
101
|
+
const cwd = process.cwd()
|
|
64
102
|
const projects = []
|
|
65
|
-
const seen = new Set() // Dedupe by path
|
|
66
103
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const { readdirSync } = require('fs')
|
|
70
|
-
const entries = readdirSync(baseDir, { withFileTypes: true })
|
|
104
|
+
try {
|
|
105
|
+
const entries = readdirSync(baseDir, { withFileTypes: true })
|
|
71
106
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
if (!entry.isDirectory()) continue
|
|
109
|
+
if (shouldExclude(entry.name, cwd)) continue
|
|
75
110
|
|
|
76
|
-
|
|
111
|
+
const dirPath = join(baseDir, entry.name)
|
|
77
112
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
113
|
+
let realPath
|
|
114
|
+
try { realPath = realpathSync(dirPath) } catch { realPath = dirPath }
|
|
115
|
+
const normalizedPath = realPath.toLowerCase()
|
|
116
|
+
if (seen.has(normalizedPath)) continue
|
|
117
|
+
seen.add(normalizedPath)
|
|
81
118
|
|
|
82
|
-
|
|
119
|
+
// Check if this dir has .sillyspec
|
|
120
|
+
const sillyspecPath = join(dirPath, '.sillyspec')
|
|
121
|
+
if (existsSync(sillyspecPath)) {
|
|
122
|
+
projects.push({
|
|
123
|
+
name: entry.name,
|
|
124
|
+
path: dirPath
|
|
125
|
+
})
|
|
126
|
+
}
|
|
83
127
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
name: entry.name,
|
|
88
|
-
path: dirPath
|
|
89
|
-
})
|
|
90
|
-
}
|
|
128
|
+
// Recurse into subdirectories if not at max depth
|
|
129
|
+
if (currentDepth < maxDepth) {
|
|
130
|
+
projects.push(...scanDirectory(dirPath, seen, maxDepth, currentDepth + 1))
|
|
91
131
|
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
132
|
+
}
|
|
133
|
+
} catch (err) {
|
|
134
|
+
// Skip directories we can't read
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return projects
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Scan cwd itself (it might be a project root)
|
|
142
|
+
* @param {Set} seen - Already seen paths
|
|
143
|
+
* @returns {Array} Found projects
|
|
144
|
+
*/
|
|
145
|
+
function scanSelf(seen) {
|
|
146
|
+
const cwd = process.cwd()
|
|
147
|
+
const projects = []
|
|
148
|
+
|
|
149
|
+
if (!seen.has(cwd)) {
|
|
150
|
+
seen.add(cwd)
|
|
151
|
+
const sillyspecPath = join(cwd, '.sillyspec')
|
|
152
|
+
if (existsSync(sillyspecPath)) {
|
|
153
|
+
projects.push({
|
|
154
|
+
name: basename(cwd),
|
|
155
|
+
path: cwd
|
|
156
|
+
})
|
|
95
157
|
}
|
|
96
158
|
}
|
|
97
159
|
|
|
160
|
+
return projects
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Discover all .sillyspec projects
|
|
165
|
+
* @returns {{ projects: Array, watchPaths: string[] }}
|
|
166
|
+
*/
|
|
167
|
+
function discoverAll() {
|
|
168
|
+
const scanDirs = buildScanDirs()
|
|
169
|
+
const seen = new Set()
|
|
170
|
+
const allProjects = []
|
|
171
|
+
|
|
172
|
+
// Check cwd itself first
|
|
173
|
+
allProjects.push(...scanSelf(seen))
|
|
174
|
+
|
|
175
|
+
// Scan each base directory
|
|
176
|
+
for (const baseDir of scanDirs) {
|
|
177
|
+
allProjects.push(...scanDirectory(baseDir, seen, 2, 0))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Build watch paths
|
|
181
|
+
const watchPaths = allProjects.map(p => join(p.path, '.sillyspec'))
|
|
182
|
+
|
|
183
|
+
return { projects: allProjects, watchPaths }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Start watching all .sillyspec directories
|
|
188
|
+
* @param {function} callback - Callback function when projects are updated
|
|
189
|
+
* @returns {object} The watcher instance
|
|
190
|
+
*/
|
|
191
|
+
export function startWatcher(callback) {
|
|
192
|
+
if (watcher) {
|
|
193
|
+
stopWatcher()
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
updateCallback = callback
|
|
197
|
+
|
|
198
|
+
const { projects, watchPaths } = discoverAll()
|
|
199
|
+
|
|
98
200
|
// Parse initial states
|
|
99
201
|
for (const project of projects) {
|
|
100
202
|
const state = parseProjectState(project.path)
|
|
@@ -144,18 +246,18 @@ export function startWatcher(callback) {
|
|
|
144
246
|
* @param {string} filePath - Path to the changed file
|
|
145
247
|
*/
|
|
146
248
|
async function handleFileChange(filePath) {
|
|
147
|
-
//
|
|
249
|
+
// Normalize path for comparison
|
|
250
|
+
const normalizedPath = filePath.replace(/\\/g, '/')
|
|
251
|
+
|
|
148
252
|
const projectName = Array.from(projectStates.values()).find(p =>
|
|
149
|
-
|
|
253
|
+
normalizedPath.startsWith(p.path.replace(/\\/g, '/'))
|
|
150
254
|
)?.name
|
|
151
255
|
|
|
152
256
|
if (!projectName) {
|
|
153
|
-
// Re-scan for new projects
|
|
154
257
|
await rescanProjects()
|
|
155
258
|
return
|
|
156
259
|
}
|
|
157
260
|
|
|
158
|
-
// Re-parse the project state
|
|
159
261
|
const project = projectStates.get(projectName)
|
|
160
262
|
if (project) {
|
|
161
263
|
const newState = parseProjectState(project.path)
|
|
@@ -164,7 +266,6 @@ async function handleFileChange(filePath) {
|
|
|
164
266
|
}
|
|
165
267
|
}
|
|
166
268
|
|
|
167
|
-
// Emit updated state
|
|
168
269
|
if (updateCallback) {
|
|
169
270
|
updateCallback(Array.from(projectStates.values()))
|
|
170
271
|
}
|
|
@@ -174,77 +275,49 @@ async function handleFileChange(filePath) {
|
|
|
174
275
|
* Re-scan for projects (e.g., new .sillyspec directories)
|
|
175
276
|
*/
|
|
176
277
|
async function rescanProjects() {
|
|
177
|
-
const
|
|
178
|
-
const cwd = process.cwd()
|
|
278
|
+
const { projects } = discoverAll()
|
|
179
279
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
for (const pattern of excludeDirs) {
|
|
190
|
-
if (pattern.includes('*')) {
|
|
191
|
-
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$')
|
|
192
|
-
if (regex.test(name)) return true
|
|
280
|
+
for (const project of projects) {
|
|
281
|
+
if (!projectStates.has(project.name)) {
|
|
282
|
+
const state = parseProjectState(project.path)
|
|
283
|
+
if (state) {
|
|
284
|
+
projectStates.set(project.name, {
|
|
285
|
+
name: project.name,
|
|
286
|
+
path: project.path,
|
|
287
|
+
state
|
|
288
|
+
})
|
|
193
289
|
}
|
|
194
290
|
}
|
|
195
|
-
if (name.startsWith('.') && name !== cwd.split('/').pop()) {
|
|
196
|
-
return true
|
|
197
|
-
}
|
|
198
|
-
return false
|
|
199
291
|
}
|
|
200
292
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const extraDirs = ['Desktop', 'Documents', 'Projects', 'Work', 'Repos', 'Code', 'src', 'dev']
|
|
204
|
-
|
|
205
|
-
for (const extra of extraDirs) {
|
|
206
|
-
const extraPath = join(home, extra)
|
|
207
|
-
if (existsSync(extraPath)) {
|
|
208
|
-
scanDirs.push(extraPath)
|
|
209
|
-
}
|
|
293
|
+
if (updateCallback) {
|
|
294
|
+
updateCallback(Array.from(projectStates.values()))
|
|
210
295
|
}
|
|
296
|
+
}
|
|
211
297
|
|
|
212
|
-
|
|
298
|
+
/**
|
|
299
|
+
* Add a custom scan path and rescan
|
|
300
|
+
* @param {string} path - Path to add
|
|
301
|
+
*/
|
|
302
|
+
export function addCustomScanPath(path) {
|
|
303
|
+
customScanPaths.add(path)
|
|
304
|
+
rescanProjects()
|
|
305
|
+
}
|
|
213
306
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (shouldExclude(entry.name)) continue
|
|
222
|
-
|
|
223
|
-
const dirPath = join(baseDir, entry.name)
|
|
224
|
-
if (seen.has(dirPath)) continue
|
|
225
|
-
seen.add(dirPath)
|
|
226
|
-
|
|
227
|
-
const sillyspecPath = join(dirPath, '.sillyspec')
|
|
228
|
-
|
|
229
|
-
if (existsSync(sillyspecPath) && !projectStates.has(entry.name)) {
|
|
230
|
-
const state = parseProjectState(dirPath)
|
|
231
|
-
if (state) {
|
|
232
|
-
projectStates.set(entry.name, {
|
|
233
|
-
name: entry.name,
|
|
234
|
-
path: dirPath,
|
|
235
|
-
state
|
|
236
|
-
})
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
} catch (err) {
|
|
241
|
-
continue
|
|
242
|
-
}
|
|
243
|
-
}
|
|
307
|
+
/**
|
|
308
|
+
* Remove a custom scan path
|
|
309
|
+
* @param {string} path - Path to remove
|
|
310
|
+
*/
|
|
311
|
+
export function removeCustomScanPath(path) {
|
|
312
|
+
customScanPaths.delete(path)
|
|
313
|
+
}
|
|
244
314
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
315
|
+
/**
|
|
316
|
+
* Get list of custom scan paths
|
|
317
|
+
* @returns {string[]}
|
|
318
|
+
*/
|
|
319
|
+
export function getCustomScanPaths() {
|
|
320
|
+
return Array.from(customScanPaths)
|
|
248
321
|
}
|
|
249
322
|
|
|
250
323
|
/**
|
|
@@ -274,4 +347,3 @@ export function getProjectStates() {
|
|
|
274
347
|
export function getProjectState(projectName) {
|
|
275
348
|
return projectStates.get(projectName) || null
|
|
276
349
|
}
|
|
277
|
-
|
|
@@ -1,43 +1,79 @@
|
|
|
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
|
|
|
6
7
|
<!-- Main Content -->
|
|
7
|
-
<div
|
|
8
|
+
<div
|
|
9
|
+
class="flex-1 flex overflow-hidden relative z-10"
|
|
10
|
+
:style="isDragging ? 'user-select: none' : ''"
|
|
11
|
+
>
|
|
8
12
|
<!-- Left: Project List -->
|
|
9
|
-
<aside
|
|
13
|
+
<aside
|
|
14
|
+
class="flex-shrink-0 relative overflow-hidden"
|
|
15
|
+
:style="`width: ${leftWidth}px; background: #FFFFFF; border-right: none;`"
|
|
16
|
+
>
|
|
10
17
|
<ProjectList
|
|
11
18
|
:projects="dashboard.state.projects"
|
|
12
19
|
:active-project="dashboard.state.activeProject"
|
|
13
20
|
:is-loading="dashboard.state.isLoading"
|
|
21
|
+
:scan-paths="scanPaths"
|
|
14
22
|
@select="handleSelectProject"
|
|
23
|
+
@scan:add-path="handleAddScanPath"
|
|
24
|
+
@scan:remove-path="handleRemoveScanPath"
|
|
15
25
|
/>
|
|
16
26
|
</aside>
|
|
17
27
|
|
|
28
|
+
<!-- Left ↔ Center resize handle -->
|
|
29
|
+
<div
|
|
30
|
+
class="w-[2px] flex-shrink-0 cursor-col-resize hover:bg-[#D97706] active:bg-[#D97706] relative z-20"
|
|
31
|
+
style="background: #2A3040;"
|
|
32
|
+
@mousedown="startDragLeft"
|
|
33
|
+
/>
|
|
34
|
+
|
|
18
35
|
<!-- Center: Pipeline View -->
|
|
19
|
-
<main class="flex-1 overflow-hidden accent-stripe">
|
|
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" />
|
|
20
38
|
<PipelineView
|
|
21
39
|
:project="dashboard.state.activeProject"
|
|
22
40
|
:active-step="dashboard.state.activeStep"
|
|
41
|
+
:active-tab="dashboard.state.activeTab"
|
|
42
|
+
:docs="dashboard.state.docs"
|
|
43
|
+
:selected-doc-file="dashboard.state.selectedDocFile"
|
|
44
|
+
:doc-content="dashboard.state.docContent"
|
|
45
|
+
:doc-loading="dashboard.state.docLoading"
|
|
23
46
|
@select-step="handleSelectStep"
|
|
47
|
+
@switch-tab="handleSwitchTab"
|
|
48
|
+
@select-doc-file="handleSelectDocFile"
|
|
24
49
|
/>
|
|
25
50
|
</main>
|
|
26
51
|
|
|
52
|
+
<!-- Center ↔ Right resize handle -->
|
|
53
|
+
<div
|
|
54
|
+
v-if="dashboard.state.isPanelOpen"
|
|
55
|
+
class="w-[2px] flex-shrink-0 cursor-col-resize hover:bg-[#D97706] active:bg-[#D97706] relative z-20"
|
|
56
|
+
style="background: #2A3040;"
|
|
57
|
+
@mousedown="startDragRight"
|
|
58
|
+
/>
|
|
59
|
+
|
|
27
60
|
<!-- Right: Detail Panel -->
|
|
28
61
|
<aside
|
|
29
62
|
:class="[
|
|
30
|
-
'flex-shrink-0
|
|
31
|
-
dashboard.state.isPanelOpen ? '
|
|
63
|
+
'flex-shrink-0 relative overflow-hidden',
|
|
64
|
+
dashboard.state.isPanelOpen ? '' : 'w-0'
|
|
32
65
|
]"
|
|
33
|
-
style="background: #
|
|
66
|
+
:style="dashboard.state.isPanelOpen ? `width: ${rightWidth}px; background: #FFFFFF; transition: none;` : 'background: #FFFFFF;'"
|
|
34
67
|
>
|
|
35
68
|
<DetailPanel
|
|
36
69
|
:is-open="dashboard.state.isPanelOpen"
|
|
37
70
|
:active-step="dashboard.state.activeStep"
|
|
38
71
|
:logs="dashboard.state.logs"
|
|
39
|
-
|
|
72
|
+
:detail-type="detailType"
|
|
73
|
+
:detail-data="detailData"
|
|
74
|
+
@close="handleDetailClose"
|
|
40
75
|
@clear-logs="dashboard.clearLogs"
|
|
76
|
+
@open-doc-file="handleOpenDocFromDetail"
|
|
41
77
|
/>
|
|
42
78
|
</aside>
|
|
43
79
|
</div>
|
|
@@ -63,10 +99,11 @@
|
|
|
63
99
|
@select-stage="handleSelectStage"
|
|
64
100
|
/>
|
|
65
101
|
</div>
|
|
102
|
+
</n-config-provider>
|
|
66
103
|
</template>
|
|
67
104
|
|
|
68
105
|
<script setup>
|
|
69
|
-
import { ref, onMounted } from 'vue'
|
|
106
|
+
import { ref, onMounted, readonly } from 'vue'
|
|
70
107
|
import { useWebSocket } from './composables/useWebSocket.js'
|
|
71
108
|
import { useDashboard } from './composables/useDashboard.js'
|
|
72
109
|
import { useDashboardKeyboard } from './composables/useKeyboard.js'
|
|
@@ -74,6 +111,7 @@ import ProjectList from './components/ProjectList.vue'
|
|
|
74
111
|
import PipelineView from './components/PipelineView.vue'
|
|
75
112
|
import DetailPanel from './components/DetailPanel.vue'
|
|
76
113
|
import ActionBar from './components/ActionBar.vue'
|
|
114
|
+
import ProjectOverview from './components/ProjectOverview.vue'
|
|
77
115
|
import CommandPalette from './components/CommandPalette.vue'
|
|
78
116
|
|
|
79
117
|
// Composables
|
|
@@ -81,6 +119,70 @@ const ws = useWebSocket()
|
|
|
81
119
|
const dashboard = useDashboard()
|
|
82
120
|
const isCommandPaletteOpen = ref(false)
|
|
83
121
|
const executionResult = ref(null)
|
|
122
|
+
const scanPaths = ref([])
|
|
123
|
+
const detailType = ref(null)
|
|
124
|
+
const detailData = ref(null)
|
|
125
|
+
|
|
126
|
+
// Panel resize state
|
|
127
|
+
const STORAGE_KEY = 'dashboard-panel-widths'
|
|
128
|
+
const MIN_LEFT = 180
|
|
129
|
+
const MIN_CENTER = 300
|
|
130
|
+
const MIN_RIGHT = 260
|
|
131
|
+
const DEFAULT_LEFT = 240
|
|
132
|
+
const DEFAULT_RIGHT = 340
|
|
133
|
+
|
|
134
|
+
const leftWidth = ref(DEFAULT_LEFT)
|
|
135
|
+
const rightWidth = ref(DEFAULT_RIGHT)
|
|
136
|
+
const isDragging = ref(false)
|
|
137
|
+
|
|
138
|
+
// Load persisted widths
|
|
139
|
+
try {
|
|
140
|
+
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY))
|
|
141
|
+
if (saved?.left >= MIN_LEFT) leftWidth.value = saved.left
|
|
142
|
+
if (saved?.right >= MIN_RIGHT) rightWidth.value = saved.right
|
|
143
|
+
} catch {}
|
|
144
|
+
|
|
145
|
+
function persistWidths() {
|
|
146
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify({ left: leftWidth.value, right: rightWidth.value }))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function startDragLeft(e) {
|
|
150
|
+
e.preventDefault()
|
|
151
|
+
isDragging.value = true
|
|
152
|
+
const startX = e.clientX
|
|
153
|
+
const startW = leftWidth.value
|
|
154
|
+
const onMove = (ev) => {
|
|
155
|
+
const delta = ev.clientX - startX
|
|
156
|
+
leftWidth.value = Math.max(MIN_LEFT, startW + delta)
|
|
157
|
+
}
|
|
158
|
+
const onUp = () => {
|
|
159
|
+
window.removeEventListener('mousemove', onMove)
|
|
160
|
+
window.removeEventListener('mouseup', onUp)
|
|
161
|
+
isDragging.value = false
|
|
162
|
+
persistWidths()
|
|
163
|
+
}
|
|
164
|
+
window.addEventListener('mousemove', onMove)
|
|
165
|
+
window.addEventListener('mouseup', onUp)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function startDragRight(e) {
|
|
169
|
+
e.preventDefault()
|
|
170
|
+
isDragging.value = true
|
|
171
|
+
const startX = e.clientX
|
|
172
|
+
const startW = rightWidth.value
|
|
173
|
+
const onMove = (ev) => {
|
|
174
|
+
const delta = startX - ev.clientX
|
|
175
|
+
rightWidth.value = Math.max(MIN_RIGHT, startW + delta)
|
|
176
|
+
}
|
|
177
|
+
const onUp = () => {
|
|
178
|
+
window.removeEventListener('mousemove', onMove)
|
|
179
|
+
window.removeEventListener('mouseup', onUp)
|
|
180
|
+
isDragging.value = false
|
|
181
|
+
persistWidths()
|
|
182
|
+
}
|
|
183
|
+
window.addEventListener('mousemove', onMove)
|
|
184
|
+
window.addEventListener('mouseup', onUp)
|
|
185
|
+
}
|
|
84
186
|
|
|
85
187
|
// Keyboard shortcuts
|
|
86
188
|
useDashboardKeyboard({
|
|
@@ -123,22 +225,96 @@ onMounted(() => {
|
|
|
123
225
|
executionResult.value = { exitCode: -1, signal: 'SIGTERM' }
|
|
124
226
|
}
|
|
125
227
|
})
|
|
228
|
+
ws.on('scan:paths', (paths) => { scanPaths.value = paths })
|
|
229
|
+
ws.on('docs:tree', (docs) => { dashboard.updateDocs(docs) })
|
|
126
230
|
})
|
|
127
231
|
|
|
128
|
-
function handleSelectProject(project) {
|
|
232
|
+
function handleSelectProject(project) {
|
|
233
|
+
dashboard.selectProject(project)
|
|
234
|
+
dashboard.selectDocFile(null)
|
|
235
|
+
dashboard.setDocContent('')
|
|
236
|
+
// Request docs for this project
|
|
237
|
+
if (project?.path) {
|
|
238
|
+
ws.send({ type: 'docs:get', data: { projectPath: project.path } })
|
|
239
|
+
}
|
|
240
|
+
}
|
|
129
241
|
function handleSelectStage({ project, stage }) { dashboard.selectProject(project) }
|
|
130
242
|
function handleSelectStep(step) { dashboard.selectStep(step) }
|
|
243
|
+
function handleSwitchTab(tab) { dashboard.setActiveTab(tab) }
|
|
244
|
+
function handleSelectDocFile(file) {
|
|
245
|
+
dashboard.selectDocFile(file)
|
|
246
|
+
dashboard.setDocLoading(true)
|
|
247
|
+
// Fetch doc content via REST API
|
|
248
|
+
fetch(`/api/docs/content?path=${encodeURIComponent(file.path)}`)
|
|
249
|
+
.then(r => r.ok ? r.text() : '')
|
|
250
|
+
.then(content => {
|
|
251
|
+
dashboard.setDocContent(content)
|
|
252
|
+
dashboard.setDocLoading(false)
|
|
253
|
+
})
|
|
254
|
+
.catch(() => {
|
|
255
|
+
dashboard.setDocContent('')
|
|
256
|
+
dashboard.setDocLoading(false)
|
|
257
|
+
})
|
|
258
|
+
}
|
|
131
259
|
function handleExecute() {
|
|
132
260
|
const projectName = dashboard.activeProjectName.value
|
|
133
261
|
if (!projectName) return
|
|
262
|
+
const progress = dashboard.state.activeProject?.state?.progress
|
|
263
|
+
const stages = ['brainstorm', 'plan', 'execute', 'verify']
|
|
264
|
+
const currentStage = dashboard.state.activeProject?.state?.currentStage
|
|
265
|
+
|| stages.find(stage => progress?.stages?.[stage]?.status !== 'completed')
|
|
266
|
+
|| 'brainstorm'
|
|
134
267
|
dashboard.clearLogs()
|
|
135
|
-
ws.send({ type: 'cli:execute', data: { projectName, command:
|
|
268
|
+
ws.send({ type: 'cli:execute', data: { projectName, command: `run ${currentStage}` } })
|
|
136
269
|
}
|
|
137
270
|
function handleKill() {
|
|
138
271
|
const projectName = dashboard.activeProjectName.value
|
|
139
272
|
if (!projectName) return
|
|
140
273
|
ws.send({ type: 'cli:kill', data: { projectName } })
|
|
141
274
|
}
|
|
275
|
+
function handleAddScanPath(path) {
|
|
276
|
+
ws.send({ type: 'scan:add-path', data: { path } })
|
|
277
|
+
}
|
|
278
|
+
function handleRemoveScanPath(path) {
|
|
279
|
+
ws.send({ type: 'scan:remove-path', data: { path } })
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function handleShowDetail(type) {
|
|
283
|
+
const project = dashboard.state.activeProject
|
|
284
|
+
if (!project?.path) return
|
|
285
|
+
detailType.value = type
|
|
286
|
+
detailData.value = null
|
|
287
|
+
dashboard.openPanel()
|
|
288
|
+
try {
|
|
289
|
+
const url = `/api/projects/${encodeURIComponent(project.path)}/detail?type=${type}`
|
|
290
|
+
const res = await fetch(url)
|
|
291
|
+
if (res.ok) detailData.value = await res.json()
|
|
292
|
+
} catch {}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function handleDetailClose() {
|
|
296
|
+
detailType.value = null
|
|
297
|
+
detailData.value = null
|
|
298
|
+
dashboard.closePanel()
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function handleOpenDocFromDetail(file) {
|
|
302
|
+
detailType.value = null
|
|
303
|
+
detailData.value = null
|
|
304
|
+
dashboard.setActiveTab('docs')
|
|
305
|
+
handleSelectDocFile(file)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const themeOverrides = {
|
|
309
|
+
common: {
|
|
310
|
+
primaryColor: '#D97706',
|
|
311
|
+
primaryColorHover: '#F59E0B',
|
|
312
|
+
primaryColorPressed: '#B45309',
|
|
313
|
+
borderRadius: '6px',
|
|
314
|
+
fontFamily: 'DM Sans, sans-serif',
|
|
315
|
+
fontFamilyMono: 'JetBrains Mono, monospace'
|
|
316
|
+
}
|
|
317
|
+
}
|
|
142
318
|
</script>
|
|
143
319
|
|
|
144
320
|
<style>
|