sillyspec 3.9.0 → 3.10.0

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 (206) 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 +105 -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 +17 -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/README.md +19 -11
  21. package/SKILL.md +15 -10
  22. package/package.json +7 -9
  23. package/packages/dashboard/dist/assets/index-BcM2J-hv.css +1 -0
  24. package/packages/dashboard/dist/assets/index-DpLHK4jv.js +7446 -0
  25. package/packages/dashboard/dist/index.html +16 -16
  26. package/packages/dashboard/dist/prototype-dashboard.html +836 -0
  27. package/packages/dashboard/dist/prototype-overview.html +256 -0
  28. package/packages/dashboard/package-lock.json +226 -6
  29. package/packages/dashboard/package.json +8 -5
  30. package/packages/dashboard/public/logo.jpg +0 -0
  31. package/packages/dashboard/public/prototype-dashboard.html +836 -0
  32. package/packages/dashboard/public/prototype-overview.html +256 -0
  33. package/packages/dashboard/server/executor.js +1 -1
  34. package/packages/dashboard/server/index.js +341 -113
  35. package/packages/dashboard/server/parser.js +442 -30
  36. package/packages/dashboard/server/watcher.js +214 -134
  37. package/packages/dashboard/src/App.vue +475 -71
  38. package/packages/dashboard/src/components/ActionBar.vue +36 -43
  39. package/packages/dashboard/src/components/CommandPalette.vue +45 -66
  40. package/packages/dashboard/src/components/DetailPanel.vue +68 -53
  41. package/packages/dashboard/src/components/DocPreview.vue +257 -0
  42. package/packages/dashboard/src/components/DocTree.vue +114 -0
  43. package/packages/dashboard/src/components/HResizeHandle.vue +48 -0
  44. package/packages/dashboard/src/components/LogStream.vue +13 -33
  45. package/packages/dashboard/src/components/PipelineStage.vue +8 -8
  46. package/packages/dashboard/src/components/PipelineView.vue +99 -45
  47. package/packages/dashboard/src/components/ProjectCard.vue +187 -0
  48. package/packages/dashboard/src/components/ProjectList.vue +103 -45
  49. package/packages/dashboard/src/components/ProjectOverview.vue +152 -0
  50. package/packages/dashboard/src/components/StageBadge.vue +13 -13
  51. package/packages/dashboard/src/components/StepCard.vue +15 -15
  52. package/packages/dashboard/src/components/VResizeHandle.vue +61 -0
  53. package/packages/dashboard/src/components/detail/DocsDetail.vue +48 -0
  54. package/packages/dashboard/src/components/detail/GitDetail.vue +61 -0
  55. package/packages/dashboard/src/components/detail/TechDetail.vue +43 -0
  56. package/packages/dashboard/src/composables/useDashboard.js +48 -6
  57. package/packages/dashboard/src/composables/useKeyboard.js +6 -4
  58. package/packages/dashboard/src/composables/useLayout.js +131 -0
  59. package/packages/dashboard/src/main.js +4 -1
  60. package/packages/dashboard/src/style.css +17 -17
  61. package/src/index.js +141 -22
  62. package/src/init.js +93 -231
  63. package/src/migrate.js +117 -0
  64. package/src/progress.js +460 -0
  65. package/src/run.js +635 -0
  66. package/src/setup.js +2 -72
  67. package/src/stages/archive.js +54 -0
  68. package/src/stages/brainstorm.js +264 -0
  69. package/src/stages/doctor.js +303 -0
  70. package/src/stages/execute.js +287 -0
  71. package/src/stages/explore.js +34 -0
  72. package/src/stages/index.js +28 -0
  73. package/src/stages/plan.js +354 -0
  74. package/src/stages/propose.js +115 -0
  75. package/src/stages/quick.js +64 -0
  76. package/src/stages/scan.js +141 -0
  77. package/src/stages/status.js +65 -0
  78. package/src/stages/verify.js +135 -0
  79. package/.sillyspec/changes/dashboard/design.md +0 -219
  80. package/.sillyspec/plans/2026-04-05-dashboard.md +0 -737
  81. package/.sillyspec/specs/2026-04-05-dashboard-design.md +0 -206
  82. package/dist/steps/brainstorm/01-load-context.md +0 -30
  83. package/dist/steps/brainstorm/02-reuse-check.md +0 -6
  84. package/dist/steps/brainstorm/03-prototype-analysis.md +0 -11
  85. package/dist/steps/brainstorm/04-module-split.md +0 -23
  86. package/dist/steps/brainstorm/05-dialog-explore.md +0 -8
  87. package/dist/steps/brainstorm/06-propose-approaches.md +0 -3
  88. package/dist/steps/brainstorm/07-present-design.md +0 -3
  89. package/dist/steps/brainstorm/08-write-design.md +0 -21
  90. package/dist/steps/brainstorm/09-self-review.md +0 -15
  91. package/dist/steps/brainstorm/10-user-confirm.md +0 -3
  92. package/dist/steps/brainstorm/11-output-spec.md +0 -7
  93. package/dist/steps/brainstorm/manifest.yaml +0 -26
  94. package/dist/steps/execute/01-load-context.md +0 -41
  95. package/dist/steps/execute/02-scan-conventions.md +0 -47
  96. package/dist/steps/execute/03-skill-mcp.md +0 -19
  97. package/dist/steps/execute/04-assign-task.md +0 -22
  98. package/dist/steps/execute/04b-prompt-template.md +0 -54
  99. package/dist/steps/execute/05-write-test.md +0 -7
  100. package/dist/steps/execute/06-write-code.md +0 -8
  101. package/dist/steps/execute/07-run-test.md +0 -26
  102. package/dist/steps/execute/08-fix-issues.md +0 -28
  103. package/dist/steps/execute/09-next-task.md +0 -33
  104. package/dist/steps/execute/manifest.yaml +0 -28
  105. package/dist/steps/plan/01-load-context.md +0 -22
  106. package/dist/steps/plan/02-anchor-confirm.md +0 -1
  107. package/dist/steps/plan/03-expand-tasks.md +0 -33
  108. package/dist/steps/plan/04-mark-order.md +0 -15
  109. package/dist/steps/plan/05-e2e-planning.md +0 -17
  110. package/dist/steps/plan/06-self-check.md +0 -16
  111. package/dist/steps/plan/07-save.md +0 -1
  112. package/dist/steps/plan/manifest.yaml +0 -18
  113. package/dist/steps/scan/01-env-detect.md +0 -51
  114. package/dist/steps/scan/02-tech-stack.md +0 -16
  115. package/dist/steps/scan/03-conventions.md +0 -16
  116. package/dist/steps/scan/04-structure.md +0 -19
  117. package/dist/steps/scan/05-quality.md +0 -18
  118. package/dist/steps/scan/06-complete.md +0 -49
  119. package/dist/steps/scan/manifest.yaml +0 -16
  120. package/dist/steps/verify/01-load-specs.md +0 -28
  121. package/dist/steps/verify/02-check-tasks.md +0 -1
  122. package/dist/steps/verify/03-check-design.md +0 -6
  123. package/dist/steps/verify/04-run-tests.md +0 -7
  124. package/dist/steps/verify/05-e2e-tests.md +0 -27
  125. package/dist/steps/verify/05b-e2e-fix.md +0 -33
  126. package/dist/steps/verify/06-code-quality.md +0 -25
  127. package/dist/steps/verify/07-lint-check.md +0 -27
  128. package/dist/steps/verify/08-output-report.md +0 -14
  129. package/dist/steps/verify/manifest.yaml +0 -22
  130. package/docs/.vitepress/config.mts +0 -45
  131. package/docs/.vitepress/dist/404.html +0 -25
  132. package/docs/.vitepress/dist/assets/app.YytxICdd.js +0 -1
  133. package/docs/.vitepress/dist/assets/chunks/framework.Czhw_PXq.js +0 -19
  134. package/docs/.vitepress/dist/assets/chunks/theme.DusTRZQk.js +0 -1
  135. package/docs/.vitepress/dist/assets/index.md.C3VCvtQA.js +0 -1
  136. package/docs/.vitepress/dist/assets/index.md.C3VCvtQA.lean.js +0 -1
  137. package/docs/.vitepress/dist/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 +0 -0
  138. package/docs/.vitepress/dist/assets/inter-italic-cyrillic.By2_1cv3.woff2 +0 -0
  139. package/docs/.vitepress/dist/assets/inter-italic-greek-ext.1u6EdAuj.woff2 +0 -0
  140. package/docs/.vitepress/dist/assets/inter-italic-greek.DJ8dCoTZ.woff2 +0 -0
  141. package/docs/.vitepress/dist/assets/inter-italic-latin-ext.CN1xVJS-.woff2 +0 -0
  142. package/docs/.vitepress/dist/assets/inter-italic-latin.C2AdPX0b.woff2 +0 -0
  143. package/docs/.vitepress/dist/assets/inter-italic-vietnamese.BSbpV94h.woff2 +0 -0
  144. package/docs/.vitepress/dist/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 +0 -0
  145. package/docs/.vitepress/dist/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 +0 -0
  146. package/docs/.vitepress/dist/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 +0 -0
  147. package/docs/.vitepress/dist/assets/inter-roman-greek.BBVDIX6e.woff2 +0 -0
  148. package/docs/.vitepress/dist/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 +0 -0
  149. package/docs/.vitepress/dist/assets/inter-roman-latin.Di8DUHzh.woff2 +0 -0
  150. package/docs/.vitepress/dist/assets/inter-roman-vietnamese.BjW4sHH5.woff2 +0 -0
  151. package/docs/.vitepress/dist/assets/sillyspec_commands.md.CXFFsj08.js +0 -15
  152. package/docs/.vitepress/dist/assets/sillyspec_commands.md.CXFFsj08.lean.js +0 -1
  153. package/docs/.vitepress/dist/assets/sillyspec_dashboard.md.BuPXHqjX.js +0 -4
  154. package/docs/.vitepress/dist/assets/sillyspec_dashboard.md.BuPXHqjX.lean.js +0 -1
  155. package/docs/.vitepress/dist/assets/sillyspec_file-io.md.Cz3x7llx.js +0 -1
  156. package/docs/.vitepress/dist/assets/sillyspec_file-io.md.Cz3x7llx.lean.js +0 -1
  157. package/docs/.vitepress/dist/assets/sillyspec_getting-started.md.ClcvV8k3.js +0 -4
  158. package/docs/.vitepress/dist/assets/sillyspec_getting-started.md.ClcvV8k3.lean.js +0 -1
  159. package/docs/.vitepress/dist/assets/sillyspec_install.md.CKuR2tiT.js +0 -5
  160. package/docs/.vitepress/dist/assets/sillyspec_install.md.CKuR2tiT.lean.js +0 -1
  161. package/docs/.vitepress/dist/assets/sillyspec_lifecycle.md.DY293cR1.js +0 -28
  162. package/docs/.vitepress/dist/assets/sillyspec_lifecycle.md.DY293cR1.lean.js +0 -1
  163. package/docs/.vitepress/dist/assets/sillyspec_structure.md.sVYS4zPs.js +0 -30
  164. package/docs/.vitepress/dist/assets/sillyspec_structure.md.sVYS4zPs.lean.js +0 -1
  165. package/docs/.vitepress/dist/assets/style.DFTx90Kk.css +0 -1
  166. package/docs/.vitepress/dist/hashmap.json +0 -1
  167. package/docs/.vitepress/dist/index.html +0 -28
  168. package/docs/.vitepress/dist/sillyspec/commands.html +0 -42
  169. package/docs/.vitepress/dist/sillyspec/dashboard.html +0 -31
  170. package/docs/.vitepress/dist/sillyspec/file-io.html +0 -28
  171. package/docs/.vitepress/dist/sillyspec/getting-started.html +0 -31
  172. package/docs/.vitepress/dist/sillyspec/install.html +0 -32
  173. package/docs/.vitepress/dist/sillyspec/lifecycle.html +0 -55
  174. package/docs/.vitepress/dist/sillyspec/structure.html +0 -57
  175. package/docs/.vitepress/dist/vp-icons.css +0 -1
  176. package/docs/index.md +0 -34
  177. package/docs/sillyspec/commands.md +0 -218
  178. package/docs/sillyspec/dashboard.md +0 -51
  179. package/docs/sillyspec/file-io.md +0 -34
  180. package/docs/sillyspec/getting-started.md +0 -61
  181. package/docs/sillyspec/install.md +0 -51
  182. package/docs/sillyspec/lifecycle.md +0 -146
  183. package/docs/sillyspec/structure.md +0 -62
  184. package/packages/dashboard/dist/assets/index-Bh-GPjKY.css +0 -1
  185. package/packages/dashboard/dist/assets/index-CrCn5Gg6.js +0 -17
  186. package/src/step.js +0 -543
  187. package/templates/archive.md +0 -120
  188. package/templates/brainstorm.md +0 -170
  189. package/templates/continue.md +0 -32
  190. package/templates/execute.md +0 -304
  191. package/templates/explore.md +0 -59
  192. package/templates/export.md +0 -21
  193. package/templates/init.md +0 -61
  194. package/templates/plan.md +0 -146
  195. package/templates/quick.md +0 -135
  196. package/templates/scan-quick.md +0 -49
  197. package/templates/scan.md +0 -156
  198. package/templates/skills/playwright-e2e/SKILL.md +0 -340
  199. package/templates/status.md +0 -75
  200. package/templates/verify.md +0 -236
  201. package/templates/workspace-sync.md +0 -99
  202. package/templates/workspace.md +0 -70
  203. /package/{docs/.vitepress/dist/logo.jpg → logo.jpg} +0 -0
  204. /package/{docs/.vitepress → packages/dashboard}/dist/favicon.jpg +0 -0
  205. /package/{docs/public → packages/dashboard/dist}/logo.jpg +0 -0
  206. /package/{docs → packages/dashboard}/public/favicon.jpg +0 -0
@@ -1,59 +1,471 @@
1
- import { readFileSync, existsSync, readdirSync } from 'fs'
2
- import { join } from 'path'
1
+ import { readFileSync, existsSync, readdirSync, statSync } from 'fs'
2
+ import { execSync } from 'child_process'
3
+ import { join, relative, sep } from 'path'
3
4
  import { fileURLToPath } from 'url'
4
5
  import { dirname } from 'path'
5
6
 
6
7
  const __dirname = dirname(fileURLToPath(import.meta.url))
7
8
 
9
+ /**
10
+ * Parse docs tree for a project
11
+ * @param {string} projectPath - Path to the project directory
12
+ * @returns {object} Docs tree grouped by type
13
+ */
14
+ // Known framework detection keywords in package.json dependencies
15
+ const FRAMEWORK_PATTERNS = [
16
+ { keys: ['react', 'react-dom'], name: 'React' },
17
+ { keys: ['vue'], name: 'Vue' },
18
+ { keys: ['next'], name: 'Next.js' },
19
+ { keys: ['nuxt'], name: 'Nuxt' },
20
+ { keys: ['express'], name: 'Express' },
21
+ { keys: ['koa'], name: 'Koa' },
22
+ { keys: ['fastify'], name: 'Fastify' },
23
+ { keys: ['nestjs', '@nestjs/core'], name: 'NestJS' },
24
+ { keys: ['svelte'], name: 'Svelte' },
25
+ { keys: ['astro'], name: 'Astro' },
26
+ { keys: ['vite'], name: 'Vite' },
27
+ { keys: ['webpack'], name: 'Webpack' },
28
+ { keys: ['typescript'], name: 'TypeScript' },
29
+ { keys: ['tailwindcss'], name: 'Tailwind' },
30
+ { keys: ['prisma'], name: 'Prisma' },
31
+ { keys: ['drizzle-orm'], name: 'Drizzle' },
32
+ ]
33
+
34
+ /**
35
+ * Parse project overview info
36
+ * @param {string} projectPath
37
+ * @returns {object} Overview data
38
+ */
39
+ export function parseProjectOverview(projectPath) {
40
+ const result = {
41
+ techStack: [],
42
+ lastActive: null,
43
+ docStats: { design: 0, plan: 0, archive: 0, changes: 0, scan: 0, quicklog: 0, total: 0 },
44
+ git: { branch: '', lastCommit: '', dirtyCount: 0 }
45
+ }
46
+
47
+ // --- Tech stack ---
48
+ const pkgPath = join(projectPath, 'package.json')
49
+ if (existsSync(pkgPath)) {
50
+ try {
51
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
52
+ const deps = Object.keys({ ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) })
53
+ for (const pattern of FRAMEWORK_PATTERNS) {
54
+ if (pattern.keys.some(k => deps.includes(k))) {
55
+ result.techStack.push(pattern.name)
56
+ }
57
+ }
58
+ } catch {}
59
+ }
60
+ if (existsSync(join(projectPath, 'pom.xml'))) {
61
+ result.techStack.push('Java')
62
+ try {
63
+ const content = readFileSync(join(projectPath, 'pom.xml'), 'utf-8')
64
+ if (content.includes('spring-boot')) result.techStack.push('Spring Boot')
65
+ } catch {}
66
+ }
67
+ if (existsSync(join(projectPath, 'build.gradle')) || existsSync(join(projectPath, 'build.gradle.kts'))) {
68
+ if (!result.techStack.includes('Java')) result.techStack.push('Gradle')
69
+ }
70
+ if (existsSync(join(projectPath, 'requirements.txt')) || existsSync(join(projectPath, 'pyproject.toml'))) {
71
+ result.techStack.push('Python')
72
+ }
73
+ if (existsSync(join(projectPath, 'go.mod'))) {
74
+ result.techStack.push('Go')
75
+ }
76
+ if (result.techStack.length === 0) result.techStack = []
77
+
78
+ // --- Last active ---
79
+ const sillyspecDir = join(projectPath, '.sillyspec')
80
+ const progressPath = join(sillyspecDir, '.runtime', 'progress.json')
81
+ if (existsSync(progressPath)) {
82
+ try {
83
+ const progress = JSON.parse(readFileSync(progressPath, 'utf-8'))
84
+ if (progress.stages) {
85
+ for (const stageData of Object.values(progress.stages)) {
86
+ if (stageData.lastActive && (!result.lastActive || new Date(stageData.lastActive) > new Date(result.lastActive))) {
87
+ result.lastActive = stageData.lastActive
88
+ }
89
+ }
90
+ }
91
+ if (progress.lastActive) result.lastActive = progress.lastActive
92
+ } catch {}
93
+ }
94
+ if (!result.lastActive) {
95
+ // Fallback: most recently modified file in .sillyspec
96
+ try {
97
+ const findRecent = (dir) => {
98
+ let latest = null
99
+ if (!existsSync(dir)) return latest
100
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
101
+ const p = join(dir, entry.name)
102
+ try {
103
+ const s = statSync(p)
104
+ if (entry.isDirectory()) {
105
+ const sub = findRecent(p)
106
+ if (sub && (!latest || sub > latest)) latest = sub
107
+ } else if (!latest || s.mtimeMs > latest) {
108
+ latest = s.mtimeMs
109
+ }
110
+ } catch {}
111
+ }
112
+ return latest
113
+ }
114
+ const ts = findRecent(sillyspecDir)
115
+ if (ts) result.lastActive = new Date(ts).toISOString()
116
+ } catch {}
117
+ }
118
+
119
+ // --- Doc stats ---
120
+ const docsDir = join(sillyspecDir, 'docs')
121
+ if (existsSync(docsDir)) {
122
+ const typeMap = { brainstorm: 'design', plan: 'plan', archive: 'archive', changes: 'changes', scan: 'scan', quicklog: 'quicklog' }
123
+ try {
124
+ for (const projDir of readdirSync(docsDir, { withFileTypes: true }).filter(d => d.isDirectory())) {
125
+ for (const typeDir of readdirSync(join(docsDir, projDir.name), { withFileTypes: true }).filter(d => d.isDirectory())) {
126
+ const key = typeMap[typeDir.name]
127
+ if (!key) continue
128
+ const count = countMdFiles(join(docsDir, projDir.name, typeDir.name))
129
+ result.docStats[key] += count
130
+ result.docStats.total += count
131
+ }
132
+ }
133
+ } catch {}
134
+ }
135
+
136
+ // --- Git info ---
137
+ try {
138
+ result.git.lastCommit = execSync('git log -1 --format=%s', { cwd: projectPath, encoding: 'utf-8' }).trim()
139
+ } catch {}
140
+ try {
141
+ result.git.branch = execSync('git branch --show-current', { cwd: projectPath, encoding: 'utf-8' }).trim()
142
+ } catch {}
143
+ try {
144
+ result.git.dirtyCount = parseInt(execSync('git status --porcelain', { cwd: projectPath, encoding: 'utf-8' }).trim().split('\n').filter(Boolean).length, 10) || 0
145
+ } catch {}
146
+
147
+ return result
148
+ }
149
+
150
+ export function parseGitDetail(projectPath) {
151
+ const result = { branch: '', commits: [], untracked: [] }
152
+ try {
153
+ result.branch = execSync('git branch --show-current', { cwd: projectPath, encoding: 'utf-8' }).trim()
154
+ } catch {}
155
+ try {
156
+ const log = execSync('git log -5 --format=%h|%s|%an|%aI', { cwd: projectPath, encoding: 'utf-8' }).trim()
157
+ result.commits = log.split('\n').filter(Boolean).map(line => {
158
+ const [hash, message, author, date] = line.split('|')
159
+ return { hash, message, author, date }
160
+ })
161
+ } catch {}
162
+ try {
163
+ const status = execSync('git status --porcelain', { cwd: projectPath, encoding: 'utf-8' }).trim()
164
+ result.untracked = status.split('\n').filter(Boolean).map(line => ({
165
+ status: line.slice(0, 2).trim(),
166
+ file: line.slice(3)
167
+ }))
168
+ } catch {}
169
+ return result
170
+ }
171
+
172
+ export function parseTechStackDetail(projectPath) {
173
+ const result = { frameworks: [], dependencies: {}, devDependencies: {} }
174
+ const pkgPath = join(projectPath, 'package.json')
175
+ if (existsSync(pkgPath)) {
176
+ try {
177
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
178
+ result.dependencies = pkg.dependencies || {}
179
+ result.devDependencies = pkg.devDependencies || {}
180
+ const allDeps = Object.keys({ ...result.dependencies, ...result.devDependencies })
181
+ for (const pattern of FRAMEWORK_PATTERNS) {
182
+ if (pattern.keys.some(k => allDeps.includes(k))) {
183
+ result.frameworks.push(pattern.name)
184
+ }
185
+ }
186
+ } catch {}
187
+ }
188
+ return result
189
+ }
190
+
191
+ export function parseDocsList(projectPath) {
192
+ const sillyspecDir = join(projectPath, '.sillyspec')
193
+ const docsDir = join(sillyspecDir, 'docs')
194
+ const groups = []
195
+ if (!existsSync(docsDir)) return groups
196
+ const typeMap = {
197
+ brainstorm: { label: '设计文档', icon: '📋' },
198
+ plan: { label: '实现计划', icon: '📐' },
199
+ archive: { label: '已归档', icon: '📦' },
200
+ changes: { label: '当前变更', icon: '⚙️' },
201
+ scan: { label: '架构文档', icon: '🔍' },
202
+ quicklog: { label: '快速修复', icon: '⚡' }
203
+ }
204
+ try {
205
+ for (const projDir of readdirSync(docsDir, { withFileTypes: true }).filter(d => d.isDirectory())) {
206
+ for (const typeDir of readdirSync(join(docsDir, projDir.name), { withFileTypes: true }).filter(d => d.isDirectory())) {
207
+ const cfg = typeMap[typeDir.name]
208
+ if (!cfg) continue
209
+ const files = listFilesRecursive(join(docsDir, projDir.name, typeDir.name))
210
+ if (files.length) {
211
+ groups.push({ key: typeDir.name, label: cfg.label, icon: cfg.icon, files })
212
+ }
213
+ }
214
+ }
215
+ } catch {}
216
+ return groups
217
+ }
218
+
219
+ function listFilesRecursive(dir) {
220
+ const files = []
221
+ try {
222
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
223
+ const p = join(dir, entry.name)
224
+ if (entry.isDirectory()) {
225
+ const sub = listFilesRecursive(p)
226
+ for (const f of sub) f.name = entry.name + '/' + f.name
227
+ files.push(...sub)
228
+ } else if (entry.name.endsWith('.md')) {
229
+ const s = statSync(p)
230
+ files.push({ name: entry.name, path: p, size: s.size, mtime: s.mtime.toISOString() })
231
+ }
232
+ }
233
+ } catch {}
234
+ return files
235
+ }
236
+
237
+ function countMdFiles(dir) {
238
+ let count = 0
239
+ try {
240
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
241
+ if (entry.isDirectory()) {
242
+ count += countMdFiles(join(dir, entry.name))
243
+ } else if (entry.name.endsWith('.md')) {
244
+ count++
245
+ }
246
+ }
247
+ } catch {}
248
+ return count
249
+ }
250
+
251
+ export function parseDocsTree(projectPath) {
252
+ const sillyspecDir = join(projectPath, '.sillyspec')
253
+ const docsDir = join(sillyspecDir, 'docs')
254
+
255
+ if (!existsSync(docsDir)) {
256
+ return { groups: [] }
257
+ }
258
+
259
+ const groupConfig = [
260
+ { key: 'brainstorm', label: '📋 设计文档', icon: '📋', dir: 'brainstorm' },
261
+ { key: 'plan', label: '📐 实现计划', icon: '📐', dir: 'plan' },
262
+ { key: 'changes', label: '⚙️ 当前变更', icon: '⚙️', dir: 'changes' },
263
+ { key: 'archive', label: '📦 已归档', icon: '📦', dir: 'archive' },
264
+ { key: 'scan', label: '🔍 架构文档', icon: '🔍', dir: 'scan' },
265
+ { key: 'quicklog', label: '⚡ 快速修复', icon: '⚡', dir: 'quicklog' },
266
+ ]
267
+
268
+ const groups = []
269
+
270
+ // Find project dirs under docs/
271
+ const projectDirs = existsSync(docsDir) ? readdirSync(docsDir, { withFileTypes: true })
272
+ .filter(d => d.isDirectory()).map(d => d.name) : []
273
+
274
+ for (const projName of projectDirs) {
275
+ const projDocsDir = join(docsDir, projName)
276
+
277
+ for (const group of groupConfig) {
278
+ const groupDir = join(projDocsDir, group.dir)
279
+ if (!existsSync(groupDir)) continue
280
+
281
+ const files = []
282
+ try {
283
+ const entries = readdirSync(groupDir, { withFileTypes: true })
284
+ for (const entry of entries) {
285
+ if (entry.isDirectory()) {
286
+ // For changes/ and archive/ subdirs
287
+ const subDir = join(groupDir, entry.name)
288
+ try {
289
+ const subFiles = readdirSync(subDir).filter(f => f.endsWith('.md'))
290
+ for (const sf of subFiles) {
291
+ const filePath = join(subDir, sf)
292
+ files.push({
293
+ name: `${entry.name}/${sf}`,
294
+ path: filePath,
295
+ title: sf.replace('.md', '')
296
+ })
297
+ }
298
+ } catch {}
299
+ } else if (entry.name.endsWith('.md')) {
300
+ const filePath = join(groupDir, entry.name)
301
+ let title = entry.name.replace('.md', '')
302
+ try {
303
+ const content = readFileSync(filePath, 'utf-8')
304
+ const titleMatch = content.match(/^#\s+(.+)$/m)
305
+ if (titleMatch) title = titleMatch[1]
306
+ } catch {}
307
+ files.push({ name: entry.name, path: filePath, title })
308
+ }
309
+ }
310
+ } catch {}
311
+
312
+ if (files.length > 0) {
313
+ groups.push({
314
+ key: `${projName}::${group.key}`,
315
+ label: group.label,
316
+ project: projName,
317
+ files
318
+ })
319
+ }
320
+ }
321
+ }
322
+
323
+ return { groups }
324
+ }
325
+
326
+ const VIEWABLE_SILLYSPEC_DOC_EXTENSIONS = new Set([
327
+ '.md', '.markdown', '.mdx',
328
+ '.html', '.htm',
329
+ '.txt', '.log',
330
+ '.json', '.yaml', '.yml', '.toml',
331
+ '.xml', '.csv'
332
+ ])
333
+
334
+ const SILLYSPEC_DOC_GROUPS = [
335
+ { key: 'docs', label: '📚 docs', dir: 'docs' },
336
+ { key: 'changes', label: '⚙️ changes', dir: 'changes' },
337
+ { key: 'plans', label: '🧾 plans', dir: 'plans' },
338
+ { key: 'quicklog', label: '⚡ quicklog', dir: 'quicklog' },
339
+ { key: 'knowledge', label: '🧠 knowledge', dir: 'knowledge' },
340
+ { key: 'projects', label: '📁 projects', dir: 'projects' },
341
+ { key: 'workspace', label: '🗂️ workspace', dir: 'workspace' },
342
+ { key: 'shared', label: '🔗 shared', dir: 'shared' },
343
+ { key: 'runtime', label: '🧰 .runtime', dir: '.runtime' }
344
+ ]
345
+
346
+ function sillyspecDocExt(fileName) {
347
+ const index = fileName.lastIndexOf('.')
348
+ return index === -1 ? '' : fileName.slice(index).toLowerCase()
349
+ }
350
+
351
+ function isViewableSillyspecDoc(fileName) {
352
+ return VIEWABLE_SILLYSPEC_DOC_EXTENSIONS.has(sillyspecDocExt(fileName))
353
+ }
354
+
355
+ function titleFromSillyspecDoc(filePath, fileName) {
356
+ const ext = sillyspecDocExt(fileName)
357
+ if (ext === '.md' || ext === '.markdown' || ext === '.mdx') {
358
+ try {
359
+ const content = readFileSync(filePath, 'utf-8')
360
+ const titleMatch = content.match(/^#\s+(.+)$/m)
361
+ if (titleMatch) return titleMatch[1]
362
+ } catch {}
363
+ }
364
+ return fileName.replace(/\.(md|markdown|mdx|html?|txt|log|json|ya?ml|toml|xml|csv)$/i, '')
365
+ }
366
+
367
+ function listSillyspecDocsRecursive(dir, rootDir) {
368
+ const files = []
369
+ try {
370
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
371
+ const filePath = join(dir, entry.name)
372
+ if (entry.isDirectory()) {
373
+ files.push(...listSillyspecDocsRecursive(filePath, rootDir))
374
+ } else if (entry.isFile() && isViewableSillyspecDoc(entry.name)) {
375
+ const s = statSync(filePath)
376
+ files.push({
377
+ name: relative(rootDir, filePath).split(sep).join('/'),
378
+ path: filePath,
379
+ title: titleFromSillyspecDoc(filePath, entry.name),
380
+ extension: sillyspecDocExt(entry.name).slice(1),
381
+ size: s.size,
382
+ mtime: s.mtime.toISOString()
383
+ })
384
+ }
385
+ }
386
+ } catch {}
387
+ return files
388
+ }
389
+
390
+ export function parseSillyspecDocsTree(projectPath) {
391
+ const sillyspecDir = join(projectPath, '.sillyspec')
392
+ if (!existsSync(sillyspecDir)) return { groups: [] }
393
+
394
+ const groups = []
395
+ const seen = new Set()
396
+
397
+ for (const group of SILLYSPEC_DOC_GROUPS) {
398
+ const groupDir = join(sillyspecDir, group.dir)
399
+ if (!existsSync(groupDir)) continue
400
+
401
+ const files = listSillyspecDocsRecursive(groupDir, groupDir)
402
+ .sort((a, b) => a.name.localeCompare(b.name))
403
+
404
+ if (files.length === 0) continue
405
+ for (const file of files) seen.add(file.path)
406
+ groups.push({ key: group.key, label: group.label, project: '.sillyspec', files })
407
+ }
408
+
409
+ const rootFiles = []
410
+ try {
411
+ for (const entry of readdirSync(sillyspecDir, { withFileTypes: true })) {
412
+ if (!entry.isFile() || !isViewableSillyspecDoc(entry.name)) continue
413
+ const filePath = join(sillyspecDir, entry.name)
414
+ if (seen.has(filePath)) continue
415
+ const s = statSync(filePath)
416
+ rootFiles.push({
417
+ name: entry.name,
418
+ path: filePath,
419
+ title: titleFromSillyspecDoc(filePath, entry.name),
420
+ extension: sillyspecDocExt(entry.name).slice(1),
421
+ size: s.size,
422
+ mtime: s.mtime.toISOString()
423
+ })
424
+ }
425
+ } catch {}
426
+
427
+ if (rootFiles.length > 0) {
428
+ groups.unshift({ key: 'root', label: '📄 .sillyspec', project: '.sillyspec', files: rootFiles })
429
+ }
430
+
431
+ return { groups }
432
+ }
433
+
8
434
  /**
9
435
  * Parse project state from .sillyspec directory
10
436
  * @param {string} projectPath - Path to the project directory
11
437
  * @returns {object} Project state with currentStage, nextStep, progress, stages, specs, lastActive
12
438
  */
13
- export async function parseProjectState(projectPath) {
439
+ export function parseProjectState(projectPath) {
14
440
  const sillyspecDir = join(projectPath, '.sillyspec')
15
441
 
16
442
  if (!existsSync(sillyspecDir)) {
17
443
  return null
18
444
  }
19
445
 
20
- let currentStage = 'unknown'
446
+ let currentStage = ''
21
447
  let nextStep = null
22
448
  let progress = { stages: {} }
23
449
  let stages = []
24
450
  let specs = []
25
451
  let lastActive = null
26
452
 
27
- // Read STATE.md for current stage and next step
28
- const statePath = join(sillyspecDir, 'STATE.md')
29
- if (existsSync(statePath)) {
30
- try {
31
- const stateContent = readFileSync(statePath, 'utf-8')
32
- const stageMatch = stateContent.match(/current_stage:\s*(\w+)/i)
33
- const stepMatch = stateContent.match(/next_step:\s*(.+)/i)
34
-
35
- if (stageMatch) currentStage = stageMatch[1]
36
- if (stepMatch) nextStep = stepMatch[1].trim()
37
- } catch (err) {
38
- // State file exists but couldn't be read
39
- }
40
- }
41
-
42
- // Read progress.json from .runtime directory
453
+ // Read progress.json for current stage
43
454
  const progressPath = join(sillyspecDir, '.runtime', 'progress.json')
44
455
  if (existsSync(progressPath)) {
45
456
  try {
46
- const progressContent = readFileSync(progressPath, 'utf-8')
47
- progress = JSON.parse(progressContent)
48
- stages = Object.keys(progress.stages || {})
457
+ const progressData = JSON.parse(readFileSync(progressPath, 'utf-8'))
458
+ progress = progressData
459
+ currentStage = progressData.currentStage || ''
460
+ stages = Object.keys(progressData.stages || {})
49
461
 
50
- // Find last active stage
51
- if (progress.stages) {
52
- for (const [stageName, stageData] of Object.entries(progress.stages)) {
53
- if (stageData.lastActive) {
54
- if (!lastActive || new Date(stageData.lastActive) > new Date(lastActive)) {
55
- lastActive = stageData.lastActive
56
- }
462
+ // Find last active
463
+ if (progressData.lastActive) lastActive = progressData.lastActive
464
+ if (progressData.stages) {
465
+ for (const [stageName, stageData] of Object.entries(progressData.stages)) {
466
+ if (stageData.lastActive || stageData.startedAt) {
467
+ const t = stageData.lastActive || stageData.startedAt
468
+ if (!lastActive || new Date(t) > new Date(lastActive)) lastActive = t
57
469
  }
58
470
  }
59
471
  }