sillyspec 3.8.4 → 3.8.6

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.
Files changed (163) hide show
  1. package/README.md +0 -6
  2. package/docs/.vitepress/config.mts +45 -0
  3. package/docs/.vitepress/dist/404.html +25 -0
  4. package/docs/.vitepress/dist/assets/app.YytxICdd.js +1 -0
  5. package/docs/.vitepress/dist/assets/chunks/framework.Czhw_PXq.js +19 -0
  6. package/docs/.vitepress/dist/assets/chunks/theme.DusTRZQk.js +1 -0
  7. package/docs/.vitepress/dist/assets/index.md.C3VCvtQA.js +1 -0
  8. package/docs/.vitepress/dist/assets/index.md.C3VCvtQA.lean.js +1 -0
  9. package/docs/.vitepress/dist/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 +0 -0
  10. package/docs/.vitepress/dist/assets/inter-italic-cyrillic.By2_1cv3.woff2 +0 -0
  11. package/docs/.vitepress/dist/assets/inter-italic-greek-ext.1u6EdAuj.woff2 +0 -0
  12. package/docs/.vitepress/dist/assets/inter-italic-greek.DJ8dCoTZ.woff2 +0 -0
  13. package/docs/.vitepress/dist/assets/inter-italic-latin-ext.CN1xVJS-.woff2 +0 -0
  14. package/docs/.vitepress/dist/assets/inter-italic-latin.C2AdPX0b.woff2 +0 -0
  15. package/docs/.vitepress/dist/assets/inter-italic-vietnamese.BSbpV94h.woff2 +0 -0
  16. package/docs/.vitepress/dist/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 +0 -0
  17. package/docs/.vitepress/dist/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 +0 -0
  18. package/docs/.vitepress/dist/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 +0 -0
  19. package/docs/.vitepress/dist/assets/inter-roman-greek.BBVDIX6e.woff2 +0 -0
  20. package/docs/.vitepress/dist/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 +0 -0
  21. package/docs/.vitepress/dist/assets/inter-roman-latin.Di8DUHzh.woff2 +0 -0
  22. package/docs/.vitepress/dist/assets/inter-roman-vietnamese.BjW4sHH5.woff2 +0 -0
  23. package/docs/.vitepress/dist/assets/sillyspec_commands.md.CXFFsj08.js +15 -0
  24. package/docs/.vitepress/dist/assets/sillyspec_commands.md.CXFFsj08.lean.js +1 -0
  25. package/docs/.vitepress/dist/assets/sillyspec_dashboard.md.BuPXHqjX.js +4 -0
  26. package/docs/.vitepress/dist/assets/sillyspec_dashboard.md.BuPXHqjX.lean.js +1 -0
  27. package/docs/.vitepress/dist/assets/sillyspec_file-io.md.Cz3x7llx.js +1 -0
  28. package/docs/.vitepress/dist/assets/sillyspec_file-io.md.Cz3x7llx.lean.js +1 -0
  29. package/docs/.vitepress/dist/assets/sillyspec_getting-started.md.ClcvV8k3.js +4 -0
  30. package/docs/.vitepress/dist/assets/sillyspec_getting-started.md.ClcvV8k3.lean.js +1 -0
  31. package/docs/.vitepress/dist/assets/sillyspec_install.md.CKuR2tiT.js +5 -0
  32. package/docs/.vitepress/dist/assets/sillyspec_install.md.CKuR2tiT.lean.js +1 -0
  33. package/docs/.vitepress/dist/assets/sillyspec_lifecycle.md.DY293cR1.js +28 -0
  34. package/docs/.vitepress/dist/assets/sillyspec_lifecycle.md.DY293cR1.lean.js +1 -0
  35. package/docs/.vitepress/dist/assets/sillyspec_structure.md.sVYS4zPs.js +30 -0
  36. package/docs/.vitepress/dist/assets/sillyspec_structure.md.sVYS4zPs.lean.js +1 -0
  37. package/docs/.vitepress/dist/assets/style.DFTx90Kk.css +1 -0
  38. package/docs/.vitepress/dist/hashmap.json +1 -0
  39. package/docs/.vitepress/dist/index.html +28 -0
  40. package/docs/.vitepress/dist/sillyspec/commands.html +42 -0
  41. package/docs/.vitepress/dist/sillyspec/dashboard.html +31 -0
  42. package/docs/.vitepress/dist/sillyspec/file-io.html +28 -0
  43. package/docs/.vitepress/dist/sillyspec/getting-started.html +31 -0
  44. package/docs/.vitepress/dist/sillyspec/install.html +32 -0
  45. package/docs/.vitepress/dist/sillyspec/lifecycle.html +55 -0
  46. package/docs/.vitepress/dist/sillyspec/structure.html +57 -0
  47. package/docs/.vitepress/dist/vp-icons.css +1 -0
  48. package/docs/index.md +34 -0
  49. package/docs/sillyspec/commands.md +218 -0
  50. package/docs/sillyspec/dashboard.md +51 -0
  51. package/docs/sillyspec/file-io.md +34 -0
  52. package/docs/sillyspec/getting-started.md +61 -0
  53. package/docs/sillyspec/install.md +51 -0
  54. package/docs/sillyspec/lifecycle.md +146 -0
  55. package/docs/sillyspec/structure.md +62 -0
  56. package/package.json +11 -9
  57. package/packages/dashboard/dist/assets/index-Bh-GPjKY.css +1 -0
  58. package/packages/dashboard/dist/assets/index-CrCn5Gg6.js +17 -0
  59. package/packages/dashboard/dist/index.html +2 -2
  60. package/packages/dashboard/package-lock.json +0 -220
  61. package/packages/dashboard/package.json +5 -8
  62. package/packages/dashboard/server/index.js +106 -255
  63. package/packages/dashboard/server/parser.js +29 -333
  64. package/packages/dashboard/server/watcher.js +131 -203
  65. package/packages/dashboard/src/App.vue +10 -181
  66. package/packages/dashboard/src/components/ActionBar.vue +42 -26
  67. package/packages/dashboard/src/components/CommandPalette.vue +65 -40
  68. package/packages/dashboard/src/components/DetailPanel.vue +53 -68
  69. package/packages/dashboard/src/components/LogStream.vue +33 -13
  70. package/packages/dashboard/src/components/PipelineStage.vue +8 -8
  71. package/packages/dashboard/src/components/PipelineView.vue +45 -80
  72. package/packages/dashboard/src/components/ProjectList.vue +45 -103
  73. package/packages/dashboard/src/components/StageBadge.vue +13 -13
  74. package/packages/dashboard/src/components/StepCard.vue +15 -15
  75. package/packages/dashboard/src/composables/useDashboard.js +6 -20
  76. package/packages/dashboard/src/composables/useKeyboard.js +4 -6
  77. package/packages/dashboard/src/main.js +1 -4
  78. package/packages/dashboard/src/style.css +17 -17
  79. package/src/index.js +12 -123
  80. package/src/init.js +227 -86
  81. package/src/setup.js +9 -1
  82. package/templates/archive.md +121 -0
  83. package/templates/brainstorm.md +240 -0
  84. package/{.claude/skills/sillyspec-commit/SKILL.md → templates/commit.md} +47 -29
  85. package/templates/continue.md +32 -0
  86. package/templates/execute.md +314 -0
  87. package/templates/explore.md +60 -0
  88. package/templates/export.md +21 -0
  89. package/templates/init.md +61 -0
  90. package/templates/plan.md +157 -0
  91. package/templates/quick.md +135 -0
  92. package/templates/scan-quick.md +49 -0
  93. package/templates/scan.md +172 -0
  94. package/templates/skills/playwright-e2e/SKILL.md +340 -0
  95. package/templates/status.md +75 -0
  96. package/templates/verify.md +253 -0
  97. package/templates/workspace-sync.md +99 -0
  98. package/templates/workspace.md +70 -0
  99. package/.claude/skills/sillyspec-archive/SKILL.md +0 -17
  100. package/.claude/skills/sillyspec-auto/SKILL.md +0 -77
  101. package/.claude/skills/sillyspec-brainstorm/SKILL.md +0 -17
  102. package/.claude/skills/sillyspec-continue/SKILL.md +0 -44
  103. package/.claude/skills/sillyspec-doctor/SKILL.md +0 -22
  104. package/.claude/skills/sillyspec-execute/SKILL.md +0 -17
  105. package/.claude/skills/sillyspec-explore/SKILL.md +0 -96
  106. package/.claude/skills/sillyspec-export/SKILL.md +0 -53
  107. package/.claude/skills/sillyspec-init/SKILL.md +0 -170
  108. package/.claude/skills/sillyspec-plan/SKILL.md +0 -52
  109. package/.claude/skills/sillyspec-propose/SKILL.md +0 -17
  110. package/.claude/skills/sillyspec-quick/SKILL.md +0 -17
  111. package/.claude/skills/sillyspec-resume/SKILL.md +0 -111
  112. package/.claude/skills/sillyspec-scan/SKILL.md +0 -17
  113. package/.claude/skills/sillyspec-state/SKILL.md +0 -54
  114. package/.claude/skills/sillyspec-status/SKILL.md +0 -17
  115. package/.claude/skills/sillyspec-verify/SKILL.md +0 -17
  116. package/.claude/skills/sillyspec-workspace/SKILL.md +0 -149
  117. package/.sillyspec/changes/archive/2026-04-08-derive-state/design.md +0 -97
  118. package/.sillyspec/changes/archive/2026-04-08-derive-state/plan.md +0 -51
  119. package/.sillyspec/changes/archive/2026-04-08-derive-state/proposal.md +0 -29
  120. package/.sillyspec/changes/archive/2026-04-08-derive-state/requirements.md +0 -34
  121. package/.sillyspec/changes/archive/2026-04-08-derive-state/tasks.md +0 -13
  122. package/.sillyspec/changes/archive/2026-04-08-derive-state/verify-result.md +0 -43
  123. package/.sillyspec/changes/auto-mode/design.md +0 -50
  124. package/.sillyspec/changes/auto-mode/proposal.md +0 -19
  125. package/.sillyspec/changes/auto-mode/requirements.md +0 -21
  126. package/.sillyspec/changes/auto-mode/tasks.md +0 -7
  127. package/.sillyspec/changes/brainstorm-archive/2026-04-05-unified-docs-design.md +0 -199
  128. package/.sillyspec/changes/dashboard/design.md.braindraft +0 -206
  129. package/.sillyspec/changes/run-command-design/design.md +0 -1230
  130. package/.sillyspec/changes/unified-docs-design/design.md +0 -199
  131. package/.sillyspec/docs/sillyspec/scan/.gitkeep +0 -0
  132. package/.sillyspec/knowledge/INDEX.md +0 -8
  133. package/.sillyspec/knowledge/uncategorized.md +0 -3
  134. package/.sillyspec/projects/sillyspec.yaml +0 -3
  135. package/packages/dashboard/dist/assets/index-D1EVTLmc.js +0 -7446
  136. package/packages/dashboard/dist/assets/index-DGe8CqeP.css +0 -1
  137. package/packages/dashboard/public/logo.jpg +0 -0
  138. package/packages/dashboard/src/components/DocPreview.vue +0 -160
  139. package/packages/dashboard/src/components/DocTree.vue +0 -58
  140. package/packages/dashboard/src/components/ProjectOverview.vue +0 -178
  141. package/packages/dashboard/src/components/detail/DocsDetail.vue +0 -48
  142. package/packages/dashboard/src/components/detail/GitDetail.vue +0 -61
  143. package/packages/dashboard/src/components/detail/TechDetail.vue +0 -43
  144. package/src/derive.js +0 -147
  145. package/src/migrate.js +0 -117
  146. package/src/progress.js +0 -495
  147. package/src/run.js +0 -640
  148. package/src/stages/archive.js +0 -54
  149. package/src/stages/brainstorm.js +0 -239
  150. package/src/stages/doctor.js +0 -312
  151. package/src/stages/execute.js +0 -258
  152. package/src/stages/index.js +0 -35
  153. package/src/stages/plan.js +0 -259
  154. package/src/stages/propose.js +0 -115
  155. package/src/stages/quick.js +0 -64
  156. package/src/stages/scan.js +0 -141
  157. package/src/stages/status.js +0 -65
  158. package/src/stages/verify.js +0 -135
  159. /package/.sillyspec/{changes/brainstorm-archive → specs}/2026-04-05-dashboard-design.md +0 -0
  160. /package/{packages/dashboard → docs/.vitepress}/dist/favicon.jpg +0 -0
  161. /package/{logo.jpg → docs/.vitepress/dist/logo.jpg} +0 -0
  162. /package/{packages/dashboard → docs}/public/favicon.jpg +0 -0
  163. /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, readdirSync, realpathSync, watch } from 'fs'
4
- import { join, dirname, basename, sep, resolve } from 'path'
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, parseDocsTree, parseProjectOverview, parseGitDetail, parseTechStackDetail, parseDocsList } from './parser.js'
9
- import { startWatcher, stopWatcher, addCustomScanPath, removeCustomScanPath, getCustomScanPaths, customScanPaths } from './watcher.js'
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
- const scanDirs = new Set()
127
- scanDirs.add(cwd)
128
- scanDirs.add(dirname(cwd))
129
- scanDirs.add(dirname(dirname(cwd)))
130
- scanDirs.add(home)
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
- const extraDirs = [
133
- 'Desktop', '桌面', 'Documents', '文档', 'Downloads', '下载',
134
- 'Projects', '项目', 'Work', '工作', 'Repos', 'Code', 'src', 'dev',
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)) scanDirs.add(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
- projects.push(...scanDirectory(baseDir, seen, 2, 0))
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) 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: { projectName, output: output.data, outputType: output.type }
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: { projectName, exitCode: result.code, signal: result.signal }
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
- broadcast({ type: 'cli:started', data: { projectName, command } })
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
- // Detail API
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({ ...project, state }))
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({ type: 'cli:killed', data: { projectName: data.data.projectName } })
427
- }
428
- break
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({ type: 'projects:updated', data: projects })
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())