sillyspec 3.8.5 → 3.8.7
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/README.md +0 -6
- package/docs/.vitepress/config.mts +45 -0
- package/docs/.vitepress/dist/404.html +25 -0
- package/docs/.vitepress/dist/assets/app.YytxICdd.js +1 -0
- package/docs/.vitepress/dist/assets/chunks/framework.Czhw_PXq.js +19 -0
- package/docs/.vitepress/dist/assets/chunks/theme.DusTRZQk.js +1 -0
- package/docs/.vitepress/dist/assets/index.md.C3VCvtQA.js +1 -0
- package/docs/.vitepress/dist/assets/index.md.C3VCvtQA.lean.js +1 -0
- 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 +15 -0
- package/docs/.vitepress/dist/assets/sillyspec_commands.md.CXFFsj08.lean.js +1 -0
- package/docs/.vitepress/dist/assets/sillyspec_dashboard.md.BuPXHqjX.js +4 -0
- package/docs/.vitepress/dist/assets/sillyspec_dashboard.md.BuPXHqjX.lean.js +1 -0
- package/docs/.vitepress/dist/assets/sillyspec_file-io.md.Cz3x7llx.js +1 -0
- package/docs/.vitepress/dist/assets/sillyspec_file-io.md.Cz3x7llx.lean.js +1 -0
- package/docs/.vitepress/dist/assets/sillyspec_getting-started.md.ClcvV8k3.js +4 -0
- package/docs/.vitepress/dist/assets/sillyspec_getting-started.md.ClcvV8k3.lean.js +1 -0
- package/docs/.vitepress/dist/assets/sillyspec_install.md.CKuR2tiT.js +5 -0
- package/docs/.vitepress/dist/assets/sillyspec_install.md.CKuR2tiT.lean.js +1 -0
- package/docs/.vitepress/dist/assets/sillyspec_lifecycle.md.DY293cR1.js +28 -0
- package/docs/.vitepress/dist/assets/sillyspec_lifecycle.md.DY293cR1.lean.js +1 -0
- package/docs/.vitepress/dist/assets/sillyspec_structure.md.sVYS4zPs.js +30 -0
- package/docs/.vitepress/dist/assets/sillyspec_structure.md.sVYS4zPs.lean.js +1 -0
- package/docs/.vitepress/dist/assets/style.DFTx90Kk.css +1 -0
- package/docs/.vitepress/dist/hashmap.json +1 -0
- package/docs/.vitepress/dist/index.html +28 -0
- package/docs/.vitepress/dist/sillyspec/commands.html +42 -0
- package/docs/.vitepress/dist/sillyspec/dashboard.html +31 -0
- package/docs/.vitepress/dist/sillyspec/file-io.html +28 -0
- package/docs/.vitepress/dist/sillyspec/getting-started.html +31 -0
- package/docs/.vitepress/dist/sillyspec/install.html +32 -0
- package/docs/.vitepress/dist/sillyspec/lifecycle.html +55 -0
- package/docs/.vitepress/dist/sillyspec/structure.html +57 -0
- package/docs/.vitepress/dist/vp-icons.css +1 -0
- package/docs/index.md +34 -0
- package/docs/sillyspec/commands.md +218 -0
- package/docs/sillyspec/dashboard.md +51 -0
- package/docs/sillyspec/file-io.md +34 -0
- package/docs/sillyspec/getting-started.md +61 -0
- package/docs/sillyspec/install.md +51 -0
- package/docs/sillyspec/lifecycle.md +146 -0
- package/docs/sillyspec/structure.md +62 -0
- package/package.json +11 -9
- 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 +2 -2
- package/packages/dashboard/package-lock.json +0 -220
- package/packages/dashboard/package.json +5 -8
- package/packages/dashboard/server/index.js +106 -255
- package/packages/dashboard/server/parser.js +29 -333
- package/packages/dashboard/server/watcher.js +131 -203
- package/packages/dashboard/src/App.vue +10 -181
- package/packages/dashboard/src/components/ActionBar.vue +42 -26
- package/packages/dashboard/src/components/CommandPalette.vue +65 -40
- package/packages/dashboard/src/components/DetailPanel.vue +53 -68
- package/packages/dashboard/src/components/LogStream.vue +33 -13
- package/packages/dashboard/src/components/PipelineStage.vue +8 -8
- package/packages/dashboard/src/components/PipelineView.vue +45 -80
- package/packages/dashboard/src/components/ProjectList.vue +45 -103
- package/packages/dashboard/src/components/StageBadge.vue +13 -13
- package/packages/dashboard/src/components/StepCard.vue +15 -15
- package/packages/dashboard/src/composables/useDashboard.js +6 -20
- package/packages/dashboard/src/composables/useKeyboard.js +4 -6
- package/packages/dashboard/src/main.js +1 -4
- package/packages/dashboard/src/style.css +17 -17
- package/src/index.js +12 -123
- package/src/init.js +227 -86
- package/src/setup.js +9 -1
- package/templates/archive.md +120 -0
- package/templates/brainstorm.md +170 -0
- package/{.claude/skills/sillyspec-commit/SKILL.md → templates/commit.md} +45 -29
- package/templates/continue.md +32 -0
- package/templates/execute.md +304 -0
- package/templates/explore.md +59 -0
- package/templates/export.md +21 -0
- package/templates/init.md +61 -0
- package/templates/plan.md +146 -0
- package/templates/quick.md +135 -0
- package/templates/scan-quick.md +49 -0
- package/templates/scan.md +156 -0
- package/templates/skills/playwright-e2e/SKILL.md +340 -0
- package/templates/status.md +75 -0
- package/templates/verify.md +236 -0
- package/templates/workspace-sync.md +99 -0
- package/templates/workspace.md +70 -0
- package/.claude/skills/sillyspec-archive/SKILL.md +0 -17
- package/.claude/skills/sillyspec-auto/SKILL.md +0 -77
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +0 -17
- package/.claude/skills/sillyspec-continue/SKILL.md +0 -44
- package/.claude/skills/sillyspec-doctor/SKILL.md +0 -22
- package/.claude/skills/sillyspec-execute/SKILL.md +0 -17
- package/.claude/skills/sillyspec-explore/SKILL.md +0 -96
- package/.claude/skills/sillyspec-export/SKILL.md +0 -53
- package/.claude/skills/sillyspec-init/SKILL.md +0 -170
- package/.claude/skills/sillyspec-plan/SKILL.md +0 -52
- package/.claude/skills/sillyspec-propose/SKILL.md +0 -17
- package/.claude/skills/sillyspec-quick/SKILL.md +0 -17
- package/.claude/skills/sillyspec-resume/SKILL.md +0 -111
- package/.claude/skills/sillyspec-scan/SKILL.md +0 -17
- package/.claude/skills/sillyspec-state/SKILL.md +0 -54
- package/.claude/skills/sillyspec-status/SKILL.md +0 -17
- package/.claude/skills/sillyspec-verify/SKILL.md +0 -17
- package/.claude/skills/sillyspec-workspace/SKILL.md +0 -149
- package/.sillyspec/changes/archive/2026-04-08-derive-state/design.md +0 -97
- package/.sillyspec/changes/archive/2026-04-08-derive-state/plan.md +0 -51
- package/.sillyspec/changes/archive/2026-04-08-derive-state/proposal.md +0 -29
- package/.sillyspec/changes/archive/2026-04-08-derive-state/requirements.md +0 -34
- package/.sillyspec/changes/archive/2026-04-08-derive-state/tasks.md +0 -13
- package/.sillyspec/changes/archive/2026-04-08-derive-state/verify-result.md +0 -43
- package/.sillyspec/changes/auto-mode/design.md +0 -50
- package/.sillyspec/changes/auto-mode/proposal.md +0 -19
- package/.sillyspec/changes/auto-mode/requirements.md +0 -21
- package/.sillyspec/changes/auto-mode/tasks.md +0 -7
- package/.sillyspec/changes/brainstorm-archive/2026-04-05-unified-docs-design.md +0 -199
- package/.sillyspec/changes/dashboard/design.md.braindraft +0 -206
- package/.sillyspec/changes/run-command-design/design.md +0 -1230
- package/.sillyspec/changes/unified-docs-design/design.md +0 -199
- package/.sillyspec/docs/sillyspec/scan/.gitkeep +0 -0
- package/.sillyspec/knowledge/INDEX.md +0 -8
- package/.sillyspec/knowledge/uncategorized.md +0 -3
- package/.sillyspec/projects/sillyspec.yaml +0 -3
- package/packages/dashboard/dist/assets/index-D1EVTLmc.js +0 -7446
- package/packages/dashboard/dist/assets/index-DGe8CqeP.css +0 -1
- package/packages/dashboard/public/logo.jpg +0 -0
- package/packages/dashboard/src/components/DocPreview.vue +0 -160
- package/packages/dashboard/src/components/DocTree.vue +0 -58
- package/packages/dashboard/src/components/ProjectOverview.vue +0 -178
- package/packages/dashboard/src/components/detail/DocsDetail.vue +0 -48
- package/packages/dashboard/src/components/detail/GitDetail.vue +0 -61
- package/packages/dashboard/src/components/detail/TechDetail.vue +0 -43
- package/src/derive.js +0 -147
- package/src/migrate.js +0 -117
- package/src/progress.js +0 -495
- package/src/run.js +0 -640
- package/src/stages/archive.js +0 -54
- package/src/stages/brainstorm.js +0 -239
- package/src/stages/doctor.js +0 -312
- package/src/stages/execute.js +0 -259
- package/src/stages/index.js +0 -35
- package/src/stages/plan.js +0 -259
- package/src/stages/propose.js +0 -115
- package/src/stages/quick.js +0 -64
- package/src/stages/scan.js +0 -141
- package/src/stages/status.js +0 -65
- package/src/stages/verify.js +0 -135
- /package/.sillyspec/{changes/brainstorm-archive → specs}/2026-04-05-dashboard-design.md +0 -0
- /package/{packages/dashboard → docs/.vitepress}/dist/favicon.jpg +0 -0
- /package/{logo.jpg → docs/.vitepress/dist/logo.jpg} +0 -0
- /package/{packages/dashboard → docs}/public/favicon.jpg +0 -0
- /package/{packages/dashboard/dist → docs/public}/logo.jpg +0 -0
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { createServer } from 'http'
|
|
2
2
|
import { WebSocketServer } from 'ws'
|
|
3
|
-
import { existsSync, readFileSync
|
|
4
|
-
import { join, dirname
|
|
3
|
+
import { existsSync, readFileSync } from 'fs'
|
|
4
|
+
import { join, dirname } from 'path'
|
|
5
5
|
import { fileURLToPath } from 'url'
|
|
6
|
-
import { homedir } from 'os'
|
|
7
6
|
import open from 'open'
|
|
8
|
-
import { parseProjectState
|
|
9
|
-
import { startWatcher, stopWatcher
|
|
7
|
+
import { parseProjectState } from './parser.js'
|
|
8
|
+
import { startWatcher, stopWatcher } from './watcher.js'
|
|
10
9
|
import { executeCommand } from './executor.js'
|
|
11
10
|
|
|
12
11
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
@@ -15,44 +14,6 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
|
15
14
|
let wss = null
|
|
16
15
|
const activeProcesses = new Map()
|
|
17
16
|
|
|
18
|
-
// Progress file watchers: projectPath -> { watcher, timer, refCount }
|
|
19
|
-
const progressWatchers = new Map()
|
|
20
|
-
|
|
21
|
-
function startProgressWatch(projectPath) {
|
|
22
|
-
if (progressWatchers.has(projectPath)) {
|
|
23
|
-
progressWatchers.get(projectPath).refCount++
|
|
24
|
-
return
|
|
25
|
-
}
|
|
26
|
-
const progressFile = join(projectPath, '.sillyspec', '.runtime', 'progress.json')
|
|
27
|
-
if (!existsSync(progressFile)) return
|
|
28
|
-
|
|
29
|
-
let timer = null
|
|
30
|
-
try {
|
|
31
|
-
const watcher = watch(progressFile, (eventType) => {
|
|
32
|
-
if (timer) clearTimeout(timer)
|
|
33
|
-
timer = setTimeout(() => {
|
|
34
|
-
timer = null
|
|
35
|
-
try {
|
|
36
|
-
const state = parseProjectState(projectPath)
|
|
37
|
-
broadcast({ type: 'project:update', data: { path: projectPath, state } })
|
|
38
|
-
} catch {}
|
|
39
|
-
}, 200)
|
|
40
|
-
})
|
|
41
|
-
progressWatchers.set(projectPath, { watcher, timer, refCount: 1 })
|
|
42
|
-
} catch {}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function stopProgressWatch(projectPath) {
|
|
46
|
-
const entry = progressWatchers.get(projectPath)
|
|
47
|
-
if (!entry) return
|
|
48
|
-
entry.refCount--
|
|
49
|
-
if (entry.refCount <= 0) {
|
|
50
|
-
entry.watcher.close()
|
|
51
|
-
if (entry.timer) clearTimeout(entry.timer)
|
|
52
|
-
progressWatchers.delete(projectPath)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
17
|
/**
|
|
57
18
|
* Broadcast message to all connected WebSocket clients
|
|
58
19
|
* @param {object} data - Data to broadcast
|
|
@@ -67,96 +28,83 @@ function broadcast(data) {
|
|
|
67
28
|
})
|
|
68
29
|
}
|
|
69
30
|
|
|
70
|
-
// --- Shared scan logic (aligned with watcher.js) ---
|
|
71
|
-
|
|
72
|
-
const excludeDirs = new Set([
|
|
73
|
-
'.Trash', '.cache', '.npm', '.local', '.vscode', 'Library',
|
|
74
|
-
'.git', 'node_modules', '.Trash-*', '.DS_Store', '.config',
|
|
75
|
-
'.cocoapods', '.gem', '.rvm', '.nvm', '.asdf', '.brew',
|
|
76
|
-
'AppData', 'Application Data', '.cargo', '.rustup',
|
|
77
|
-
'.nuget', '.android', '.gradle', '.m2', '.vscode-server'
|
|
78
|
-
])
|
|
79
|
-
|
|
80
|
-
function shouldExclude(name, cwd) {
|
|
81
|
-
if (excludeDirs.has(name)) return true
|
|
82
|
-
for (const pattern of excludeDirs) {
|
|
83
|
-
if (pattern.includes('*')) {
|
|
84
|
-
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$')
|
|
85
|
-
if (regex.test(name)) return true
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
const cwdName = cwd.split(sep).pop() || cwd.split('/').pop() || ''
|
|
89
|
-
if (name.startsWith('.') && name !== cwdName) return true
|
|
90
|
-
return false
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function scanDirectory(baseDir, seen, maxDepth = 2, currentDepth = 0) {
|
|
94
|
-
const cwd = process.cwd()
|
|
95
|
-
const projects = []
|
|
96
|
-
try {
|
|
97
|
-
const entries = readdirSync(baseDir, { withFileTypes: true })
|
|
98
|
-
for (const entry of entries) {
|
|
99
|
-
if (!entry.isDirectory()) continue
|
|
100
|
-
if (shouldExclude(entry.name, cwd)) continue
|
|
101
|
-
const dirPath = join(baseDir, entry.name)
|
|
102
|
-
let realPath
|
|
103
|
-
try { realPath = realpathSync(dirPath) } catch { realPath = dirPath }
|
|
104
|
-
const normalizedPath = realPath.toLowerCase()
|
|
105
|
-
if (seen.has(normalizedPath)) continue
|
|
106
|
-
seen.add(normalizedPath)
|
|
107
|
-
if (existsSync(join(dirPath, '.sillyspec'))) {
|
|
108
|
-
projects.push({ name: entry.name, path: dirPath })
|
|
109
|
-
}
|
|
110
|
-
if (currentDepth < maxDepth) {
|
|
111
|
-
projects.push(...scanDirectory(dirPath, seen, maxDepth, currentDepth + 1))
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
} catch (err) {}
|
|
115
|
-
return projects
|
|
116
|
-
}
|
|
117
|
-
|
|
118
31
|
/**
|
|
119
32
|
* Discover all SillySpec projects
|
|
120
33
|
* @returns {Promise<Array>} Array of project objects
|
|
121
34
|
*/
|
|
122
35
|
async function discoverProjects() {
|
|
36
|
+
const { homedir } = await import('os')
|
|
123
37
|
const home = homedir()
|
|
124
38
|
const cwd = process.cwd()
|
|
125
39
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
40
|
+
// Directories to exclude (system junk, cache, etc.)
|
|
41
|
+
const excludeDirs = new Set([
|
|
42
|
+
'.Trash', '.cache', '.npm', '.local', '.vscode', 'Library',
|
|
43
|
+
'.git', 'node_modules', '.Trash-*', '.DS_Store', '.config',
|
|
44
|
+
'.cocoapods', '.gem', '.rvm', '.nvm', '.asdf', '.brew'
|
|
45
|
+
])
|
|
46
|
+
|
|
47
|
+
// Helper to check if directory should be excluded
|
|
48
|
+
const shouldExclude = (name) => {
|
|
49
|
+
// Check exact matches
|
|
50
|
+
if (excludeDirs.has(name)) return true
|
|
51
|
+
// Check wildcard patterns (like .Trash-*)
|
|
52
|
+
for (const pattern of excludeDirs) {
|
|
53
|
+
if (pattern.includes('*')) {
|
|
54
|
+
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$')
|
|
55
|
+
if (regex.test(name)) return true
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Exclude hidden directories (starting with .) unless it's the cwd basename
|
|
59
|
+
if (name.startsWith('.') && name !== cwd.split('/').pop()) {
|
|
60
|
+
return true
|
|
61
|
+
}
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
131
64
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
'workspace', '工作区'
|
|
136
|
-
]
|
|
65
|
+
// Build scan directories: cwd + home subdirs + common project locations
|
|
66
|
+
const scanDirs = [cwd, home]
|
|
67
|
+
const extraDirs = ['Desktop', 'Documents', 'Projects', 'Work', 'Repos', 'Code', 'src', 'dev']
|
|
137
68
|
|
|
138
69
|
for (const extra of extraDirs) {
|
|
139
70
|
const extraPath = join(home, extra)
|
|
140
|
-
if (existsSync(extraPath))
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
for (const customPath of customScanPaths) {
|
|
144
|
-
if (existsSync(customPath)) scanDirs.add(customPath)
|
|
71
|
+
if (existsSync(extraPath)) {
|
|
72
|
+
scanDirs.push(extraPath)
|
|
73
|
+
}
|
|
145
74
|
}
|
|
146
75
|
|
|
147
|
-
const seen = new Set()
|
|
148
76
|
const projects = []
|
|
149
|
-
|
|
150
|
-
// Normalize cwd for dedup
|
|
151
|
-
let cwdReal
|
|
152
|
-
try { cwdReal = realpathSync(cwd) } catch { cwdReal = cwd }
|
|
153
|
-
seen.add(cwdReal.toLowerCase())
|
|
154
|
-
if (existsSync(join(cwd, '.sillyspec'))) {
|
|
155
|
-
projects.push({ name: basename(cwd), path: cwd })
|
|
156
|
-
}
|
|
77
|
+
const seen = new Set() // Dedupe by path
|
|
157
78
|
|
|
158
79
|
for (const baseDir of scanDirs) {
|
|
159
|
-
|
|
80
|
+
try {
|
|
81
|
+
const { readdirSync } = await import('fs')
|
|
82
|
+
const entries = readdirSync(baseDir, { withFileTypes: true })
|
|
83
|
+
|
|
84
|
+
for (const entry of entries) {
|
|
85
|
+
if (!entry.isDirectory()) continue
|
|
86
|
+
if (shouldExclude(entry.name)) continue
|
|
87
|
+
|
|
88
|
+
const dirPath = join(baseDir, entry.name)
|
|
89
|
+
|
|
90
|
+
// Skip if we've already seen this path (handles symlinks, dupes)
|
|
91
|
+
const realPath = dirPath // Could add realpath for true deduping
|
|
92
|
+
if (seen.has(realPath)) continue
|
|
93
|
+
seen.add(realPath)
|
|
94
|
+
|
|
95
|
+
const sillyspecPath = join(dirPath, '.sillyspec')
|
|
96
|
+
|
|
97
|
+
if (existsSync(sillyspecPath)) {
|
|
98
|
+
projects.push({
|
|
99
|
+
name: entry.name,
|
|
100
|
+
path: dirPath
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} catch (err) {
|
|
105
|
+
// Skip directories we can't read
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
160
108
|
}
|
|
161
109
|
|
|
162
110
|
return projects
|
|
@@ -178,6 +126,7 @@ function handleCliExecute(ws, data) {
|
|
|
178
126
|
return
|
|
179
127
|
}
|
|
180
128
|
|
|
129
|
+
// Find project
|
|
181
130
|
discoverProjects().then(projects => {
|
|
182
131
|
const project = projects.find(p => p.name === projectName)
|
|
183
132
|
if (!project) {
|
|
@@ -188,29 +137,45 @@ function handleCliExecute(ws, data) {
|
|
|
188
137
|
return
|
|
189
138
|
}
|
|
190
139
|
|
|
140
|
+
// Kill existing process for this project if any
|
|
191
141
|
const existingKill = activeProcesses.get(projectName)
|
|
192
|
-
if (existingKill)
|
|
142
|
+
if (existingKill) {
|
|
143
|
+
existingKill()
|
|
144
|
+
}
|
|
193
145
|
|
|
146
|
+
// Execute command
|
|
194
147
|
const kill = executeCommand(
|
|
195
148
|
project.path,
|
|
196
149
|
command,
|
|
197
150
|
(output) => {
|
|
198
151
|
broadcast({
|
|
199
152
|
type: 'cli:output',
|
|
200
|
-
data: {
|
|
153
|
+
data: {
|
|
154
|
+
projectName,
|
|
155
|
+
output: output.data,
|
|
156
|
+
outputType: output.type
|
|
157
|
+
}
|
|
201
158
|
})
|
|
202
159
|
},
|
|
203
160
|
(result) => {
|
|
204
161
|
activeProcesses.delete(projectName)
|
|
205
162
|
broadcast({
|
|
206
163
|
type: 'cli:complete',
|
|
207
|
-
data: {
|
|
164
|
+
data: {
|
|
165
|
+
projectName,
|
|
166
|
+
exitCode: result.code,
|
|
167
|
+
signal: result.signal
|
|
168
|
+
}
|
|
208
169
|
})
|
|
209
170
|
}
|
|
210
171
|
)
|
|
211
172
|
|
|
212
173
|
activeProcesses.set(projectName, kill)
|
|
213
|
-
|
|
174
|
+
|
|
175
|
+
broadcast({
|
|
176
|
+
type: 'cli:started',
|
|
177
|
+
data: { projectName, command }
|
|
178
|
+
})
|
|
214
179
|
})
|
|
215
180
|
}
|
|
216
181
|
|
|
@@ -221,6 +186,7 @@ function handleCliExecute(ws, data) {
|
|
|
221
186
|
*/
|
|
222
187
|
function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
223
188
|
const server = createServer((req, res) => {
|
|
189
|
+
// CORS headers
|
|
224
190
|
res.setHeader('Access-Control-Allow-Origin', '*')
|
|
225
191
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
|
226
192
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
|
|
@@ -231,6 +197,7 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
|
231
197
|
return
|
|
232
198
|
}
|
|
233
199
|
|
|
200
|
+
// API: List all projects
|
|
234
201
|
if (req.url === '/api/projects') {
|
|
235
202
|
discoverProjects().then(projects => {
|
|
236
203
|
res.setHeader('Content-Type', 'application/json')
|
|
@@ -243,44 +210,7 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
|
243
210
|
return
|
|
244
211
|
}
|
|
245
212
|
|
|
246
|
-
//
|
|
247
|
-
const detailMatch = req.url?.match(/^\/api\/projects\/(.+)\/detail(\?|$)/)
|
|
248
|
-
if (detailMatch) {
|
|
249
|
-
const projectPath = decodeURIComponent(detailMatch[1])
|
|
250
|
-
const url = new URL(req.url, `http://${req.headers.host}`)
|
|
251
|
-
const type = url.searchParams.get('type')
|
|
252
|
-
let data
|
|
253
|
-
try {
|
|
254
|
-
if (type === 'git') data = parseGitDetail(projectPath)
|
|
255
|
-
else if (type === 'tech') data = parseTechStackDetail(projectPath)
|
|
256
|
-
else if (type === 'docs') data = parseDocsList(projectPath)
|
|
257
|
-
else { res.writeHead(400); res.end(JSON.stringify({ error: 'Invalid type' })); return }
|
|
258
|
-
res.setHeader('Content-Type', 'application/json')
|
|
259
|
-
res.writeHead(200)
|
|
260
|
-
res.end(JSON.stringify(data))
|
|
261
|
-
} catch (err) {
|
|
262
|
-
res.writeHead(500)
|
|
263
|
-
res.end(JSON.stringify({ error: err.message }))
|
|
264
|
-
}
|
|
265
|
-
return
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Overview API
|
|
269
|
-
if (req.url?.startsWith('/api/projects/') && req.url.endsWith('/overview')) {
|
|
270
|
-
const parts = req.url.replace('/api/projects/', '').replace('/overview', '').split('/')
|
|
271
|
-
const projectPath = decodeURIComponent(parts.join('/'))
|
|
272
|
-
try {
|
|
273
|
-
const overview = parseProjectOverview(projectPath)
|
|
274
|
-
res.setHeader('Content-Type', 'application/json')
|
|
275
|
-
res.writeHead(200)
|
|
276
|
-
res.end(JSON.stringify(overview))
|
|
277
|
-
} catch (err) {
|
|
278
|
-
res.writeHead(500)
|
|
279
|
-
res.end(JSON.stringify({ error: err.message }))
|
|
280
|
-
}
|
|
281
|
-
return
|
|
282
|
-
}
|
|
283
|
-
|
|
213
|
+
// API: Get project details with state
|
|
284
214
|
if (req.url?.startsWith('/api/project/')) {
|
|
285
215
|
const projectName = decodeURIComponent(req.url.split('/').pop())
|
|
286
216
|
discoverProjects().then(projects => {
|
|
@@ -289,7 +219,10 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
|
289
219
|
const state = parseProjectState(project.path)
|
|
290
220
|
res.setHeader('Content-Type', 'application/json')
|
|
291
221
|
res.writeHead(200)
|
|
292
|
-
res.end(JSON.stringify({
|
|
222
|
+
res.end(JSON.stringify({
|
|
223
|
+
...project,
|
|
224
|
+
state
|
|
225
|
+
}))
|
|
293
226
|
} else {
|
|
294
227
|
res.writeHead(404)
|
|
295
228
|
res.end(JSON.stringify({ error: 'Project not found' }))
|
|
@@ -301,54 +234,6 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
|
301
234
|
return
|
|
302
235
|
}
|
|
303
236
|
|
|
304
|
-
// Docs API
|
|
305
|
-
if (req.url?.startsWith('/api/docs/content')) {
|
|
306
|
-
const url = new URL(req.url, `http://${req.headers.host}`)
|
|
307
|
-
const filePath = url.searchParams.get('path')
|
|
308
|
-
if (!filePath) {
|
|
309
|
-
res.writeHead(400)
|
|
310
|
-
res.end(JSON.stringify({ error: 'Missing path parameter' }))
|
|
311
|
-
return
|
|
312
|
-
}
|
|
313
|
-
// Security: only allow reading files under .sillyspec/docs/
|
|
314
|
-
const normalizedPath = resolve(filePath)
|
|
315
|
-
if (!normalizedPath.includes('.sillyspec' + sep + 'docs')) {
|
|
316
|
-
res.writeHead(403)
|
|
317
|
-
res.end(JSON.stringify({ error: 'Access denied' }))
|
|
318
|
-
return
|
|
319
|
-
}
|
|
320
|
-
if (existsSync(normalizedPath)) {
|
|
321
|
-
try {
|
|
322
|
-
const content = readFileSync(normalizedPath, 'utf-8')
|
|
323
|
-
res.setHeader('Content-Type', 'text/plain; charset=utf-8')
|
|
324
|
-
res.writeHead(200)
|
|
325
|
-
res.end(content)
|
|
326
|
-
} catch (err) {
|
|
327
|
-
res.writeHead(500)
|
|
328
|
-
res.end(JSON.stringify({ error: err.message }))
|
|
329
|
-
}
|
|
330
|
-
} else {
|
|
331
|
-
res.writeHead(404)
|
|
332
|
-
res.end(JSON.stringify({ error: 'File not found' }))
|
|
333
|
-
}
|
|
334
|
-
return
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
if (req.url?.startsWith('/api/docs')) {
|
|
338
|
-
const url = new URL(req.url, `http://${req.headers.host}`)
|
|
339
|
-
const projectPath = url.searchParams.get('project')
|
|
340
|
-
if (!projectPath) {
|
|
341
|
-
res.writeHead(400)
|
|
342
|
-
res.end(JSON.stringify({ error: 'Missing project parameter' }))
|
|
343
|
-
return
|
|
344
|
-
}
|
|
345
|
-
const docs = parseDocsTree(projectPath)
|
|
346
|
-
res.setHeader('Content-Type', 'application/json')
|
|
347
|
-
res.writeHead(200)
|
|
348
|
-
res.end(JSON.stringify(docs))
|
|
349
|
-
return
|
|
350
|
-
}
|
|
351
|
-
|
|
352
237
|
// Serve static files (dist/)
|
|
353
238
|
const distDir = join(__dirname, '../dist')
|
|
354
239
|
const indexPath = join(distDir, 'index.html')
|
|
@@ -362,6 +247,7 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
|
362
247
|
res.end(readFileSync(filePath))
|
|
363
248
|
return
|
|
364
249
|
}
|
|
250
|
+
// SPA fallback: serve index.html for unknown routes
|
|
365
251
|
if (existsSync(indexPath)) {
|
|
366
252
|
res.setHeader('Content-Type', 'text/html')
|
|
367
253
|
res.writeHead(200)
|
|
@@ -370,10 +256,12 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
|
370
256
|
}
|
|
371
257
|
}
|
|
372
258
|
|
|
259
|
+
// 404
|
|
373
260
|
res.writeHead(404)
|
|
374
261
|
res.end('Not found')
|
|
375
262
|
})
|
|
376
263
|
|
|
264
|
+
// WebSocket Server
|
|
377
265
|
wss = new WebSocketServer({ server })
|
|
378
266
|
|
|
379
267
|
wss.on('error', (err) => {
|
|
@@ -387,25 +275,13 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
|
387
275
|
discoverProjects().then(projects => {
|
|
388
276
|
const projectsWithState = projects.map(p => ({
|
|
389
277
|
...p,
|
|
390
|
-
state: parseProjectState(p.path)
|
|
391
|
-
overview: parseProjectOverview(p.path)
|
|
278
|
+
state: parseProjectState(p.path)
|
|
392
279
|
}))
|
|
393
280
|
|
|
394
281
|
ws.send(JSON.stringify({
|
|
395
282
|
type: 'projects:init',
|
|
396
283
|
data: projectsWithState
|
|
397
284
|
}))
|
|
398
|
-
|
|
399
|
-
// Start watching progress files for all discovered projects
|
|
400
|
-
for (const p of projectsWithState) {
|
|
401
|
-
startProgressWatch(p.path)
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Send current scan paths
|
|
405
|
-
ws.send(JSON.stringify({
|
|
406
|
-
type: 'scan:paths',
|
|
407
|
-
data: getCustomScanPaths()
|
|
408
|
-
}))
|
|
409
285
|
}).catch(err => {
|
|
410
286
|
console.error('Error sending initial projects:', err)
|
|
411
287
|
})
|
|
@@ -423,37 +299,12 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
|
423
299
|
if (kill) {
|
|
424
300
|
kill()
|
|
425
301
|
activeProcesses.delete(data.data.projectName)
|
|
426
|
-
broadcast({
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
case 'scan:add-path':
|
|
430
|
-
if (data.data?.path) {
|
|
431
|
-
addCustomScanPath(data.data.path)
|
|
432
|
-
broadcast({ type: 'scan:paths', data: getCustomScanPaths() })
|
|
433
|
-
// Resend projects after scan
|
|
434
|
-
discoverProjects().then(projects => {
|
|
435
|
-
broadcast({
|
|
436
|
-
type: 'projects:updated',
|
|
437
|
-
data: projects.map(p => ({ ...p, state: parseProjectState(p.path) }))
|
|
438
|
-
})
|
|
302
|
+
broadcast({
|
|
303
|
+
type: 'cli:killed',
|
|
304
|
+
data: { projectName: data.data.projectName }
|
|
439
305
|
})
|
|
440
306
|
}
|
|
441
307
|
break
|
|
442
|
-
case 'scan:remove-path':
|
|
443
|
-
if (data.data?.path) {
|
|
444
|
-
removeCustomScanPath(data.data.path)
|
|
445
|
-
ws.send(JSON.stringify({ type: 'scan:paths', data: getCustomScanPaths() }))
|
|
446
|
-
}
|
|
447
|
-
break
|
|
448
|
-
case 'scan:get-paths':
|
|
449
|
-
ws.send(JSON.stringify({ type: 'scan:paths', data: getCustomScanPaths() }))
|
|
450
|
-
break
|
|
451
|
-
case 'docs:get':
|
|
452
|
-
if (data.data?.projectPath) {
|
|
453
|
-
const docs = parseDocsTree(data.data.projectPath)
|
|
454
|
-
ws.send(JSON.stringify({ type: 'docs:tree', data: docs }))
|
|
455
|
-
}
|
|
456
|
-
break
|
|
457
308
|
default:
|
|
458
309
|
console.log('Unknown message type:', data.type)
|
|
459
310
|
}
|
|
@@ -464,12 +315,6 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
|
464
315
|
|
|
465
316
|
ws.on('close', () => {
|
|
466
317
|
console.log('WebSocket client disconnected')
|
|
467
|
-
// Decrement progress watchers — we track which projects this client was watching
|
|
468
|
-
// via the initial projects:init. For simplicity, stop all that we started.
|
|
469
|
-
// (Other clients will re-start via their connection if still active)
|
|
470
|
-
for (const [path] of progressWatchers) {
|
|
471
|
-
stopProgressWatch(path)
|
|
472
|
-
}
|
|
473
318
|
})
|
|
474
319
|
|
|
475
320
|
ws.on('error', (err) => {
|
|
@@ -477,9 +322,13 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
|
477
322
|
})
|
|
478
323
|
})
|
|
479
324
|
|
|
325
|
+
// Start file watcher (wrapped to avoid crashing server)
|
|
480
326
|
try {
|
|
481
327
|
startWatcher((projects) => {
|
|
482
|
-
broadcast({
|
|
328
|
+
broadcast({
|
|
329
|
+
type: 'projects:updated',
|
|
330
|
+
data: projects
|
|
331
|
+
})
|
|
483
332
|
})
|
|
484
333
|
} catch (err) {
|
|
485
334
|
console.error('Failed to start file watcher:', err)
|
|
@@ -487,11 +336,13 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
|
487
336
|
|
|
488
337
|
server.listen(port, () => {
|
|
489
338
|
console.log(`Dashboard server running on http://localhost:${port}`)
|
|
339
|
+
|
|
490
340
|
if (openBrowser) {
|
|
491
341
|
open(`http://localhost:${port}`)
|
|
492
342
|
}
|
|
493
343
|
})
|
|
494
344
|
|
|
345
|
+
// Handle shutdown
|
|
495
346
|
const shutdown = () => {
|
|
496
347
|
stopWatcher()
|
|
497
348
|
activeProcesses.forEach(kill => kill())
|