sillyspec 3.9.1 → 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-commit/SKILL.md +1 -1
- package/.claude/skills/sillyspec-continue/SKILL.md +1 -1
- package/.claude/skills/sillyspec-explore/SKILL.md +9 -0
- package/.claude/skills/sillyspec-plan/SKILL.md +0 -35
- package/.claude/skills/sillyspec-workspace/SKILL.md +1 -1
- package/README.md +7 -6
- package/SKILL.md +15 -10
- package/package.json +1 -1
- package/packages/dashboard/dist/assets/index-BcM2J-hv.css +1 -0
- package/packages/dashboard/dist/assets/{index-RsLVPAy7.js → index-DpLHK4jv.js} +974 -974
- package/packages/dashboard/dist/index.html +16 -17
- package/packages/dashboard/dist/prototype-dashboard.html +836 -0
- package/packages/dashboard/dist/prototype-overview.html +256 -0
- package/packages/dashboard/public/prototype-dashboard.html +836 -0
- package/packages/dashboard/public/prototype-overview.html +256 -0
- package/packages/dashboard/server/index.js +18 -13
- package/packages/dashboard/server/parser.js +109 -1
- package/packages/dashboard/server/watcher.js +14 -6
- package/packages/dashboard/src/App.vue +414 -186
- package/packages/dashboard/src/components/ActionBar.vue +10 -1
- package/packages/dashboard/src/components/CommandPalette.vue +5 -1
- package/packages/dashboard/src/components/DocPreview.vue +105 -8
- package/packages/dashboard/src/components/DocTree.vue +75 -19
- package/packages/dashboard/src/components/HResizeHandle.vue +48 -0
- package/packages/dashboard/src/components/PipelineView.vue +23 -4
- package/packages/dashboard/src/components/ProjectCard.vue +187 -0
- package/packages/dashboard/src/components/ProjectOverview.vue +113 -139
- package/packages/dashboard/src/components/VResizeHandle.vue +61 -0
- package/packages/dashboard/src/composables/useDashboard.js +28 -0
- package/packages/dashboard/src/composables/useLayout.js +131 -0
- package/src/index.js +7 -0
- package/src/init.js +17 -10
- package/src/migrate.js +5 -5
- package/src/progress.js +2 -1
- package/src/run.js +72 -61
- package/src/stages/brainstorm.js +28 -3
- package/src/stages/execute.js +52 -27
- package/src/stages/explore.js +34 -0
- package/src/stages/index.js +3 -1
- package/src/stages/plan.js +86 -14
- package/src/stages/scan.js +11 -11
- package/src/stages/status.js +1 -1
- package/.sillyspec/changes/archive/2026-04-08-derive-state/design.md +0 -97
- package/.sillyspec/changes/archive/2026-04-08-derive-state/plan.md +0 -51
- package/.sillyspec/changes/archive/2026-04-08-derive-state/proposal.md +0 -29
- package/.sillyspec/changes/archive/2026-04-08-derive-state/requirements.md +0 -34
- package/.sillyspec/changes/archive/2026-04-08-derive-state/tasks.md +0 -13
- package/.sillyspec/changes/archive/2026-04-08-derive-state/verify-result.md +0 -43
- package/.sillyspec/changes/auto-mode/design.md +0 -50
- package/.sillyspec/changes/auto-mode/proposal.md +0 -19
- package/.sillyspec/changes/auto-mode/requirements.md +0 -21
- package/.sillyspec/changes/auto-mode/tasks.md +0 -7
- package/.sillyspec/changes/brainstorm-archive/2026-04-05-dashboard-design.md +0 -206
- package/.sillyspec/changes/brainstorm-archive/2026-04-05-unified-docs-design.md +0 -199
- package/.sillyspec/changes/dashboard/design.md +0 -219
- package/.sillyspec/changes/dashboard/design.md.braindraft +0 -206
- package/.sillyspec/changes/run-command-design/design.md +0 -1230
- package/.sillyspec/changes/unified-docs-design/design.md +0 -199
- package/.sillyspec/docs/sillyspec/scan/.gitkeep +0 -0
- package/.sillyspec/knowledge/INDEX.md +0 -8
- package/.sillyspec/knowledge/uncategorized.md +0 -3
- package/.sillyspec/plans/2026-04-05-dashboard.md +0 -737
- package/.sillyspec/projects/sillyspec.yaml +0 -3
- package/packages/dashboard/dist/assets/index-CntACGUN.css +0 -1
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Dashboard 概览区域原型</title>
|
|
7
|
+
<style>
|
|
8
|
+
* {
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
}
|
|
13
|
+
body {
|
|
14
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
15
|
+
background: #F5F5F7;
|
|
16
|
+
height: 100vh;
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
}
|
|
20
|
+
/* 概览区域 - 40% */
|
|
21
|
+
.overview-section {
|
|
22
|
+
height: 40%;
|
|
23
|
+
background: #FFFFFF;
|
|
24
|
+
border-bottom: 2px solid #2A3040;
|
|
25
|
+
padding: 16px 24px;
|
|
26
|
+
display: flex;
|
|
27
|
+
flex-direction: column;
|
|
28
|
+
}
|
|
29
|
+
.section-header {
|
|
30
|
+
display: flex;
|
|
31
|
+
justify-content: space-between;
|
|
32
|
+
align-items: center;
|
|
33
|
+
margin-bottom: 16px;
|
|
34
|
+
}
|
|
35
|
+
.section-title {
|
|
36
|
+
font-size: 18px;
|
|
37
|
+
font-weight: 600;
|
|
38
|
+
color: #1A1A1A;
|
|
39
|
+
}
|
|
40
|
+
.section-actions {
|
|
41
|
+
display: flex;
|
|
42
|
+
gap: 8px;
|
|
43
|
+
}
|
|
44
|
+
.btn {
|
|
45
|
+
padding: 6px 12px;
|
|
46
|
+
border-radius: 6px;
|
|
47
|
+
border: none;
|
|
48
|
+
cursor: pointer;
|
|
49
|
+
font-size: 13px;
|
|
50
|
+
font-weight: 500;
|
|
51
|
+
}
|
|
52
|
+
.btn-primary {
|
|
53
|
+
background: #D97706;
|
|
54
|
+
color: white;
|
|
55
|
+
}
|
|
56
|
+
.btn-secondary {
|
|
57
|
+
background: #E5E7EB;
|
|
58
|
+
color: #374151;
|
|
59
|
+
}
|
|
60
|
+
/* 水平滚动卡片容器 */
|
|
61
|
+
.cards-container {
|
|
62
|
+
flex: 1;
|
|
63
|
+
display: flex;
|
|
64
|
+
gap: 16px;
|
|
65
|
+
overflow-x: auto;
|
|
66
|
+
overflow-y: hidden;
|
|
67
|
+
padding-bottom: 8px;
|
|
68
|
+
}
|
|
69
|
+
.cards-container::-webkit-scrollbar {
|
|
70
|
+
height: 8px;
|
|
71
|
+
}
|
|
72
|
+
.cards-container::-webkit-scrollbar-track {
|
|
73
|
+
background: #F3F4F6;
|
|
74
|
+
border-radius: 4px;
|
|
75
|
+
}
|
|
76
|
+
.cards-container::-webkit-scrollbar-thumb {
|
|
77
|
+
background: #D1D5DB;
|
|
78
|
+
border-radius: 4px;
|
|
79
|
+
}
|
|
80
|
+
/* 项目卡片 */
|
|
81
|
+
.project-card {
|
|
82
|
+
width: 280px;
|
|
83
|
+
height: 120px;
|
|
84
|
+
background: white;
|
|
85
|
+
border: 2px solid #E5E7EB;
|
|
86
|
+
border-radius: 12px;
|
|
87
|
+
padding: 16px;
|
|
88
|
+
display: flex;
|
|
89
|
+
flex-direction: column;
|
|
90
|
+
justify-content: space-between;
|
|
91
|
+
cursor: pointer;
|
|
92
|
+
transition: all 0.2s;
|
|
93
|
+
flex-shrink: 0;
|
|
94
|
+
}
|
|
95
|
+
.project-card:hover {
|
|
96
|
+
border-color: #D97706;
|
|
97
|
+
box-shadow: 0 4px 12px rgba(217, 119, 6, 0.15);
|
|
98
|
+
}
|
|
99
|
+
.project-card.selected {
|
|
100
|
+
border-color: #D97706;
|
|
101
|
+
box-shadow: 0 0 0 3px rgba(217, 119, 6, 0.2);
|
|
102
|
+
}
|
|
103
|
+
.card-header {
|
|
104
|
+
display: flex;
|
|
105
|
+
justify-content: space-between;
|
|
106
|
+
align-items: flex-start;
|
|
107
|
+
}
|
|
108
|
+
.project-name {
|
|
109
|
+
font-size: 16px;
|
|
110
|
+
font-weight: 600;
|
|
111
|
+
color: #1A1A1A;
|
|
112
|
+
}
|
|
113
|
+
.last-active {
|
|
114
|
+
font-size: 11px;
|
|
115
|
+
color: #9CA3AF;
|
|
116
|
+
}
|
|
117
|
+
.stage-badge {
|
|
118
|
+
display: inline-block;
|
|
119
|
+
padding: 4px 10px;
|
|
120
|
+
border-radius: 12px;
|
|
121
|
+
font-size: 12px;
|
|
122
|
+
font-weight: 500;
|
|
123
|
+
}
|
|
124
|
+
.stage-badge.in-progress {
|
|
125
|
+
background: #DBEAFE;
|
|
126
|
+
color: #1D4ED8;
|
|
127
|
+
}
|
|
128
|
+
.stage-badge.completed {
|
|
129
|
+
background: #D1FAE5;
|
|
130
|
+
color: #047857;
|
|
131
|
+
}
|
|
132
|
+
.stage-badge.pending {
|
|
133
|
+
background: #F3F4F6;
|
|
134
|
+
color: #6B7280;
|
|
135
|
+
}
|
|
136
|
+
.progress-section {
|
|
137
|
+
display: flex;
|
|
138
|
+
align-items: center;
|
|
139
|
+
gap: 8px;
|
|
140
|
+
}
|
|
141
|
+
.progress-bar {
|
|
142
|
+
flex: 1;
|
|
143
|
+
height: 6px;
|
|
144
|
+
background: #E5E7EB;
|
|
145
|
+
border-radius: 3px;
|
|
146
|
+
overflow: hidden;
|
|
147
|
+
}
|
|
148
|
+
.progress-fill {
|
|
149
|
+
height: 100%;
|
|
150
|
+
border-radius: 3px;
|
|
151
|
+
transition: width 0.3s;
|
|
152
|
+
}
|
|
153
|
+
.progress-fill.in-progress { background: #3B82F6; }
|
|
154
|
+
.progress-fill.completed { background: #10B981; }
|
|
155
|
+
.progress-fill.pending { background: #9CA3AF; }
|
|
156
|
+
.progress-text {
|
|
157
|
+
font-size: 12px;
|
|
158
|
+
font-weight: 600;
|
|
159
|
+
color: #374151;
|
|
160
|
+
}
|
|
161
|
+
/* 详情区域 - 60% */
|
|
162
|
+
.detail-section {
|
|
163
|
+
flex: 1;
|
|
164
|
+
background: #FAFAFA;
|
|
165
|
+
padding: 24px;
|
|
166
|
+
display: flex;
|
|
167
|
+
align-items: center;
|
|
168
|
+
justify-content: center;
|
|
169
|
+
color: #9CA3AF;
|
|
170
|
+
font-size: 14px;
|
|
171
|
+
}
|
|
172
|
+
</style>
|
|
173
|
+
</head>
|
|
174
|
+
<body>
|
|
175
|
+
<!-- 概览区域 -->
|
|
176
|
+
<div class="overview-section">
|
|
177
|
+
<div class="section-header">
|
|
178
|
+
<h1 class="section-title">项目概览</h1>
|
|
179
|
+
<div class="section-actions">
|
|
180
|
+
<button class="btn btn-secondary">刷新</button>
|
|
181
|
+
<button class="btn btn-primary">添加项目</button>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
<div class="cards-container">
|
|
185
|
+
<!-- 项目卡片 1 - 进行中 -->
|
|
186
|
+
<div class="project-card selected">
|
|
187
|
+
<div class="card-header">
|
|
188
|
+
<span class="project-name">sillyspec</span>
|
|
189
|
+
<span class="last-active">2分钟前</span>
|
|
190
|
+
</div>
|
|
191
|
+
<div>
|
|
192
|
+
<span class="stage-badge in-progress">需求探索</span>
|
|
193
|
+
</div>
|
|
194
|
+
<div class="progress-section">
|
|
195
|
+
<div class="progress-bar">
|
|
196
|
+
<div class="progress-fill in-progress" style="width: 40%"></div>
|
|
197
|
+
</div>
|
|
198
|
+
<span class="progress-text">40%</span>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
<!-- 项目卡片 2 - 进行中 -->
|
|
202
|
+
<div class="project-card">
|
|
203
|
+
<div class="card-header">
|
|
204
|
+
<span class="project-name">my-blog</span>
|
|
205
|
+
<span class="last-active">15分钟前</span>
|
|
206
|
+
</div>
|
|
207
|
+
<div>
|
|
208
|
+
<span class="stage-badge in-progress">实现计划</span>
|
|
209
|
+
</div>
|
|
210
|
+
<div class="progress-section">
|
|
211
|
+
<div class="progress-bar">
|
|
212
|
+
<div class="progress-fill in-progress" style="width: 75%"></div>
|
|
213
|
+
</div>
|
|
214
|
+
<span class="progress-text">75%</span>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
<!-- 项目卡片 3 - 已完成 -->
|
|
218
|
+
<div class="project-card">
|
|
219
|
+
<div class="card-header">
|
|
220
|
+
<span class="project-name">user-service</span>
|
|
221
|
+
<span class="last-active">1小时前</span>
|
|
222
|
+
</div>
|
|
223
|
+
<div>
|
|
224
|
+
<span class="stage-badge completed">验证确认</span>
|
|
225
|
+
</div>
|
|
226
|
+
<div class="progress-section">
|
|
227
|
+
<div class="progress-bar">
|
|
228
|
+
<div class="progress-fill completed" style="width: 100%"></div>
|
|
229
|
+
</div>
|
|
230
|
+
<span class="progress-text">100%</span>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
<!-- 项目卡片 4 - 未开始 -->
|
|
234
|
+
<div class="project-card">
|
|
235
|
+
<div class="card-header">
|
|
236
|
+
<span class="project-name">payment-api</span>
|
|
237
|
+
<span class="last-active">昨天</span>
|
|
238
|
+
</div>
|
|
239
|
+
<div>
|
|
240
|
+
<span class="stage-badge pending">未开始</span>
|
|
241
|
+
</div>
|
|
242
|
+
<div class="progress-section">
|
|
243
|
+
<div class="progress-bar">
|
|
244
|
+
<div class="progress-fill pending" style="width: 0%"></div>
|
|
245
|
+
</div>
|
|
246
|
+
<span class="progress-text">0%</span>
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
<!-- 详情区域 -->
|
|
252
|
+
<div class="detail-section">
|
|
253
|
+
点击上方卡片查看项目详情
|
|
254
|
+
</div>
|
|
255
|
+
</body>
|
|
256
|
+
</html>
|
|
@@ -5,7 +5,7 @@ import { join, dirname, basename, sep, resolve, relative } from 'path'
|
|
|
5
5
|
import { fileURLToPath } from 'url'
|
|
6
6
|
import { homedir } from 'os'
|
|
7
7
|
import open from 'open'
|
|
8
|
-
import { parseProjectState,
|
|
8
|
+
import { parseProjectState, parseSillyspecDocsTree, parseProjectOverview, parseGitDetail, parseTechStackDetail } from './parser.js'
|
|
9
9
|
import { startWatcher, stopWatcher, addCustomScanPath, removeCustomScanPath, getCustomScanPaths, customScanPaths } from './watcher.js'
|
|
10
10
|
import { executeCommand } from './executor.js'
|
|
11
11
|
|
|
@@ -14,7 +14,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
|
14
14
|
// WebSocket clients and active processes
|
|
15
15
|
let wss = null
|
|
16
16
|
const activeProcesses = new Map()
|
|
17
|
-
const allowedStages = new Set(['brainstorm', 'plan', 'execute', 'verify', 'scan', 'quick', 'archive', 'status', 'doctor', 'auto'])
|
|
17
|
+
const allowedStages = new Set(['brainstorm', 'plan', 'execute', 'verify', 'scan', 'quick', 'explore', 'archive', 'status', 'doctor', 'auto'])
|
|
18
18
|
|
|
19
19
|
function isAllowedCliCommand(command) {
|
|
20
20
|
const args = String(command || '').trim().split(/\s+/).filter(Boolean)
|
|
@@ -97,12 +97,13 @@ function isInside(parent, child) {
|
|
|
97
97
|
return rel === '' || (!!rel && !rel.startsWith('..') && !rel.includes(`..${sep}`))
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
function
|
|
100
|
+
function isSillyspecPath(filePath) {
|
|
101
101
|
const parts = resolve(filePath).split(/[\\/]+/)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
return parts.includes('.sillyspec')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isViewableDocPath(filePath) {
|
|
106
|
+
return /\.(md|markdown|mdx|html?|txt|log|json|ya?ml|toml|xml|csv)$/i.test(filePath)
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
function isLocalOrigin(origin) {
|
|
@@ -316,7 +317,7 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
|
316
317
|
try {
|
|
317
318
|
if (type === 'git') data = parseGitDetail(projectPath)
|
|
318
319
|
else if (type === 'tech') data = parseTechStackDetail(projectPath)
|
|
319
|
-
else if (type === 'docs') data =
|
|
320
|
+
else if (type === 'docs') data = parseSillyspecDocsTree(projectPath).groups
|
|
320
321
|
else { res.writeHead(400); res.end(JSON.stringify({ error: 'Invalid type' })); return }
|
|
321
322
|
res.setHeader('Content-Type', 'application/json')
|
|
322
323
|
res.writeHead(200)
|
|
@@ -373,9 +374,9 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
|
373
374
|
res.end(JSON.stringify({ error: 'Missing path parameter' }))
|
|
374
375
|
return
|
|
375
376
|
}
|
|
376
|
-
// Security: only allow reading
|
|
377
|
+
// Security: only allow reading viewable text documents under a .sillyspec tree.
|
|
377
378
|
const normalizedPath = resolve(filePath)
|
|
378
|
-
if (!
|
|
379
|
+
if (!isSillyspecPath(normalizedPath) || !isViewableDocPath(normalizedPath)) {
|
|
379
380
|
res.writeHead(403)
|
|
380
381
|
res.end(JSON.stringify({ error: 'Access denied' }))
|
|
381
382
|
return
|
|
@@ -405,7 +406,7 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
|
405
406
|
res.end(JSON.stringify({ error: 'Missing project parameter' }))
|
|
406
407
|
return
|
|
407
408
|
}
|
|
408
|
-
const docs =
|
|
409
|
+
const docs = parseSillyspecDocsTree(projectPath)
|
|
409
410
|
res.setHeader('Content-Type', 'application/json')
|
|
410
411
|
res.writeHead(200)
|
|
411
412
|
res.end(JSON.stringify(docs))
|
|
@@ -508,7 +509,11 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
|
508
509
|
discoverProjects().then(projects => {
|
|
509
510
|
broadcast({
|
|
510
511
|
type: 'projects:updated',
|
|
511
|
-
data: projects.map(p => ({
|
|
512
|
+
data: projects.map(p => ({
|
|
513
|
+
...p,
|
|
514
|
+
state: parseProjectState(p.path),
|
|
515
|
+
overview: parseProjectOverview(p.path)
|
|
516
|
+
}))
|
|
512
517
|
})
|
|
513
518
|
})
|
|
514
519
|
}
|
|
@@ -524,7 +529,7 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
|
524
529
|
break
|
|
525
530
|
case 'docs:get':
|
|
526
531
|
if (data.data?.projectPath) {
|
|
527
|
-
const docs =
|
|
532
|
+
const docs = parseSillyspecDocsTree(data.data.projectPath)
|
|
528
533
|
ws.send(JSON.stringify({ type: 'docs:tree', data: docs }))
|
|
529
534
|
}
|
|
530
535
|
break
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync, existsSync, readdirSync, statSync } from 'fs'
|
|
2
2
|
import { execSync } from 'child_process'
|
|
3
|
-
import { join } from 'path'
|
|
3
|
+
import { join, relative, sep } from 'path'
|
|
4
4
|
import { fileURLToPath } from 'url'
|
|
5
5
|
import { dirname } from 'path'
|
|
6
6
|
|
|
@@ -323,6 +323,114 @@ export function parseDocsTree(projectPath) {
|
|
|
323
323
|
return { groups }
|
|
324
324
|
}
|
|
325
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
|
+
|
|
326
434
|
/**
|
|
327
435
|
* Parse project state from .sillyspec directory
|
|
328
436
|
* @param {string} projectPath - Path to the project directory
|
|
@@ -2,7 +2,7 @@ import chokidar from 'chokidar'
|
|
|
2
2
|
import { join, basename, dirname, sep } from 'path'
|
|
3
3
|
import { homedir } from 'os'
|
|
4
4
|
import { existsSync, readdirSync, realpathSync } from 'fs'
|
|
5
|
-
import { parseProjectState } from './parser.js'
|
|
5
|
+
import { parseProjectState, parseProjectOverview } from './parser.js'
|
|
6
6
|
|
|
7
7
|
let watcher = null
|
|
8
8
|
let updateCallback = null
|
|
@@ -146,8 +146,12 @@ function scanSelf(seen) {
|
|
|
146
146
|
const cwd = process.cwd()
|
|
147
147
|
const projects = []
|
|
148
148
|
|
|
149
|
-
|
|
150
|
-
|
|
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({
|
|
@@ -200,8 +204,9 @@ export function startWatcher(callback) {
|
|
|
200
204
|
// Parse initial states
|
|
201
205
|
for (const project of projects) {
|
|
202
206
|
const state = parseProjectState(project.path)
|
|
207
|
+
const overview = parseProjectOverview(project.path)
|
|
203
208
|
if (state) {
|
|
204
|
-
projectStates.set(project.name, { ...project, state })
|
|
209
|
+
projectStates.set(project.name, { ...project, state, overview })
|
|
205
210
|
}
|
|
206
211
|
}
|
|
207
212
|
|
|
@@ -261,8 +266,9 @@ async function handleFileChange(filePath) {
|
|
|
261
266
|
const project = projectStates.get(projectName)
|
|
262
267
|
if (project) {
|
|
263
268
|
const newState = parseProjectState(project.path)
|
|
269
|
+
const overview = parseProjectOverview(project.path)
|
|
264
270
|
if (newState) {
|
|
265
|
-
projectStates.set(projectName, { ...project, state: newState })
|
|
271
|
+
projectStates.set(projectName, { ...project, state: newState, overview })
|
|
266
272
|
}
|
|
267
273
|
}
|
|
268
274
|
|
|
@@ -280,11 +286,13 @@ async function rescanProjects() {
|
|
|
280
286
|
for (const project of projects) {
|
|
281
287
|
if (!projectStates.has(project.name)) {
|
|
282
288
|
const state = parseProjectState(project.path)
|
|
289
|
+
const overview = parseProjectOverview(project.path)
|
|
283
290
|
if (state) {
|
|
284
291
|
projectStates.set(project.name, {
|
|
285
292
|
name: project.name,
|
|
286
293
|
path: project.path,
|
|
287
|
-
state
|
|
294
|
+
state,
|
|
295
|
+
overview
|
|
288
296
|
})
|
|
289
297
|
}
|
|
290
298
|
}
|