sillyspec 3.20.3 → 3.20.5

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 (225) hide show
  1. package/.claude/skills/sillyspec-archive/SKILL.md +21 -17
  2. package/.claude/skills/sillyspec-auto/SKILL.md +83 -77
  3. package/.claude/skills/sillyspec-brainstorm/SKILL.md +44 -17
  4. package/.claude/skills/sillyspec-commit/SKILL.md +106 -105
  5. package/.claude/skills/sillyspec-continue/SKILL.md +45 -44
  6. package/.claude/skills/sillyspec-doctor/SKILL.md +31 -22
  7. package/.claude/skills/sillyspec-execute/SKILL.md +30 -17
  8. package/.claude/skills/sillyspec-explore/SKILL.md +109 -96
  9. package/.claude/skills/sillyspec-knowledge/SKILL.md +270 -0
  10. package/.claude/skills/sillyspec-plan/SKILL.md +21 -52
  11. package/.claude/skills/sillyspec-propose/SKILL.md +21 -17
  12. package/.claude/skills/sillyspec-quick/SKILL.md +21 -17
  13. package/.claude/skills/sillyspec-resume/SKILL.md +68 -111
  14. package/.claude/skills/sillyspec-scan/SKILL.md +21 -17
  15. package/.claude/skills/sillyspec-state/SKILL.md +54 -54
  16. package/.claude/skills/sillyspec-status/SKILL.md +21 -17
  17. package/.claude/skills/sillyspec-verify/SKILL.md +21 -17
  18. package/.claude/skills/sillyspec-workspace/SKILL.md +157 -149
  19. package/.husky/pre-push +13 -0
  20. package/CLAUDE.md +18 -0
  21. package/README.md +198 -186
  22. package/SKILL.md +90 -87
  23. package/bin/sillyspec.js +2 -2
  24. package/docs/brainstorm-plan-contract.md +64 -0
  25. package/docs/plan-execute-contract.md +123 -0
  26. package/docs/platform-scan-protocol.md +298 -0
  27. package/docs/revision-mode.md +115 -0
  28. package/docs/sillyspec/file-lifecycle/known-implementation-gaps.md +99 -0
  29. package/docs/sillyspec/file-lifecycle/platform-workflows-sync.md +218 -0
  30. package/docs/sillyspec/file-lifecycle/stage-artifacts.md +167 -0
  31. package/docs/sillyspec/file-lifecycle/storage-and-state.md +148 -0
  32. package/docs/sillyspec/file-lifecycle/worktree-and-guard.md +211 -0
  33. package/docs/sillyspec/file-lifecycle.md +131 -0
  34. package/docs/workflow-contract-regression.md +106 -0
  35. package/docs/worktree-isolation.md +252 -0
  36. package/package.json +40 -34
  37. package/packages/dashboard/dist/assets/{index-D1EVTLmc.js → index-Bq_Z2hne.js} +7446 -7446
  38. package/packages/dashboard/dist/assets/index-O2W5RV4z.css +1 -0
  39. package/packages/dashboard/dist/index.html +16 -16
  40. package/packages/dashboard/dist/prototype-dashboard.html +836 -0
  41. package/packages/dashboard/dist/prototype-overview.html +256 -0
  42. package/packages/dashboard/index.html +15 -15
  43. package/packages/dashboard/package-lock.json +2384 -2384
  44. package/packages/dashboard/package.json +25 -25
  45. package/packages/dashboard/public/prototype-dashboard.html +836 -0
  46. package/packages/dashboard/public/prototype-overview.html +256 -0
  47. package/packages/dashboard/server/executor.js +86 -86
  48. package/packages/dashboard/server/index.js +588 -508
  49. package/packages/dashboard/server/parser.js +526 -458
  50. package/packages/dashboard/server/watcher.js +350 -342
  51. package/packages/dashboard/src/App.vue +558 -325
  52. package/packages/dashboard/src/components/ActionBar.vue +93 -84
  53. package/packages/dashboard/src/components/CommandPalette.vue +96 -92
  54. package/packages/dashboard/src/components/DetailPanel.vue +137 -137
  55. package/packages/dashboard/src/components/DocPreview.vue +105 -8
  56. package/packages/dashboard/src/components/DocTree.vue +75 -19
  57. package/packages/dashboard/src/components/HResizeHandle.vue +48 -0
  58. package/packages/dashboard/src/components/LogStream.vue +65 -65
  59. package/packages/dashboard/src/components/PipelineStage.vue +95 -75
  60. package/packages/dashboard/src/components/PipelineView.vue +156 -129
  61. package/packages/dashboard/src/components/ProjectCard.vue +187 -0
  62. package/packages/dashboard/src/components/ProjectList.vue +210 -210
  63. package/packages/dashboard/src/components/ProjectOverview.vue +113 -139
  64. package/packages/dashboard/src/components/StageBadge.vue +67 -53
  65. package/packages/dashboard/src/components/StepCard.vue +94 -89
  66. package/packages/dashboard/src/components/VResizeHandle.vue +61 -0
  67. package/packages/dashboard/src/components/detail/DocsDetail.vue +48 -48
  68. package/packages/dashboard/src/components/detail/GitDetail.vue +61 -61
  69. package/packages/dashboard/src/components/detail/TechDetail.vue +43 -43
  70. package/packages/dashboard/src/composables/useDashboard.js +192 -164
  71. package/packages/dashboard/src/composables/useKeyboard.js +119 -119
  72. package/packages/dashboard/src/composables/useLayout.js +131 -0
  73. package/packages/dashboard/src/composables/useWebSocket.js +129 -129
  74. package/packages/dashboard/src/main.js +8 -8
  75. package/packages/dashboard/src/style.css +132 -132
  76. package/packages/dashboard/vite.config.js +18 -18
  77. package/src/brainstorm-postcheck.js +158 -0
  78. package/src/change-list.js +52 -0
  79. package/src/change-risk-profile.js +352 -0
  80. package/src/classify-change.js +73 -0
  81. package/src/constants.js +70 -0
  82. package/src/contract-matrix.js +278 -0
  83. package/src/db.js +201 -0
  84. package/src/endpoint-extractor.js +315 -0
  85. package/src/hooks/claude-pre-tool-use.cjs +125 -0
  86. package/src/hooks/worktree-guard.js +761 -0
  87. package/src/index.js +922 -252
  88. package/src/init.js +470 -375
  89. package/src/knowledge-match.js +130 -0
  90. package/src/migrate.js +117 -117
  91. package/src/modules.js +482 -0
  92. package/src/progress.js +1734 -508
  93. package/src/run.js +3465 -652
  94. package/src/scan-postcheck.js +387 -0
  95. package/src/setup.js +398 -460
  96. package/src/stage-contract.js +700 -0
  97. package/src/stages/archive.js +160 -54
  98. package/src/stages/brainstorm-auto.js +229 -0
  99. package/src/stages/brainstorm.js +645 -239
  100. package/src/stages/doctor.js +365 -312
  101. package/src/stages/execute.js +634 -264
  102. package/src/stages/explore.js +34 -0
  103. package/src/stages/index.js +29 -35
  104. package/src/stages/knowledge.js +498 -0
  105. package/src/stages/plan-postcheck.js +518 -0
  106. package/src/stages/plan.js +582 -279
  107. package/src/stages/propose.js +174 -115
  108. package/src/stages/quick.js +102 -63
  109. package/src/stages/scan.js +558 -141
  110. package/src/stages/status.js +65 -65
  111. package/src/stages/verify.js +322 -135
  112. package/src/sync.js +497 -0
  113. package/src/task-review.js +346 -0
  114. package/src/workflow.js +785 -0
  115. package/src/worktree-apply.js +549 -0
  116. package/src/worktree-deps.js +185 -0
  117. package/src/worktree.js +982 -0
  118. package/templates/workflows/archive-impact.yaml +79 -0
  119. package/templates/workflows/scan-docs.yaml +132 -0
  120. package/test/brainstorm-plan-contract.test.mjs +273 -0
  121. package/test/check-syntax.mjs +26 -0
  122. package/test/cli-top-level-aliases.test.mjs +174 -0
  123. package/test/contract-artifacts.test.mjs +323 -0
  124. package/test/decision-supersede.test.mjs +277 -0
  125. package/test/knowledge-match.test.mjs +231 -0
  126. package/test/plan-execute-contract.test.mjs +357 -0
  127. package/test/plan-optimization.test.mjs +572 -0
  128. package/test/platform-artifacts.test.mjs +190 -0
  129. package/test/platform-failure-samples.test.mjs +199 -0
  130. package/test/platform-recovery-chain.test.mjs +179 -0
  131. package/test/platform-recovery.test.mjs +167 -0
  132. package/test/platform-scan-p0.test.mjs +175 -0
  133. package/test/revision-v1.test.mjs +1145 -0
  134. package/test/run-sanitize-project-name.test.mjs +51 -0
  135. package/test/run-scan-postcheck-fail.test.mjs +64 -0
  136. package/test/run-scan-project-parse.test.mjs +200 -0
  137. package/test/run-tests.mjs +48 -0
  138. package/test/runtime-cleanup-keeps-worktree.test.mjs +107 -0
  139. package/test/scan-docs-yaml-placeholders.test.mjs +84 -0
  140. package/test/scan-knowledge.test.mjs +175 -0
  141. package/test/scan-paths.test.mjs +68 -0
  142. package/test/scan-postcheck-project-priority.test.mjs +85 -0
  143. package/test/scan-postcheck.test.mjs +197 -0
  144. package/test/scan-workflow-anyfailed-block.test.mjs +52 -0
  145. package/test/spec-dir.test.mjs +206 -0
  146. package/test/stage-contract-failed-post-check.test.mjs +102 -0
  147. package/test/stage-contract.test.mjs +299 -0
  148. package/test/stage-definitions.test.mjs +39 -0
  149. package/test/wait-gates.test.mjs +501 -0
  150. package/test/workflow-spec-base.test.mjs +142 -0
  151. package/test/worktree-deps-provision.test.mjs +148 -0
  152. package/test/worktree-guard.test.mjs +136 -0
  153. package/test/worktree-native-overlay.test.mjs +188 -0
  154. package/.sillyspec/changes/archive/2026-04-08-derive-state/design.md +0 -97
  155. package/.sillyspec/changes/archive/2026-04-08-derive-state/plan.md +0 -51
  156. package/.sillyspec/changes/archive/2026-04-08-derive-state/proposal.md +0 -29
  157. package/.sillyspec/changes/archive/2026-04-08-derive-state/requirements.md +0 -34
  158. package/.sillyspec/changes/archive/2026-04-08-derive-state/tasks.md +0 -13
  159. package/.sillyspec/changes/archive/2026-04-08-derive-state/verify-result.md +0 -43
  160. package/.sillyspec/changes/auto-mode/design.md +0 -50
  161. package/.sillyspec/changes/auto-mode/proposal.md +0 -19
  162. package/.sillyspec/changes/auto-mode/requirements.md +0 -21
  163. package/.sillyspec/changes/auto-mode/tasks.md +0 -7
  164. package/.sillyspec/changes/brainstorm-archive/2026-04-05-dashboard-design.md +0 -206
  165. package/.sillyspec/changes/brainstorm-archive/2026-04-05-unified-docs-design.md +0 -199
  166. package/.sillyspec/changes/dashboard/design.md +0 -219
  167. package/.sillyspec/changes/dashboard/design.md.braindraft +0 -206
  168. package/.sillyspec/changes/run-command-design/design.md +0 -1230
  169. package/.sillyspec/changes/unified-docs-design/design.md +0 -199
  170. package/.sillyspec/docs/sillyspec/scan/.gitkeep +0 -0
  171. package/.sillyspec/knowledge/INDEX.md +0 -8
  172. package/.sillyspec/knowledge/uncategorized.md +0 -3
  173. package/.sillyspec/plans/2026-04-05-dashboard.md +0 -737
  174. package/.sillyspec/projects/sillyspec.yaml +0 -3
  175. package/dist/steps/brainstorm/01-load-context.md +0 -30
  176. package/dist/steps/brainstorm/02-reuse-check.md +0 -6
  177. package/dist/steps/brainstorm/03-prototype-analysis.md +0 -11
  178. package/dist/steps/brainstorm/04-module-split.md +0 -23
  179. package/dist/steps/brainstorm/05-dialog-explore.md +0 -8
  180. package/dist/steps/brainstorm/06-propose-approaches.md +0 -3
  181. package/dist/steps/brainstorm/07-present-design.md +0 -3
  182. package/dist/steps/brainstorm/08-write-design.md +0 -21
  183. package/dist/steps/brainstorm/09-self-review.md +0 -15
  184. package/dist/steps/brainstorm/10-user-confirm.md +0 -3
  185. package/dist/steps/brainstorm/11-output-spec.md +0 -7
  186. package/dist/steps/brainstorm/manifest.yaml +0 -26
  187. package/dist/steps/execute/01-load-context.md +0 -41
  188. package/dist/steps/execute/02-scan-conventions.md +0 -47
  189. package/dist/steps/execute/03-skill-mcp.md +0 -19
  190. package/dist/steps/execute/04-assign-task.md +0 -22
  191. package/dist/steps/execute/04b-prompt-template.md +0 -54
  192. package/dist/steps/execute/05-write-test.md +0 -7
  193. package/dist/steps/execute/06-write-code.md +0 -8
  194. package/dist/steps/execute/07-run-test.md +0 -26
  195. package/dist/steps/execute/08-fix-issues.md +0 -28
  196. package/dist/steps/execute/09-next-task.md +0 -33
  197. package/dist/steps/execute/manifest.yaml +0 -28
  198. package/dist/steps/plan/01-load-context.md +0 -22
  199. package/dist/steps/plan/02-anchor-confirm.md +0 -1
  200. package/dist/steps/plan/03-expand-tasks.md +0 -33
  201. package/dist/steps/plan/04-mark-order.md +0 -15
  202. package/dist/steps/plan/05-e2e-planning.md +0 -17
  203. package/dist/steps/plan/06-self-check.md +0 -16
  204. package/dist/steps/plan/07-save.md +0 -1
  205. package/dist/steps/plan/manifest.yaml +0 -18
  206. package/dist/steps/scan/01-env-detect.md +0 -51
  207. package/dist/steps/scan/02-tech-stack.md +0 -16
  208. package/dist/steps/scan/03-conventions.md +0 -16
  209. package/dist/steps/scan/04-structure.md +0 -19
  210. package/dist/steps/scan/05-quality.md +0 -18
  211. package/dist/steps/scan/06-complete.md +0 -49
  212. package/dist/steps/scan/manifest.yaml +0 -16
  213. package/dist/steps/verify/01-load-specs.md +0 -28
  214. package/dist/steps/verify/02-check-tasks.md +0 -1
  215. package/dist/steps/verify/03-check-design.md +0 -6
  216. package/dist/steps/verify/04-run-tests.md +0 -7
  217. package/dist/steps/verify/05-e2e-tests.md +0 -27
  218. package/dist/steps/verify/05b-e2e-fix.md +0 -33
  219. package/dist/steps/verify/06-code-quality.md +0 -25
  220. package/dist/steps/verify/07-lint-check.md +0 -27
  221. package/dist/steps/verify/08-output-report.md +0 -14
  222. package/dist/steps/verify/manifest.yaml +0 -22
  223. package/packages/dashboard/dist/assets/index-DGe8CqeP.css +0 -1
  224. package/src/derive.js +0 -147
  225. package/src/step.js +0 -543
@@ -1,349 +1,357 @@
1
- import chokidar from 'chokidar'
2
- import { join, basename, dirname, sep } from 'path'
3
- import { homedir } from 'os'
4
- import { existsSync, readdirSync, realpathSync } from 'fs'
5
- import { parseProjectState } from './parser.js'
6
-
7
- let watcher = null
8
- let updateCallback = null
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
- ])
20
-
21
- /**
22
- * Check if directory should be excluded from scanning
23
- * @param {string} name - Directory name
24
- * @returns {boolean}
25
- */
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
- }
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
- }
42
-
43
- /**
44
- * Build list of directories to scan
45
- * @returns {string[]}
46
- */
47
- function buildScanDirs() {
48
- const home = homedir()
49
- const cwd = process.cwd()
50
-
51
- const scanDirs = new Set()
52
-
53
- // Always scan cwd and its parent
54
- scanDirs.add(cwd)
55
- scanDirs.add(dirname(cwd))
56
-
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
- ]
74
-
75
- for (const extra of extraDirs) {
76
- const extraPath = join(home, extra)
77
- if (existsSync(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)
86
- }
87
- }
88
-
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()
102
- const projects = []
103
-
104
- try {
105
- const entries = readdirSync(baseDir, { withFileTypes: true })
106
-
107
- for (const entry of entries) {
108
- if (!entry.isDirectory()) continue
109
- if (shouldExclude(entry.name, cwd)) continue
110
-
111
- const dirPath = join(baseDir, entry.name)
112
-
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)
118
-
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
- }
127
-
128
- // Recurse into subdirectories if not at max depth
129
- if (currentDepth < maxDepth) {
130
- projects.push(...scanDirectory(dirPath, seen, maxDepth, currentDepth + 1))
131
- }
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
- */
1
+ import chokidar from 'chokidar'
2
+ import { join, basename, dirname, sep } from 'path'
3
+ import { homedir } from 'os'
4
+ import { existsSync, readdirSync, realpathSync } from 'fs'
5
+ import { parseProjectState, parseProjectOverview } from './parser.js'
6
+
7
+ let watcher = null
8
+ let updateCallback = null
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
+ ])
20
+
21
+ /**
22
+ * Check if directory should be excluded from scanning
23
+ * @param {string} name - Directory name
24
+ * @returns {boolean}
25
+ */
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
+ }
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
+ }
42
+
43
+ /**
44
+ * Build list of directories to scan
45
+ * @returns {string[]}
46
+ */
47
+ function buildScanDirs() {
48
+ const home = homedir()
49
+ const cwd = process.cwd()
50
+
51
+ const scanDirs = new Set()
52
+
53
+ // Always scan cwd and its parent
54
+ scanDirs.add(cwd)
55
+ scanDirs.add(dirname(cwd))
56
+
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
+ ]
74
+
75
+ for (const extra of extraDirs) {
76
+ const extraPath = join(home, extra)
77
+ if (existsSync(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)
86
+ }
87
+ }
88
+
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()
102
+ const projects = []
103
+
104
+ try {
105
+ const entries = readdirSync(baseDir, { withFileTypes: true })
106
+
107
+ for (const entry of entries) {
108
+ if (!entry.isDirectory()) continue
109
+ if (shouldExclude(entry.name, cwd)) continue
110
+
111
+ const dirPath = join(baseDir, entry.name)
112
+
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)
118
+
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
+ }
127
+
128
+ // Recurse into subdirectories if not at max depth
129
+ if (currentDepth < maxDepth) {
130
+ projects.push(...scanDirectory(dirPath, seen, maxDepth, currentDepth + 1))
131
+ }
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
145
  function scanSelf(seen) {
146
146
  const cwd = process.cwd()
147
147
  const projects = []
148
148
 
149
- if (!seen.has(cwd)) {
150
- seen.add(cwd)
149
+ let realPath
150
+ try { realPath = realpathSync(cwd) } catch { realPath = cwd }
151
+ const normalizedPath = realPath.toLowerCase()
152
+
153
+ if (!seen.has(normalizedPath)) {
154
+ seen.add(normalizedPath)
151
155
  const sillyspecPath = join(cwd, '.sillyspec')
152
156
  if (existsSync(sillyspecPath)) {
153
157
  projects.push({
154
- name: basename(cwd),
155
- path: cwd
156
- })
157
- }
158
- }
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
-
200
- // Parse initial states
201
- for (const project of projects) {
202
- const state = parseProjectState(project.path)
203
- if (state) {
204
- projectStates.set(project.name, { ...project, state })
205
- }
206
- }
207
-
208
- // Emit initial state
209
- if (updateCallback) {
210
- updateCallback(Array.from(projectStates.values()))
211
- }
212
-
213
- // Watch for changes
214
- watcher = chokidar.watch(watchPaths, {
215
- ignored: /node_modules/,
216
- persistent: true,
217
- ignoreInitial: true,
218
- awaitWriteFinish: {
219
- stabilityThreshold: 300,
220
- pollInterval: 100
221
- },
222
- depth: 5
223
- })
224
-
225
- watcher.on('change', async (filePath) => {
226
- await handleFileChange(filePath)
227
- })
228
-
229
- watcher.on('add', async (filePath) => {
230
- await handleFileChange(filePath)
231
- })
232
-
233
- watcher.on('unlink', async (filePath) => {
234
- await handleFileChange(filePath)
235
- })
236
-
237
- watcher.on('error', (error) => {
238
- console.error('Watcher error:', error)
239
- })
240
-
241
- return watcher
242
- }
243
-
244
- /**
245
- * Handle file change events
246
- * @param {string} filePath - Path to the changed file
247
- */
248
- async function handleFileChange(filePath) {
249
- // Normalize path for comparison
250
- const normalizedPath = filePath.replace(/\\/g, '/')
251
-
252
- const projectName = Array.from(projectStates.values()).find(p =>
253
- normalizedPath.startsWith(p.path.replace(/\\/g, '/'))
254
- )?.name
255
-
256
- if (!projectName) {
257
- await rescanProjects()
258
- return
259
- }
260
-
261
- const project = projectStates.get(projectName)
262
- if (project) {
263
- const newState = parseProjectState(project.path)
264
- if (newState) {
265
- projectStates.set(projectName, { ...project, state: newState })
266
- }
267
- }
268
-
269
- if (updateCallback) {
270
- updateCallback(Array.from(projectStates.values()))
271
- }
272
- }
273
-
274
- /**
275
- * Re-scan for projects (e.g., new .sillyspec directories)
276
- */
277
- async function rescanProjects() {
278
- const { projects } = discoverAll()
279
-
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
- })
289
- }
290
- }
291
- }
292
-
293
- if (updateCallback) {
294
- updateCallback(Array.from(projectStates.values()))
295
- }
296
- }
297
-
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
- }
306
-
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
- }
314
-
315
- /**
316
- * Get list of custom scan paths
317
- * @returns {string[]}
318
- */
319
- export function getCustomScanPaths() {
320
- return Array.from(customScanPaths)
321
- }
322
-
323
- /**
324
- * Stop the file watcher
325
- */
326
- export function stopWatcher() {
327
- if (watcher) {
328
- watcher.close()
329
- watcher = null
330
- projectStates.clear()
331
- }
332
- }
333
-
334
- /**
335
- * Get current project states
336
- * @returns {array} Array of project states
337
- */
338
- export function getProjectStates() {
339
- return Array.from(projectStates.values())
340
- }
341
-
342
- /**
343
- * Get a specific project state
344
- * @param {string} projectName - Name of the project
345
- * @returns {object|null} Project state or null
346
- */
347
- export function getProjectState(projectName) {
348
- return projectStates.get(projectName) || null
349
- }
158
+ name: basename(cwd),
159
+ path: cwd
160
+ })
161
+ }
162
+ }
163
+
164
+ return projects
165
+ }
166
+
167
+ /**
168
+ * Discover all .sillyspec projects
169
+ * @returns {{ projects: Array, watchPaths: string[] }}
170
+ */
171
+ function discoverAll() {
172
+ const scanDirs = buildScanDirs()
173
+ const seen = new Set()
174
+ const allProjects = []
175
+
176
+ // Check cwd itself first
177
+ allProjects.push(...scanSelf(seen))
178
+
179
+ // Scan each base directory
180
+ for (const baseDir of scanDirs) {
181
+ allProjects.push(...scanDirectory(baseDir, seen, 2, 0))
182
+ }
183
+
184
+ // Build watch paths
185
+ const watchPaths = allProjects.map(p => join(p.path, '.sillyspec'))
186
+
187
+ return { projects: allProjects, watchPaths }
188
+ }
189
+
190
+ /**
191
+ * Start watching all .sillyspec directories
192
+ * @param {function} callback - Callback function when projects are updated
193
+ * @returns {object} The watcher instance
194
+ */
195
+ export function startWatcher(callback) {
196
+ if (watcher) {
197
+ stopWatcher()
198
+ }
199
+
200
+ updateCallback = callback
201
+
202
+ const { projects, watchPaths } = discoverAll()
203
+
204
+ // Parse initial states
205
+ for (const project of projects) {
206
+ const state = parseProjectState(project.path)
207
+ const overview = parseProjectOverview(project.path)
208
+ if (state) {
209
+ projectStates.set(project.name, { ...project, state, overview })
210
+ }
211
+ }
212
+
213
+ // Emit initial state
214
+ if (updateCallback) {
215
+ updateCallback(Array.from(projectStates.values()))
216
+ }
217
+
218
+ // Watch for changes
219
+ watcher = chokidar.watch(watchPaths, {
220
+ ignored: /node_modules/,
221
+ persistent: true,
222
+ ignoreInitial: true,
223
+ awaitWriteFinish: {
224
+ stabilityThreshold: 300,
225
+ pollInterval: 100
226
+ },
227
+ depth: 5
228
+ })
229
+
230
+ watcher.on('change', async (filePath) => {
231
+ await handleFileChange(filePath)
232
+ })
233
+
234
+ watcher.on('add', async (filePath) => {
235
+ await handleFileChange(filePath)
236
+ })
237
+
238
+ watcher.on('unlink', async (filePath) => {
239
+ await handleFileChange(filePath)
240
+ })
241
+
242
+ watcher.on('error', (error) => {
243
+ console.error('Watcher error:', error)
244
+ })
245
+
246
+ return watcher
247
+ }
248
+
249
+ /**
250
+ * Handle file change events
251
+ * @param {string} filePath - Path to the changed file
252
+ */
253
+ async function handleFileChange(filePath) {
254
+ // Normalize path for comparison
255
+ const normalizedPath = filePath.replace(/\\/g, '/')
256
+
257
+ const projectName = Array.from(projectStates.values()).find(p =>
258
+ normalizedPath.startsWith(p.path.replace(/\\/g, '/'))
259
+ )?.name
260
+
261
+ if (!projectName) {
262
+ await rescanProjects()
263
+ return
264
+ }
265
+
266
+ const project = projectStates.get(projectName)
267
+ if (project) {
268
+ const newState = parseProjectState(project.path)
269
+ const overview = parseProjectOverview(project.path)
270
+ if (newState) {
271
+ projectStates.set(projectName, { ...project, state: newState, overview })
272
+ }
273
+ }
274
+
275
+ if (updateCallback) {
276
+ updateCallback(Array.from(projectStates.values()))
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Re-scan for projects (e.g., new .sillyspec directories)
282
+ */
283
+ async function rescanProjects() {
284
+ const { projects } = discoverAll()
285
+
286
+ for (const project of projects) {
287
+ if (!projectStates.has(project.name)) {
288
+ const state = parseProjectState(project.path)
289
+ const overview = parseProjectOverview(project.path)
290
+ if (state) {
291
+ projectStates.set(project.name, {
292
+ name: project.name,
293
+ path: project.path,
294
+ state,
295
+ overview
296
+ })
297
+ }
298
+ }
299
+ }
300
+
301
+ if (updateCallback) {
302
+ updateCallback(Array.from(projectStates.values()))
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Add a custom scan path and rescan
308
+ * @param {string} path - Path to add
309
+ */
310
+ export function addCustomScanPath(path) {
311
+ customScanPaths.add(path)
312
+ rescanProjects()
313
+ }
314
+
315
+ /**
316
+ * Remove a custom scan path
317
+ * @param {string} path - Path to remove
318
+ */
319
+ export function removeCustomScanPath(path) {
320
+ customScanPaths.delete(path)
321
+ }
322
+
323
+ /**
324
+ * Get list of custom scan paths
325
+ * @returns {string[]}
326
+ */
327
+ export function getCustomScanPaths() {
328
+ return Array.from(customScanPaths)
329
+ }
330
+
331
+ /**
332
+ * Stop the file watcher
333
+ */
334
+ export function stopWatcher() {
335
+ if (watcher) {
336
+ watcher.close()
337
+ watcher = null
338
+ projectStates.clear()
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Get current project states
344
+ * @returns {array} Array of project states
345
+ */
346
+ export function getProjectStates() {
347
+ return Array.from(projectStates.values())
348
+ }
349
+
350
+ /**
351
+ * Get a specific project state
352
+ * @param {string} projectName - Name of the project
353
+ * @returns {object|null} Project state or null
354
+ */
355
+ export function getProjectState(projectName) {
356
+ return projectStates.get(projectName) || null
357
+ }