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.
- package/.claude/skills/sillyspec-archive/SKILL.md +17 -0
- package/.claude/skills/sillyspec-auto/SKILL.md +78 -0
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +17 -0
- package/{templates/commit.md → .claude/skills/sillyspec-commit/SKILL.md} +32 -47
- package/.claude/skills/sillyspec-continue/SKILL.md +45 -0
- package/.claude/skills/sillyspec-doctor/SKILL.md +27 -0
- package/.claude/skills/sillyspec-execute/SKILL.md +17 -0
- package/.claude/skills/sillyspec-explore/SKILL.md +105 -0
- package/.claude/skills/sillyspec-export/SKILL.md +53 -0
- package/.claude/skills/sillyspec-init/SKILL.md +170 -0
- package/.claude/skills/sillyspec-plan/SKILL.md +17 -0
- package/.claude/skills/sillyspec-propose/SKILL.md +17 -0
- package/.claude/skills/sillyspec-quick/SKILL.md +17 -0
- package/.claude/skills/sillyspec-resume/SKILL.md +111 -0
- package/.claude/skills/sillyspec-scan/SKILL.md +17 -0
- package/.claude/skills/sillyspec-state/SKILL.md +54 -0
- package/.claude/skills/sillyspec-status/SKILL.md +17 -0
- package/.claude/skills/sillyspec-verify/SKILL.md +17 -0
- package/.claude/skills/sillyspec-workspace/SKILL.md +149 -0
- package/README.md +19 -11
- package/SKILL.md +15 -10
- package/package.json +7 -9
- package/packages/dashboard/dist/assets/index-BcM2J-hv.css +1 -0
- package/packages/dashboard/dist/assets/index-DpLHK4jv.js +7446 -0
- package/packages/dashboard/dist/index.html +16 -16
- package/packages/dashboard/dist/prototype-dashboard.html +836 -0
- package/packages/dashboard/dist/prototype-overview.html +256 -0
- package/packages/dashboard/package-lock.json +226 -6
- package/packages/dashboard/package.json +8 -5
- package/packages/dashboard/public/logo.jpg +0 -0
- package/packages/dashboard/public/prototype-dashboard.html +836 -0
- package/packages/dashboard/public/prototype-overview.html +256 -0
- package/packages/dashboard/server/executor.js +1 -1
- package/packages/dashboard/server/index.js +341 -113
- package/packages/dashboard/server/parser.js +442 -30
- package/packages/dashboard/server/watcher.js +214 -134
- package/packages/dashboard/src/App.vue +475 -71
- package/packages/dashboard/src/components/ActionBar.vue +36 -43
- package/packages/dashboard/src/components/CommandPalette.vue +45 -66
- package/packages/dashboard/src/components/DetailPanel.vue +68 -53
- package/packages/dashboard/src/components/DocPreview.vue +257 -0
- package/packages/dashboard/src/components/DocTree.vue +114 -0
- package/packages/dashboard/src/components/HResizeHandle.vue +48 -0
- package/packages/dashboard/src/components/LogStream.vue +13 -33
- package/packages/dashboard/src/components/PipelineStage.vue +8 -8
- package/packages/dashboard/src/components/PipelineView.vue +99 -45
- package/packages/dashboard/src/components/ProjectCard.vue +187 -0
- package/packages/dashboard/src/components/ProjectList.vue +103 -45
- package/packages/dashboard/src/components/ProjectOverview.vue +152 -0
- package/packages/dashboard/src/components/StageBadge.vue +13 -13
- package/packages/dashboard/src/components/StepCard.vue +15 -15
- package/packages/dashboard/src/components/VResizeHandle.vue +61 -0
- package/packages/dashboard/src/components/detail/DocsDetail.vue +48 -0
- package/packages/dashboard/src/components/detail/GitDetail.vue +61 -0
- package/packages/dashboard/src/components/detail/TechDetail.vue +43 -0
- package/packages/dashboard/src/composables/useDashboard.js +48 -6
- package/packages/dashboard/src/composables/useKeyboard.js +6 -4
- package/packages/dashboard/src/composables/useLayout.js +131 -0
- package/packages/dashboard/src/main.js +4 -1
- package/packages/dashboard/src/style.css +17 -17
- package/src/index.js +141 -22
- package/src/init.js +93 -231
- package/src/migrate.js +117 -0
- package/src/progress.js +460 -0
- package/src/run.js +635 -0
- package/src/setup.js +2 -72
- package/src/stages/archive.js +54 -0
- package/src/stages/brainstorm.js +264 -0
- package/src/stages/doctor.js +303 -0
- package/src/stages/execute.js +287 -0
- package/src/stages/explore.js +34 -0
- package/src/stages/index.js +28 -0
- package/src/stages/plan.js +354 -0
- package/src/stages/propose.js +115 -0
- package/src/stages/quick.js +64 -0
- package/src/stages/scan.js +141 -0
- package/src/stages/status.js +65 -0
- package/src/stages/verify.js +135 -0
- package/.sillyspec/changes/dashboard/design.md +0 -219
- package/.sillyspec/plans/2026-04-05-dashboard.md +0 -737
- package/.sillyspec/specs/2026-04-05-dashboard-design.md +0 -206
- package/dist/steps/brainstorm/01-load-context.md +0 -30
- package/dist/steps/brainstorm/02-reuse-check.md +0 -6
- package/dist/steps/brainstorm/03-prototype-analysis.md +0 -11
- package/dist/steps/brainstorm/04-module-split.md +0 -23
- package/dist/steps/brainstorm/05-dialog-explore.md +0 -8
- package/dist/steps/brainstorm/06-propose-approaches.md +0 -3
- package/dist/steps/brainstorm/07-present-design.md +0 -3
- package/dist/steps/brainstorm/08-write-design.md +0 -21
- package/dist/steps/brainstorm/09-self-review.md +0 -15
- package/dist/steps/brainstorm/10-user-confirm.md +0 -3
- package/dist/steps/brainstorm/11-output-spec.md +0 -7
- package/dist/steps/brainstorm/manifest.yaml +0 -26
- package/dist/steps/execute/01-load-context.md +0 -41
- package/dist/steps/execute/02-scan-conventions.md +0 -47
- package/dist/steps/execute/03-skill-mcp.md +0 -19
- package/dist/steps/execute/04-assign-task.md +0 -22
- package/dist/steps/execute/04b-prompt-template.md +0 -54
- package/dist/steps/execute/05-write-test.md +0 -7
- package/dist/steps/execute/06-write-code.md +0 -8
- package/dist/steps/execute/07-run-test.md +0 -26
- package/dist/steps/execute/08-fix-issues.md +0 -28
- package/dist/steps/execute/09-next-task.md +0 -33
- package/dist/steps/execute/manifest.yaml +0 -28
- package/dist/steps/plan/01-load-context.md +0 -22
- package/dist/steps/plan/02-anchor-confirm.md +0 -1
- package/dist/steps/plan/03-expand-tasks.md +0 -33
- package/dist/steps/plan/04-mark-order.md +0 -15
- package/dist/steps/plan/05-e2e-planning.md +0 -17
- package/dist/steps/plan/06-self-check.md +0 -16
- package/dist/steps/plan/07-save.md +0 -1
- package/dist/steps/plan/manifest.yaml +0 -18
- package/dist/steps/scan/01-env-detect.md +0 -51
- package/dist/steps/scan/02-tech-stack.md +0 -16
- package/dist/steps/scan/03-conventions.md +0 -16
- package/dist/steps/scan/04-structure.md +0 -19
- package/dist/steps/scan/05-quality.md +0 -18
- package/dist/steps/scan/06-complete.md +0 -49
- package/dist/steps/scan/manifest.yaml +0 -16
- package/dist/steps/verify/01-load-specs.md +0 -28
- package/dist/steps/verify/02-check-tasks.md +0 -1
- package/dist/steps/verify/03-check-design.md +0 -6
- package/dist/steps/verify/04-run-tests.md +0 -7
- package/dist/steps/verify/05-e2e-tests.md +0 -27
- package/dist/steps/verify/05b-e2e-fix.md +0 -33
- package/dist/steps/verify/06-code-quality.md +0 -25
- package/dist/steps/verify/07-lint-check.md +0 -27
- package/dist/steps/verify/08-output-report.md +0 -14
- package/dist/steps/verify/manifest.yaml +0 -22
- package/docs/.vitepress/config.mts +0 -45
- package/docs/.vitepress/dist/404.html +0 -25
- package/docs/.vitepress/dist/assets/app.YytxICdd.js +0 -1
- package/docs/.vitepress/dist/assets/chunks/framework.Czhw_PXq.js +0 -19
- package/docs/.vitepress/dist/assets/chunks/theme.DusTRZQk.js +0 -1
- package/docs/.vitepress/dist/assets/index.md.C3VCvtQA.js +0 -1
- package/docs/.vitepress/dist/assets/index.md.C3VCvtQA.lean.js +0 -1
- package/docs/.vitepress/dist/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-cyrillic.By2_1cv3.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-greek-ext.1u6EdAuj.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-greek.DJ8dCoTZ.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-latin-ext.CN1xVJS-.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-latin.C2AdPX0b.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-vietnamese.BSbpV94h.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-greek.BBVDIX6e.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-latin.Di8DUHzh.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-vietnamese.BjW4sHH5.woff2 +0 -0
- package/docs/.vitepress/dist/assets/sillyspec_commands.md.CXFFsj08.js +0 -15
- package/docs/.vitepress/dist/assets/sillyspec_commands.md.CXFFsj08.lean.js +0 -1
- package/docs/.vitepress/dist/assets/sillyspec_dashboard.md.BuPXHqjX.js +0 -4
- package/docs/.vitepress/dist/assets/sillyspec_dashboard.md.BuPXHqjX.lean.js +0 -1
- package/docs/.vitepress/dist/assets/sillyspec_file-io.md.Cz3x7llx.js +0 -1
- package/docs/.vitepress/dist/assets/sillyspec_file-io.md.Cz3x7llx.lean.js +0 -1
- package/docs/.vitepress/dist/assets/sillyspec_getting-started.md.ClcvV8k3.js +0 -4
- package/docs/.vitepress/dist/assets/sillyspec_getting-started.md.ClcvV8k3.lean.js +0 -1
- package/docs/.vitepress/dist/assets/sillyspec_install.md.CKuR2tiT.js +0 -5
- package/docs/.vitepress/dist/assets/sillyspec_install.md.CKuR2tiT.lean.js +0 -1
- package/docs/.vitepress/dist/assets/sillyspec_lifecycle.md.DY293cR1.js +0 -28
- package/docs/.vitepress/dist/assets/sillyspec_lifecycle.md.DY293cR1.lean.js +0 -1
- package/docs/.vitepress/dist/assets/sillyspec_structure.md.sVYS4zPs.js +0 -30
- package/docs/.vitepress/dist/assets/sillyspec_structure.md.sVYS4zPs.lean.js +0 -1
- package/docs/.vitepress/dist/assets/style.DFTx90Kk.css +0 -1
- package/docs/.vitepress/dist/hashmap.json +0 -1
- package/docs/.vitepress/dist/index.html +0 -28
- package/docs/.vitepress/dist/sillyspec/commands.html +0 -42
- package/docs/.vitepress/dist/sillyspec/dashboard.html +0 -31
- package/docs/.vitepress/dist/sillyspec/file-io.html +0 -28
- package/docs/.vitepress/dist/sillyspec/getting-started.html +0 -31
- package/docs/.vitepress/dist/sillyspec/install.html +0 -32
- package/docs/.vitepress/dist/sillyspec/lifecycle.html +0 -55
- package/docs/.vitepress/dist/sillyspec/structure.html +0 -57
- package/docs/.vitepress/dist/vp-icons.css +0 -1
- package/docs/index.md +0 -34
- package/docs/sillyspec/commands.md +0 -218
- package/docs/sillyspec/dashboard.md +0 -51
- package/docs/sillyspec/file-io.md +0 -34
- package/docs/sillyspec/getting-started.md +0 -61
- package/docs/sillyspec/install.md +0 -51
- package/docs/sillyspec/lifecycle.md +0 -146
- package/docs/sillyspec/structure.md +0 -62
- package/packages/dashboard/dist/assets/index-Bh-GPjKY.css +0 -1
- package/packages/dashboard/dist/assets/index-CrCn5Gg6.js +0 -17
- package/src/step.js +0 -543
- package/templates/archive.md +0 -120
- package/templates/brainstorm.md +0 -170
- package/templates/continue.md +0 -32
- package/templates/execute.md +0 -304
- package/templates/explore.md +0 -59
- package/templates/export.md +0 -21
- package/templates/init.md +0 -61
- package/templates/plan.md +0 -146
- package/templates/quick.md +0 -135
- package/templates/scan-quick.md +0 -49
- package/templates/scan.md +0 -156
- package/templates/skills/playwright-e2e/SKILL.md +0 -340
- package/templates/status.md +0 -75
- package/templates/verify.md +0 -236
- package/templates/workspace-sync.md +0 -99
- package/templates/workspace.md +0 -70
- /package/{docs/.vitepress/dist/logo.jpg → logo.jpg} +0 -0
- /package/{docs/.vitepress → packages/dashboard}/dist/favicon.jpg +0 -0
- /package/{docs/public → packages/dashboard/dist}/logo.jpg +0 -0
- /package/{docs → packages/dashboard}/public/favicon.jpg +0 -0
|
@@ -1,59 +1,471 @@
|
|
|
1
|
-
import { readFileSync, existsSync, readdirSync } from 'fs'
|
|
2
|
-
import {
|
|
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
|
|
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 = '
|
|
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
|
|
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
|
|
47
|
-
progress =
|
|
48
|
-
|
|
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
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
}
|