sillyspec 3.8.7 → 3.9.1

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