sillyspec 3.9.0 → 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.
Files changed (212) hide show
  1. package/.claude/skills/sillyspec-archive/SKILL.md +17 -0
  2. package/.claude/skills/sillyspec-auto/SKILL.md +78 -0
  3. package/.claude/skills/sillyspec-brainstorm/SKILL.md +17 -0
  4. package/{templates/commit.md → .claude/skills/sillyspec-commit/SKILL.md} +32 -47
  5. package/.claude/skills/sillyspec-continue/SKILL.md +45 -0
  6. package/.claude/skills/sillyspec-doctor/SKILL.md +27 -0
  7. package/.claude/skills/sillyspec-execute/SKILL.md +17 -0
  8. package/.claude/skills/sillyspec-explore/SKILL.md +96 -0
  9. package/.claude/skills/sillyspec-export/SKILL.md +53 -0
  10. package/.claude/skills/sillyspec-init/SKILL.md +170 -0
  11. package/.claude/skills/sillyspec-plan/SKILL.md +52 -0
  12. package/.claude/skills/sillyspec-propose/SKILL.md +17 -0
  13. package/.claude/skills/sillyspec-quick/SKILL.md +17 -0
  14. package/.claude/skills/sillyspec-resume/SKILL.md +111 -0
  15. package/.claude/skills/sillyspec-scan/SKILL.md +17 -0
  16. package/.claude/skills/sillyspec-state/SKILL.md +54 -0
  17. package/.claude/skills/sillyspec-status/SKILL.md +17 -0
  18. package/.claude/skills/sillyspec-verify/SKILL.md +17 -0
  19. package/.claude/skills/sillyspec-workspace/SKILL.md +149 -0
  20. package/.sillyspec/changes/archive/2026-04-08-derive-state/design.md +97 -0
  21. package/.sillyspec/changes/archive/2026-04-08-derive-state/plan.md +51 -0
  22. package/.sillyspec/changes/archive/2026-04-08-derive-state/proposal.md +29 -0
  23. package/.sillyspec/changes/archive/2026-04-08-derive-state/requirements.md +34 -0
  24. package/.sillyspec/changes/archive/2026-04-08-derive-state/tasks.md +13 -0
  25. package/.sillyspec/changes/archive/2026-04-08-derive-state/verify-result.md +43 -0
  26. package/.sillyspec/changes/auto-mode/design.md +50 -0
  27. package/.sillyspec/changes/auto-mode/proposal.md +19 -0
  28. package/.sillyspec/changes/auto-mode/requirements.md +21 -0
  29. package/.sillyspec/changes/auto-mode/tasks.md +7 -0
  30. package/.sillyspec/changes/brainstorm-archive/2026-04-05-unified-docs-design.md +199 -0
  31. package/.sillyspec/changes/dashboard/design.md.braindraft +206 -0
  32. package/.sillyspec/changes/run-command-design/design.md +1230 -0
  33. package/.sillyspec/changes/unified-docs-design/design.md +199 -0
  34. package/.sillyspec/docs/sillyspec/scan/.gitkeep +0 -0
  35. package/.sillyspec/knowledge/INDEX.md +8 -0
  36. package/.sillyspec/knowledge/uncategorized.md +3 -0
  37. package/.sillyspec/projects/sillyspec.yaml +3 -0
  38. package/README.md +12 -5
  39. package/package.json +7 -9
  40. package/packages/dashboard/dist/assets/index-CntACGUN.css +1 -0
  41. package/packages/dashboard/dist/assets/index-RsLVPAy7.js +7446 -0
  42. package/packages/dashboard/dist/index.html +3 -2
  43. package/packages/dashboard/package-lock.json +226 -6
  44. package/packages/dashboard/package.json +8 -5
  45. package/packages/dashboard/public/logo.jpg +0 -0
  46. package/packages/dashboard/server/executor.js +1 -1
  47. package/packages/dashboard/server/index.js +336 -113
  48. package/packages/dashboard/server/parser.js +333 -29
  49. package/packages/dashboard/server/watcher.js +203 -131
  50. package/packages/dashboard/src/App.vue +187 -11
  51. package/packages/dashboard/src/components/ActionBar.vue +26 -42
  52. package/packages/dashboard/src/components/CommandPalette.vue +40 -65
  53. package/packages/dashboard/src/components/DetailPanel.vue +68 -53
  54. package/packages/dashboard/src/components/DocPreview.vue +160 -0
  55. package/packages/dashboard/src/components/DocTree.vue +58 -0
  56. package/packages/dashboard/src/components/LogStream.vue +13 -33
  57. package/packages/dashboard/src/components/PipelineStage.vue +8 -8
  58. package/packages/dashboard/src/components/PipelineView.vue +80 -45
  59. package/packages/dashboard/src/components/ProjectList.vue +103 -45
  60. package/packages/dashboard/src/components/ProjectOverview.vue +178 -0
  61. package/packages/dashboard/src/components/StageBadge.vue +13 -13
  62. package/packages/dashboard/src/components/StepCard.vue +15 -15
  63. package/packages/dashboard/src/components/detail/DocsDetail.vue +48 -0
  64. package/packages/dashboard/src/components/detail/GitDetail.vue +61 -0
  65. package/packages/dashboard/src/components/detail/TechDetail.vue +43 -0
  66. package/packages/dashboard/src/composables/useDashboard.js +20 -6
  67. package/packages/dashboard/src/composables/useKeyboard.js +6 -4
  68. package/packages/dashboard/src/main.js +4 -1
  69. package/packages/dashboard/src/style.css +17 -17
  70. package/src/index.js +134 -22
  71. package/src/init.js +83 -228
  72. package/src/migrate.js +117 -0
  73. package/src/progress.js +459 -0
  74. package/src/run.js +624 -0
  75. package/src/setup.js +2 -72
  76. package/src/stages/archive.js +54 -0
  77. package/src/stages/brainstorm.js +239 -0
  78. package/src/stages/doctor.js +303 -0
  79. package/src/stages/execute.js +262 -0
  80. package/src/stages/index.js +26 -0
  81. package/src/stages/plan.js +282 -0
  82. package/src/stages/propose.js +115 -0
  83. package/src/stages/quick.js +64 -0
  84. package/src/stages/scan.js +141 -0
  85. package/src/stages/status.js +65 -0
  86. package/src/stages/verify.js +135 -0
  87. package/dist/steps/brainstorm/01-load-context.md +0 -30
  88. package/dist/steps/brainstorm/02-reuse-check.md +0 -6
  89. package/dist/steps/brainstorm/03-prototype-analysis.md +0 -11
  90. package/dist/steps/brainstorm/04-module-split.md +0 -23
  91. package/dist/steps/brainstorm/05-dialog-explore.md +0 -8
  92. package/dist/steps/brainstorm/06-propose-approaches.md +0 -3
  93. package/dist/steps/brainstorm/07-present-design.md +0 -3
  94. package/dist/steps/brainstorm/08-write-design.md +0 -21
  95. package/dist/steps/brainstorm/09-self-review.md +0 -15
  96. package/dist/steps/brainstorm/10-user-confirm.md +0 -3
  97. package/dist/steps/brainstorm/11-output-spec.md +0 -7
  98. package/dist/steps/brainstorm/manifest.yaml +0 -26
  99. package/dist/steps/execute/01-load-context.md +0 -41
  100. package/dist/steps/execute/02-scan-conventions.md +0 -47
  101. package/dist/steps/execute/03-skill-mcp.md +0 -19
  102. package/dist/steps/execute/04-assign-task.md +0 -22
  103. package/dist/steps/execute/04b-prompt-template.md +0 -54
  104. package/dist/steps/execute/05-write-test.md +0 -7
  105. package/dist/steps/execute/06-write-code.md +0 -8
  106. package/dist/steps/execute/07-run-test.md +0 -26
  107. package/dist/steps/execute/08-fix-issues.md +0 -28
  108. package/dist/steps/execute/09-next-task.md +0 -33
  109. package/dist/steps/execute/manifest.yaml +0 -28
  110. package/dist/steps/plan/01-load-context.md +0 -22
  111. package/dist/steps/plan/02-anchor-confirm.md +0 -1
  112. package/dist/steps/plan/03-expand-tasks.md +0 -33
  113. package/dist/steps/plan/04-mark-order.md +0 -15
  114. package/dist/steps/plan/05-e2e-planning.md +0 -17
  115. package/dist/steps/plan/06-self-check.md +0 -16
  116. package/dist/steps/plan/07-save.md +0 -1
  117. package/dist/steps/plan/manifest.yaml +0 -18
  118. package/dist/steps/scan/01-env-detect.md +0 -51
  119. package/dist/steps/scan/02-tech-stack.md +0 -16
  120. package/dist/steps/scan/03-conventions.md +0 -16
  121. package/dist/steps/scan/04-structure.md +0 -19
  122. package/dist/steps/scan/05-quality.md +0 -18
  123. package/dist/steps/scan/06-complete.md +0 -49
  124. package/dist/steps/scan/manifest.yaml +0 -16
  125. package/dist/steps/verify/01-load-specs.md +0 -28
  126. package/dist/steps/verify/02-check-tasks.md +0 -1
  127. package/dist/steps/verify/03-check-design.md +0 -6
  128. package/dist/steps/verify/04-run-tests.md +0 -7
  129. package/dist/steps/verify/05-e2e-tests.md +0 -27
  130. package/dist/steps/verify/05b-e2e-fix.md +0 -33
  131. package/dist/steps/verify/06-code-quality.md +0 -25
  132. package/dist/steps/verify/07-lint-check.md +0 -27
  133. package/dist/steps/verify/08-output-report.md +0 -14
  134. package/dist/steps/verify/manifest.yaml +0 -22
  135. package/docs/.vitepress/config.mts +0 -45
  136. package/docs/.vitepress/dist/404.html +0 -25
  137. package/docs/.vitepress/dist/assets/app.YytxICdd.js +0 -1
  138. package/docs/.vitepress/dist/assets/chunks/framework.Czhw_PXq.js +0 -19
  139. package/docs/.vitepress/dist/assets/chunks/theme.DusTRZQk.js +0 -1
  140. package/docs/.vitepress/dist/assets/index.md.C3VCvtQA.js +0 -1
  141. package/docs/.vitepress/dist/assets/index.md.C3VCvtQA.lean.js +0 -1
  142. package/docs/.vitepress/dist/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 +0 -0
  143. package/docs/.vitepress/dist/assets/inter-italic-cyrillic.By2_1cv3.woff2 +0 -0
  144. package/docs/.vitepress/dist/assets/inter-italic-greek-ext.1u6EdAuj.woff2 +0 -0
  145. package/docs/.vitepress/dist/assets/inter-italic-greek.DJ8dCoTZ.woff2 +0 -0
  146. package/docs/.vitepress/dist/assets/inter-italic-latin-ext.CN1xVJS-.woff2 +0 -0
  147. package/docs/.vitepress/dist/assets/inter-italic-latin.C2AdPX0b.woff2 +0 -0
  148. package/docs/.vitepress/dist/assets/inter-italic-vietnamese.BSbpV94h.woff2 +0 -0
  149. package/docs/.vitepress/dist/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 +0 -0
  150. package/docs/.vitepress/dist/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 +0 -0
  151. package/docs/.vitepress/dist/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 +0 -0
  152. package/docs/.vitepress/dist/assets/inter-roman-greek.BBVDIX6e.woff2 +0 -0
  153. package/docs/.vitepress/dist/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 +0 -0
  154. package/docs/.vitepress/dist/assets/inter-roman-latin.Di8DUHzh.woff2 +0 -0
  155. package/docs/.vitepress/dist/assets/inter-roman-vietnamese.BjW4sHH5.woff2 +0 -0
  156. package/docs/.vitepress/dist/assets/sillyspec_commands.md.CXFFsj08.js +0 -15
  157. package/docs/.vitepress/dist/assets/sillyspec_commands.md.CXFFsj08.lean.js +0 -1
  158. package/docs/.vitepress/dist/assets/sillyspec_dashboard.md.BuPXHqjX.js +0 -4
  159. package/docs/.vitepress/dist/assets/sillyspec_dashboard.md.BuPXHqjX.lean.js +0 -1
  160. package/docs/.vitepress/dist/assets/sillyspec_file-io.md.Cz3x7llx.js +0 -1
  161. package/docs/.vitepress/dist/assets/sillyspec_file-io.md.Cz3x7llx.lean.js +0 -1
  162. package/docs/.vitepress/dist/assets/sillyspec_getting-started.md.ClcvV8k3.js +0 -4
  163. package/docs/.vitepress/dist/assets/sillyspec_getting-started.md.ClcvV8k3.lean.js +0 -1
  164. package/docs/.vitepress/dist/assets/sillyspec_install.md.CKuR2tiT.js +0 -5
  165. package/docs/.vitepress/dist/assets/sillyspec_install.md.CKuR2tiT.lean.js +0 -1
  166. package/docs/.vitepress/dist/assets/sillyspec_lifecycle.md.DY293cR1.js +0 -28
  167. package/docs/.vitepress/dist/assets/sillyspec_lifecycle.md.DY293cR1.lean.js +0 -1
  168. package/docs/.vitepress/dist/assets/sillyspec_structure.md.sVYS4zPs.js +0 -30
  169. package/docs/.vitepress/dist/assets/sillyspec_structure.md.sVYS4zPs.lean.js +0 -1
  170. package/docs/.vitepress/dist/assets/style.DFTx90Kk.css +0 -1
  171. package/docs/.vitepress/dist/hashmap.json +0 -1
  172. package/docs/.vitepress/dist/index.html +0 -28
  173. package/docs/.vitepress/dist/sillyspec/commands.html +0 -42
  174. package/docs/.vitepress/dist/sillyspec/dashboard.html +0 -31
  175. package/docs/.vitepress/dist/sillyspec/file-io.html +0 -28
  176. package/docs/.vitepress/dist/sillyspec/getting-started.html +0 -31
  177. package/docs/.vitepress/dist/sillyspec/install.html +0 -32
  178. package/docs/.vitepress/dist/sillyspec/lifecycle.html +0 -55
  179. package/docs/.vitepress/dist/sillyspec/structure.html +0 -57
  180. package/docs/.vitepress/dist/vp-icons.css +0 -1
  181. package/docs/index.md +0 -34
  182. package/docs/sillyspec/commands.md +0 -218
  183. package/docs/sillyspec/dashboard.md +0 -51
  184. package/docs/sillyspec/file-io.md +0 -34
  185. package/docs/sillyspec/getting-started.md +0 -61
  186. package/docs/sillyspec/install.md +0 -51
  187. package/docs/sillyspec/lifecycle.md +0 -146
  188. package/docs/sillyspec/structure.md +0 -62
  189. package/packages/dashboard/dist/assets/index-Bh-GPjKY.css +0 -1
  190. package/packages/dashboard/dist/assets/index-CrCn5Gg6.js +0 -17
  191. package/src/step.js +0 -543
  192. package/templates/archive.md +0 -120
  193. package/templates/brainstorm.md +0 -170
  194. package/templates/continue.md +0 -32
  195. package/templates/execute.md +0 -304
  196. package/templates/explore.md +0 -59
  197. package/templates/export.md +0 -21
  198. package/templates/init.md +0 -61
  199. package/templates/plan.md +0 -146
  200. package/templates/quick.md +0 -135
  201. package/templates/scan-quick.md +0 -49
  202. package/templates/scan.md +0 -156
  203. package/templates/skills/playwright-e2e/SKILL.md +0 -340
  204. package/templates/status.md +0 -75
  205. package/templates/verify.md +0 -236
  206. package/templates/workspace-sync.md +0 -99
  207. package/templates/workspace.md +0 -70
  208. /package/.sillyspec/{specs → changes/brainstorm-archive}/2026-04-05-dashboard-design.md +0 -0
  209. /package/{docs/.vitepress/dist/logo.jpg → logo.jpg} +0 -0
  210. /package/{docs/.vitepress → packages/dashboard}/dist/favicon.jpg +0 -0
  211. /package/{docs/public → packages/dashboard/dist}/logo.jpg +0 -0
  212. /package/{docs → packages/dashboard}/public/favicon.jpg +0 -0
@@ -1,11 +1,12 @@
1
1
  import { createServer } from 'http'
2
2
  import { WebSocketServer } from 'ws'
3
- import { existsSync, readFileSync } from 'fs'
4
- import { join, dirname } from 'path'
3
+ import { existsSync, readFileSync, readdirSync, realpathSync, watch } from 'fs'
4
+ import { join, dirname, basename, sep, resolve, relative } from 'path'
5
5
  import { fileURLToPath } from 'url'
6
+ import { homedir } from 'os'
6
7
  import open from 'open'
7
- import { parseProjectState } from './parser.js'
8
- import { startWatcher, stopWatcher } from './watcher.js'
8
+ import { parseProjectState, parseDocsTree, parseProjectOverview, parseGitDetail, parseTechStackDetail, parseDocsList } from './parser.js'
9
+ import { startWatcher, stopWatcher, addCustomScanPath, removeCustomScanPath, getCustomScanPaths, customScanPaths } from './watcher.js'
9
10
  import { executeCommand } from './executor.js'
10
11
 
11
12
  const __dirname = dirname(fileURLToPath(import.meta.url))
@@ -13,6 +14,69 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
13
14
  // WebSocket clients and active processes
14
15
  let wss = null
15
16
  const activeProcesses = new Map()
17
+ const allowedStages = new Set(['brainstorm', 'plan', 'execute', 'verify', 'scan', 'quick', 'archive', 'status', 'doctor', 'auto'])
18
+
19
+ function isAllowedCliCommand(command) {
20
+ const args = String(command || '').trim().split(/\s+/).filter(Boolean)
21
+ if (args.length === 0) return false
22
+ if (args[0] === 'run') {
23
+ if (!allowedStages.has(args[1])) return false
24
+ const allowedFlags = new Set(['--status', '--reset', '--done', '--skip', '--output', '--input', '--change'])
25
+ for (let i = 2; i < args.length; i++) {
26
+ if (args[i].startsWith('-') && !allowedFlags.has(args[i])) return false
27
+ if (['--output', '--input', '--change'].includes(args[i])) i++
28
+ }
29
+ return true
30
+ }
31
+ if (args[0] === 'progress') {
32
+ const sub = args[1]
33
+ if (!['show', 'status', 'validate', 'reset'].includes(sub)) return false
34
+ for (let i = 2; i < args.length; i++) {
35
+ if (!['--stage', '--deep'].includes(args[i])) return false
36
+ if (args[i] === '--stage') i++
37
+ }
38
+ return true
39
+ }
40
+ return false
41
+ }
42
+
43
+ // Progress file watchers: projectPath -> { watcher, timer, refCount }
44
+ const progressWatchers = new Map()
45
+
46
+ function startProgressWatch(projectPath) {
47
+ if (progressWatchers.has(projectPath)) {
48
+ progressWatchers.get(projectPath).refCount++
49
+ return
50
+ }
51
+ const progressFile = join(projectPath, '.sillyspec', '.runtime', 'progress.json')
52
+ if (!existsSync(progressFile)) return
53
+
54
+ let timer = null
55
+ try {
56
+ const watcher = watch(progressFile, (eventType) => {
57
+ if (timer) clearTimeout(timer)
58
+ timer = setTimeout(() => {
59
+ timer = null
60
+ try {
61
+ const state = parseProjectState(projectPath)
62
+ broadcast({ type: 'project:update', data: { path: projectPath, state } })
63
+ } catch {}
64
+ }, 200)
65
+ })
66
+ progressWatchers.set(projectPath, { watcher, timer, refCount: 1 })
67
+ } catch {}
68
+ }
69
+
70
+ function stopProgressWatch(projectPath) {
71
+ const entry = progressWatchers.get(projectPath)
72
+ if (!entry) return
73
+ entry.refCount--
74
+ if (entry.refCount <= 0) {
75
+ entry.watcher.close()
76
+ if (entry.timer) clearTimeout(entry.timer)
77
+ progressWatchers.delete(projectPath)
78
+ }
79
+ }
16
80
 
17
81
  /**
18
82
  * Broadcast message to all connected WebSocket clients
@@ -28,83 +92,119 @@ function broadcast(data) {
28
92
  })
29
93
  }
30
94
 
95
+ function isInside(parent, child) {
96
+ const rel = relative(resolve(parent), resolve(child))
97
+ return rel === '' || (!!rel && !rel.startsWith('..') && !rel.includes(`..${sep}`))
98
+ }
99
+
100
+ function isSillyspecDocsPath(filePath) {
101
+ const parts = resolve(filePath).split(/[\\/]+/)
102
+ for (let i = 0; i < parts.length - 1; i++) {
103
+ if (parts[i] === '.sillyspec' && parts[i + 1] === 'docs') return true
104
+ }
105
+ return false
106
+ }
107
+
108
+ function isLocalOrigin(origin) {
109
+ if (!origin) return true
110
+ try {
111
+ const { hostname } = new URL(origin)
112
+ return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1'
113
+ } catch {
114
+ return false
115
+ }
116
+ }
117
+
118
+ // --- Shared scan logic (aligned with watcher.js) ---
119
+
120
+ const excludeDirs = new Set([
121
+ '.Trash', '.cache', '.npm', '.local', '.vscode', 'Library',
122
+ '.git', 'node_modules', '.Trash-*', '.DS_Store', '.config',
123
+ '.cocoapods', '.gem', '.rvm', '.nvm', '.asdf', '.brew',
124
+ 'AppData', 'Application Data', '.cargo', '.rustup',
125
+ '.nuget', '.android', '.gradle', '.m2', '.vscode-server'
126
+ ])
127
+
128
+ function shouldExclude(name, cwd) {
129
+ if (excludeDirs.has(name)) return true
130
+ for (const pattern of excludeDirs) {
131
+ if (pattern.includes('*')) {
132
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$')
133
+ if (regex.test(name)) return true
134
+ }
135
+ }
136
+ const cwdName = cwd.split(sep).pop() || cwd.split('/').pop() || ''
137
+ if (name.startsWith('.') && name !== cwdName) return true
138
+ return false
139
+ }
140
+
141
+ function scanDirectory(baseDir, seen, maxDepth = 2, currentDepth = 0) {
142
+ const cwd = process.cwd()
143
+ const projects = []
144
+ try {
145
+ const entries = readdirSync(baseDir, { withFileTypes: true })
146
+ for (const entry of entries) {
147
+ if (!entry.isDirectory()) continue
148
+ if (shouldExclude(entry.name, cwd)) continue
149
+ const dirPath = join(baseDir, entry.name)
150
+ let realPath
151
+ try { realPath = realpathSync(dirPath) } catch { realPath = dirPath }
152
+ const normalizedPath = realPath.toLowerCase()
153
+ if (seen.has(normalizedPath)) continue
154
+ seen.add(normalizedPath)
155
+ if (existsSync(join(dirPath, '.sillyspec'))) {
156
+ projects.push({ name: entry.name, path: dirPath })
157
+ }
158
+ if (currentDepth < maxDepth) {
159
+ projects.push(...scanDirectory(dirPath, seen, maxDepth, currentDepth + 1))
160
+ }
161
+ }
162
+ } catch (err) {}
163
+ return projects
164
+ }
165
+
31
166
  /**
32
167
  * Discover all SillySpec projects
33
168
  * @returns {Promise<Array>} Array of project objects
34
169
  */
35
170
  async function discoverProjects() {
36
- const { homedir } = await import('os')
37
171
  const home = homedir()
38
172
  const cwd = process.cwd()
39
173
 
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
- }
174
+ const scanDirs = new Set()
175
+ scanDirs.add(cwd)
176
+ scanDirs.add(dirname(cwd))
177
+ scanDirs.add(dirname(dirname(cwd)))
178
+ scanDirs.add(home)
64
179
 
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']
180
+ const extraDirs = [
181
+ 'Desktop', '桌面', 'Documents', '文档', 'Downloads', '下载',
182
+ 'Projects', '项目', 'Work', '工作', 'Repos', 'Code', 'src', 'dev',
183
+ 'workspace', '工作区'
184
+ ]
68
185
 
69
186
  for (const extra of extraDirs) {
70
187
  const extraPath = join(home, extra)
71
- if (existsSync(extraPath)) {
72
- scanDirs.push(extraPath)
73
- }
188
+ if (existsSync(extraPath)) scanDirs.add(extraPath)
74
189
  }
75
190
 
76
- const projects = []
77
- const seen = new Set() // Dedupe by path
78
-
79
- for (const baseDir of scanDirs) {
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)
191
+ for (const customPath of customScanPaths) {
192
+ if (existsSync(customPath)) scanDirs.add(customPath)
193
+ }
89
194
 
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)
195
+ const seen = new Set()
196
+ const projects = []
94
197
 
95
- const sillyspecPath = join(dirPath, '.sillyspec')
198
+ // Normalize cwd for dedup
199
+ let cwdReal
200
+ try { cwdReal = realpathSync(cwd) } catch { cwdReal = cwd }
201
+ seen.add(cwdReal.toLowerCase())
202
+ if (existsSync(join(cwd, '.sillyspec'))) {
203
+ projects.push({ name: basename(cwd), path: cwd })
204
+ }
96
205
 
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
- }
206
+ for (const baseDir of scanDirs) {
207
+ projects.push(...scanDirectory(baseDir, seen, 2, 0))
108
208
  }
109
209
 
110
210
  return projects
@@ -126,7 +226,14 @@ function handleCliExecute(ws, data) {
126
226
  return
127
227
  }
128
228
 
129
- // Find project
229
+ if (!isAllowedCliCommand(command)) {
230
+ ws.send(JSON.stringify({
231
+ type: 'cli:error',
232
+ data: { message: `Command not allowed: ${command}` }
233
+ }))
234
+ return
235
+ }
236
+
130
237
  discoverProjects().then(projects => {
131
238
  const project = projects.find(p => p.name === projectName)
132
239
  if (!project) {
@@ -137,45 +244,29 @@ function handleCliExecute(ws, data) {
137
244
  return
138
245
  }
139
246
 
140
- // Kill existing process for this project if any
141
247
  const existingKill = activeProcesses.get(projectName)
142
- if (existingKill) {
143
- existingKill()
144
- }
248
+ if (existingKill) existingKill()
145
249
 
146
- // Execute command
147
250
  const kill = executeCommand(
148
251
  project.path,
149
252
  command,
150
253
  (output) => {
151
254
  broadcast({
152
255
  type: 'cli:output',
153
- data: {
154
- projectName,
155
- output: output.data,
156
- outputType: output.type
157
- }
256
+ data: { projectName, output: output.data, outputType: output.type }
158
257
  })
159
258
  },
160
259
  (result) => {
161
260
  activeProcesses.delete(projectName)
162
261
  broadcast({
163
262
  type: 'cli:complete',
164
- data: {
165
- projectName,
166
- exitCode: result.code,
167
- signal: result.signal
168
- }
263
+ data: { projectName, exitCode: result.code, signal: result.signal }
169
264
  })
170
265
  }
171
266
  )
172
267
 
173
268
  activeProcesses.set(projectName, kill)
174
-
175
- broadcast({
176
- type: 'cli:started',
177
- data: { projectName, command }
178
- })
269
+ broadcast({ type: 'cli:started', data: { projectName, command } })
179
270
  })
180
271
  }
181
272
 
@@ -186,8 +277,14 @@ function handleCliExecute(ws, data) {
186
277
  */
187
278
  function startServer({ port = 3456, open: openBrowser = true } = {}) {
188
279
  const server = createServer((req, res) => {
189
- // CORS headers
190
- res.setHeader('Access-Control-Allow-Origin', '*')
280
+ const origin = req.headers.origin
281
+ if (!isLocalOrigin(origin)) {
282
+ res.writeHead(403)
283
+ res.end('Forbidden')
284
+ return
285
+ }
286
+ res.setHeader('Access-Control-Allow-Origin', origin || `http://127.0.0.1:${port}`)
287
+ res.setHeader('Vary', 'Origin')
191
288
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
192
289
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
193
290
 
@@ -197,7 +294,6 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
197
294
  return
198
295
  }
199
296
 
200
- // API: List all projects
201
297
  if (req.url === '/api/projects') {
202
298
  discoverProjects().then(projects => {
203
299
  res.setHeader('Content-Type', 'application/json')
@@ -210,7 +306,44 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
210
306
  return
211
307
  }
212
308
 
213
- // API: Get project details with state
309
+ // Detail API
310
+ const detailMatch = req.url?.match(/^\/api\/projects\/(.+)\/detail(\?|$)/)
311
+ if (detailMatch) {
312
+ const projectPath = decodeURIComponent(detailMatch[1])
313
+ const url = new URL(req.url, `http://${req.headers.host}`)
314
+ const type = url.searchParams.get('type')
315
+ let data
316
+ try {
317
+ if (type === 'git') data = parseGitDetail(projectPath)
318
+ else if (type === 'tech') data = parseTechStackDetail(projectPath)
319
+ else if (type === 'docs') data = parseDocsList(projectPath)
320
+ else { res.writeHead(400); res.end(JSON.stringify({ error: 'Invalid type' })); return }
321
+ res.setHeader('Content-Type', 'application/json')
322
+ res.writeHead(200)
323
+ res.end(JSON.stringify(data))
324
+ } catch (err) {
325
+ res.writeHead(500)
326
+ res.end(JSON.stringify({ error: err.message }))
327
+ }
328
+ return
329
+ }
330
+
331
+ // Overview API
332
+ if (req.url?.startsWith('/api/projects/') && req.url.endsWith('/overview')) {
333
+ const parts = req.url.replace('/api/projects/', '').replace('/overview', '').split('/')
334
+ const projectPath = decodeURIComponent(parts.join('/'))
335
+ try {
336
+ const overview = parseProjectOverview(projectPath)
337
+ res.setHeader('Content-Type', 'application/json')
338
+ res.writeHead(200)
339
+ res.end(JSON.stringify(overview))
340
+ } catch (err) {
341
+ res.writeHead(500)
342
+ res.end(JSON.stringify({ error: err.message }))
343
+ }
344
+ return
345
+ }
346
+
214
347
  if (req.url?.startsWith('/api/project/')) {
215
348
  const projectName = decodeURIComponent(req.url.split('/').pop())
216
349
  discoverProjects().then(projects => {
@@ -219,10 +352,7 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
219
352
  const state = parseProjectState(project.path)
220
353
  res.setHeader('Content-Type', 'application/json')
221
354
  res.writeHead(200)
222
- res.end(JSON.stringify({
223
- ...project,
224
- state
225
- }))
355
+ res.end(JSON.stringify({ ...project, state }))
226
356
  } else {
227
357
  res.writeHead(404)
228
358
  res.end(JSON.stringify({ error: 'Project not found' }))
@@ -234,12 +364,67 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
234
364
  return
235
365
  }
236
366
 
367
+ // Docs API
368
+ if (req.url?.startsWith('/api/docs/content')) {
369
+ const url = new URL(req.url, `http://${req.headers.host}`)
370
+ const filePath = url.searchParams.get('path')
371
+ if (!filePath) {
372
+ res.writeHead(400)
373
+ res.end(JSON.stringify({ error: 'Missing path parameter' }))
374
+ return
375
+ }
376
+ // Security: only allow reading files under a .sillyspec/docs/ tree.
377
+ const normalizedPath = resolve(filePath)
378
+ if (!isSillyspecDocsPath(normalizedPath)) {
379
+ res.writeHead(403)
380
+ res.end(JSON.stringify({ error: 'Access denied' }))
381
+ return
382
+ }
383
+ if (existsSync(normalizedPath)) {
384
+ try {
385
+ const content = readFileSync(normalizedPath, 'utf-8')
386
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8')
387
+ res.writeHead(200)
388
+ res.end(content)
389
+ } catch (err) {
390
+ res.writeHead(500)
391
+ res.end(JSON.stringify({ error: err.message }))
392
+ }
393
+ } else {
394
+ res.writeHead(404)
395
+ res.end(JSON.stringify({ error: 'File not found' }))
396
+ }
397
+ return
398
+ }
399
+
400
+ if (req.url?.startsWith('/api/docs')) {
401
+ const url = new URL(req.url, `http://${req.headers.host}`)
402
+ const projectPath = url.searchParams.get('project')
403
+ if (!projectPath) {
404
+ res.writeHead(400)
405
+ res.end(JSON.stringify({ error: 'Missing project parameter' }))
406
+ return
407
+ }
408
+ const docs = parseDocsTree(projectPath)
409
+ res.setHeader('Content-Type', 'application/json')
410
+ res.writeHead(200)
411
+ res.end(JSON.stringify(docs))
412
+ return
413
+ }
414
+
237
415
  // Serve static files (dist/)
238
416
  const distDir = join(__dirname, '../dist')
239
417
  const indexPath = join(distDir, 'index.html')
240
418
  if (existsSync(distDir)) {
241
- const filePath = join(distDir, req.url === '/' ? 'index.html' : req.url.replace(/^\//, ''))
242
- if (existsSync(filePath) && !filePath.includes('..')) {
419
+ const url = new URL(req.url || '/', `http://${req.headers.host}`)
420
+ const requestPath = decodeURIComponent(url.pathname)
421
+ const filePath = resolve(distDir, requestPath === '/' ? 'index.html' : `.${requestPath}`)
422
+ if (!isInside(distDir, filePath)) {
423
+ res.writeHead(403)
424
+ res.end('Access denied')
425
+ return
426
+ }
427
+ if (existsSync(filePath)) {
243
428
  const ext = filePath.split('.').pop()
244
429
  const mimeTypes = { html: 'text/html', js: 'application/javascript', css: 'text/css', svg: 'image/svg+xml', png: 'image/png', jpg: 'image/jpeg' }
245
430
  res.setHeader('Content-Type', mimeTypes[ext] || 'application/octet-stream')
@@ -247,7 +432,6 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
247
432
  res.end(readFileSync(filePath))
248
433
  return
249
434
  }
250
- // SPA fallback: serve index.html for unknown routes
251
435
  if (existsSync(indexPath)) {
252
436
  res.setHeader('Content-Type', 'text/html')
253
437
  res.writeHead(200)
@@ -256,32 +440,46 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
256
440
  }
257
441
  }
258
442
 
259
- // 404
260
443
  res.writeHead(404)
261
444
  res.end('Not found')
262
445
  })
263
446
 
264
- // WebSocket Server
265
447
  wss = new WebSocketServer({ server })
266
448
 
267
449
  wss.on('error', (err) => {
268
450
  console.error('WebSocket server error:', err)
269
451
  })
270
452
 
271
- wss.on('connection', (ws) => {
453
+ wss.on('connection', (ws, req) => {
454
+ if (!isLocalOrigin(req.headers.origin)) {
455
+ ws.close(1008, 'Forbidden origin')
456
+ return
457
+ }
272
458
  console.log('WebSocket client connected')
273
459
 
274
460
  // Send initial projects list
275
461
  discoverProjects().then(projects => {
276
462
  const projectsWithState = projects.map(p => ({
277
463
  ...p,
278
- state: parseProjectState(p.path)
464
+ state: parseProjectState(p.path),
465
+ overview: parseProjectOverview(p.path)
279
466
  }))
280
467
 
281
468
  ws.send(JSON.stringify({
282
469
  type: 'projects:init',
283
470
  data: projectsWithState
284
471
  }))
472
+
473
+ // Start watching progress files for all discovered projects
474
+ for (const p of projectsWithState) {
475
+ startProgressWatch(p.path)
476
+ }
477
+
478
+ // Send current scan paths
479
+ ws.send(JSON.stringify({
480
+ type: 'scan:paths',
481
+ data: getCustomScanPaths()
482
+ }))
285
483
  }).catch(err => {
286
484
  console.error('Error sending initial projects:', err)
287
485
  })
@@ -299,12 +497,37 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
299
497
  if (kill) {
300
498
  kill()
301
499
  activeProcesses.delete(data.data.projectName)
302
- broadcast({
303
- type: 'cli:killed',
304
- data: { projectName: data.data.projectName }
500
+ broadcast({ type: 'cli:killed', data: { projectName: data.data.projectName } })
501
+ }
502
+ break
503
+ case 'scan:add-path':
504
+ if (data.data?.path) {
505
+ addCustomScanPath(data.data.path)
506
+ broadcast({ type: 'scan:paths', data: getCustomScanPaths() })
507
+ // Resend projects after scan
508
+ discoverProjects().then(projects => {
509
+ broadcast({
510
+ type: 'projects:updated',
511
+ data: projects.map(p => ({ ...p, state: parseProjectState(p.path) }))
512
+ })
305
513
  })
306
514
  }
307
515
  break
516
+ case 'scan:remove-path':
517
+ if (data.data?.path) {
518
+ removeCustomScanPath(data.data.path)
519
+ ws.send(JSON.stringify({ type: 'scan:paths', data: getCustomScanPaths() }))
520
+ }
521
+ break
522
+ case 'scan:get-paths':
523
+ ws.send(JSON.stringify({ type: 'scan:paths', data: getCustomScanPaths() }))
524
+ break
525
+ case 'docs:get':
526
+ if (data.data?.projectPath) {
527
+ const docs = parseDocsTree(data.data.projectPath)
528
+ ws.send(JSON.stringify({ type: 'docs:tree', data: docs }))
529
+ }
530
+ break
308
531
  default:
309
532
  console.log('Unknown message type:', data.type)
310
533
  }
@@ -315,6 +538,12 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
315
538
 
316
539
  ws.on('close', () => {
317
540
  console.log('WebSocket client disconnected')
541
+ // Decrement progress watchers — we track which projects this client was watching
542
+ // via the initial projects:init. For simplicity, stop all that we started.
543
+ // (Other clients will re-start via their connection if still active)
544
+ for (const [path] of progressWatchers) {
545
+ stopProgressWatch(path)
546
+ }
318
547
  })
319
548
 
320
549
  ws.on('error', (err) => {
@@ -322,27 +551,21 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
322
551
  })
323
552
  })
324
553
 
325
- // Start file watcher (wrapped to avoid crashing server)
326
554
  try {
327
555
  startWatcher((projects) => {
328
- broadcast({
329
- type: 'projects:updated',
330
- data: projects
331
- })
556
+ broadcast({ type: 'projects:updated', data: projects })
332
557
  })
333
558
  } catch (err) {
334
559
  console.error('Failed to start file watcher:', err)
335
560
  }
336
561
 
337
- server.listen(port, () => {
338
- console.log(`Dashboard server running on http://localhost:${port}`)
339
-
562
+ server.listen(port, '127.0.0.1', () => {
563
+ console.log(`Dashboard server running on http://127.0.0.1:${port}`)
340
564
  if (openBrowser) {
341
- open(`http://localhost:${port}`)
565
+ open(`http://127.0.0.1:${port}`)
342
566
  }
343
567
  })
344
568
 
345
- // Handle shutdown
346
569
  const shutdown = () => {
347
570
  stopWatcher()
348
571
  activeProcesses.forEach(kill => kill())