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,11 +1,12 @@
|
|
|
1
1
|
import { createServer } from 'http'
|
|
2
2
|
import { WebSocketServer } from 'ws'
|
|
3
|
-
import { existsSync, readFileSync } from 'fs'
|
|
4
|
-
import { join, dirname } from 'path'
|
|
3
|
+
import { existsSync, readFileSync, readdirSync, realpathSync, watch } from 'fs'
|
|
4
|
+
import { join, dirname, basename, sep, resolve, relative } from 'path'
|
|
5
5
|
import { fileURLToPath } from 'url'
|
|
6
|
+
import { homedir } from 'os'
|
|
6
7
|
import open from 'open'
|
|
7
|
-
import { parseProjectState } from './parser.js'
|
|
8
|
-
import { startWatcher, stopWatcher } from './watcher.js'
|
|
8
|
+
import { parseProjectState, parseSillyspecDocsTree, parseProjectOverview, parseGitDetail, parseTechStackDetail } from './parser.js'
|
|
9
|
+
import { startWatcher, stopWatcher, addCustomScanPath, removeCustomScanPath, getCustomScanPaths, customScanPaths } from './watcher.js'
|
|
9
10
|
import { executeCommand } from './executor.js'
|
|
10
11
|
|
|
11
12
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
@@ -13,6 +14,69 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
|
13
14
|
// WebSocket clients and active processes
|
|
14
15
|
let wss = null
|
|
15
16
|
const activeProcesses = new Map()
|
|
17
|
+
const allowedStages = new Set(['brainstorm', 'plan', 'execute', 'verify', 'scan', 'quick', 'explore', 'archive', 'status', 'doctor', 'auto'])
|
|
18
|
+
|
|
19
|
+
function isAllowedCliCommand(command) {
|
|
20
|
+
const args = String(command || '').trim().split(/\s+/).filter(Boolean)
|
|
21
|
+
if (args.length === 0) return false
|
|
22
|
+
if (args[0] === 'run') {
|
|
23
|
+
if (!allowedStages.has(args[1])) return false
|
|
24
|
+
const allowedFlags = new Set(['--status', '--reset', '--done', '--skip', '--output', '--input', '--change'])
|
|
25
|
+
for (let i = 2; i < args.length; i++) {
|
|
26
|
+
if (args[i].startsWith('-') && !allowedFlags.has(args[i])) return false
|
|
27
|
+
if (['--output', '--input', '--change'].includes(args[i])) i++
|
|
28
|
+
}
|
|
29
|
+
return true
|
|
30
|
+
}
|
|
31
|
+
if (args[0] === 'progress') {
|
|
32
|
+
const sub = args[1]
|
|
33
|
+
if (!['show', 'status', 'validate', 'reset'].includes(sub)) return false
|
|
34
|
+
for (let i = 2; i < args.length; i++) {
|
|
35
|
+
if (!['--stage', '--deep'].includes(args[i])) return false
|
|
36
|
+
if (args[i] === '--stage') i++
|
|
37
|
+
}
|
|
38
|
+
return true
|
|
39
|
+
}
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Progress file watchers: projectPath -> { watcher, timer, refCount }
|
|
44
|
+
const progressWatchers = new Map()
|
|
45
|
+
|
|
46
|
+
function startProgressWatch(projectPath) {
|
|
47
|
+
if (progressWatchers.has(projectPath)) {
|
|
48
|
+
progressWatchers.get(projectPath).refCount++
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
const progressFile = join(projectPath, '.sillyspec', '.runtime', 'progress.json')
|
|
52
|
+
if (!existsSync(progressFile)) return
|
|
53
|
+
|
|
54
|
+
let timer = null
|
|
55
|
+
try {
|
|
56
|
+
const watcher = watch(progressFile, (eventType) => {
|
|
57
|
+
if (timer) clearTimeout(timer)
|
|
58
|
+
timer = setTimeout(() => {
|
|
59
|
+
timer = null
|
|
60
|
+
try {
|
|
61
|
+
const state = parseProjectState(projectPath)
|
|
62
|
+
broadcast({ type: 'project:update', data: { path: projectPath, state } })
|
|
63
|
+
} catch {}
|
|
64
|
+
}, 200)
|
|
65
|
+
})
|
|
66
|
+
progressWatchers.set(projectPath, { watcher, timer, refCount: 1 })
|
|
67
|
+
} catch {}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function stopProgressWatch(projectPath) {
|
|
71
|
+
const entry = progressWatchers.get(projectPath)
|
|
72
|
+
if (!entry) return
|
|
73
|
+
entry.refCount--
|
|
74
|
+
if (entry.refCount <= 0) {
|
|
75
|
+
entry.watcher.close()
|
|
76
|
+
if (entry.timer) clearTimeout(entry.timer)
|
|
77
|
+
progressWatchers.delete(projectPath)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
16
80
|
|
|
17
81
|
/**
|
|
18
82
|
* Broadcast message to all connected WebSocket clients
|
|
@@ -28,83 +92,120 @@ function broadcast(data) {
|
|
|
28
92
|
})
|
|
29
93
|
}
|
|
30
94
|
|
|
95
|
+
function isInside(parent, child) {
|
|
96
|
+
const rel = relative(resolve(parent), resolve(child))
|
|
97
|
+
return rel === '' || (!!rel && !rel.startsWith('..') && !rel.includes(`..${sep}`))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isSillyspecPath(filePath) {
|
|
101
|
+
const parts = resolve(filePath).split(/[\\/]+/)
|
|
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)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isLocalOrigin(origin) {
|
|
110
|
+
if (!origin) return true
|
|
111
|
+
try {
|
|
112
|
+
const { hostname } = new URL(origin)
|
|
113
|
+
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1'
|
|
114
|
+
} catch {
|
|
115
|
+
return false
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// --- Shared scan logic (aligned with watcher.js) ---
|
|
120
|
+
|
|
121
|
+
const excludeDirs = new Set([
|
|
122
|
+
'.Trash', '.cache', '.npm', '.local', '.vscode', 'Library',
|
|
123
|
+
'.git', 'node_modules', '.Trash-*', '.DS_Store', '.config',
|
|
124
|
+
'.cocoapods', '.gem', '.rvm', '.nvm', '.asdf', '.brew',
|
|
125
|
+
'AppData', 'Application Data', '.cargo', '.rustup',
|
|
126
|
+
'.nuget', '.android', '.gradle', '.m2', '.vscode-server'
|
|
127
|
+
])
|
|
128
|
+
|
|
129
|
+
function shouldExclude(name, cwd) {
|
|
130
|
+
if (excludeDirs.has(name)) return true
|
|
131
|
+
for (const pattern of excludeDirs) {
|
|
132
|
+
if (pattern.includes('*')) {
|
|
133
|
+
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$')
|
|
134
|
+
if (regex.test(name)) return true
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const cwdName = cwd.split(sep).pop() || cwd.split('/').pop() || ''
|
|
138
|
+
if (name.startsWith('.') && name !== cwdName) return true
|
|
139
|
+
return false
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function scanDirectory(baseDir, seen, maxDepth = 2, currentDepth = 0) {
|
|
143
|
+
const cwd = process.cwd()
|
|
144
|
+
const projects = []
|
|
145
|
+
try {
|
|
146
|
+
const entries = readdirSync(baseDir, { withFileTypes: true })
|
|
147
|
+
for (const entry of entries) {
|
|
148
|
+
if (!entry.isDirectory()) continue
|
|
149
|
+
if (shouldExclude(entry.name, cwd)) continue
|
|
150
|
+
const dirPath = join(baseDir, entry.name)
|
|
151
|
+
let realPath
|
|
152
|
+
try { realPath = realpathSync(dirPath) } catch { realPath = dirPath }
|
|
153
|
+
const normalizedPath = realPath.toLowerCase()
|
|
154
|
+
if (seen.has(normalizedPath)) continue
|
|
155
|
+
seen.add(normalizedPath)
|
|
156
|
+
if (existsSync(join(dirPath, '.sillyspec'))) {
|
|
157
|
+
projects.push({ name: entry.name, path: dirPath })
|
|
158
|
+
}
|
|
159
|
+
if (currentDepth < maxDepth) {
|
|
160
|
+
projects.push(...scanDirectory(dirPath, seen, maxDepth, currentDepth + 1))
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} catch (err) {}
|
|
164
|
+
return projects
|
|
165
|
+
}
|
|
166
|
+
|
|
31
167
|
/**
|
|
32
168
|
* Discover all SillySpec projects
|
|
33
169
|
* @returns {Promise<Array>} Array of project objects
|
|
34
170
|
*/
|
|
35
171
|
async function discoverProjects() {
|
|
36
|
-
const { homedir } = await import('os')
|
|
37
172
|
const home = homedir()
|
|
38
173
|
const cwd = process.cwd()
|
|
39
174
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
])
|
|
46
|
-
|
|
47
|
-
// Helper to check if directory should be excluded
|
|
48
|
-
const shouldExclude = (name) => {
|
|
49
|
-
// Check exact matches
|
|
50
|
-
if (excludeDirs.has(name)) return true
|
|
51
|
-
// Check wildcard patterns (like .Trash-*)
|
|
52
|
-
for (const pattern of excludeDirs) {
|
|
53
|
-
if (pattern.includes('*')) {
|
|
54
|
-
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$')
|
|
55
|
-
if (regex.test(name)) return true
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
// Exclude hidden directories (starting with .) unless it's the cwd basename
|
|
59
|
-
if (name.startsWith('.') && name !== cwd.split('/').pop()) {
|
|
60
|
-
return true
|
|
61
|
-
}
|
|
62
|
-
return false
|
|
63
|
-
}
|
|
175
|
+
const scanDirs = new Set()
|
|
176
|
+
scanDirs.add(cwd)
|
|
177
|
+
scanDirs.add(dirname(cwd))
|
|
178
|
+
scanDirs.add(dirname(dirname(cwd)))
|
|
179
|
+
scanDirs.add(home)
|
|
64
180
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
181
|
+
const extraDirs = [
|
|
182
|
+
'Desktop', '桌面', 'Documents', '文档', 'Downloads', '下载',
|
|
183
|
+
'Projects', '项目', 'Work', '工作', 'Repos', 'Code', 'src', 'dev',
|
|
184
|
+
'workspace', '工作区'
|
|
185
|
+
]
|
|
68
186
|
|
|
69
187
|
for (const extra of extraDirs) {
|
|
70
188
|
const extraPath = join(home, extra)
|
|
71
|
-
if (existsSync(extraPath))
|
|
72
|
-
scanDirs.push(extraPath)
|
|
73
|
-
}
|
|
189
|
+
if (existsSync(extraPath)) scanDirs.add(extraPath)
|
|
74
190
|
}
|
|
75
191
|
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
for (const baseDir of scanDirs) {
|
|
80
|
-
try {
|
|
81
|
-
const { readdirSync } = await import('fs')
|
|
82
|
-
const entries = readdirSync(baseDir, { withFileTypes: true })
|
|
83
|
-
|
|
84
|
-
for (const entry of entries) {
|
|
85
|
-
if (!entry.isDirectory()) continue
|
|
86
|
-
if (shouldExclude(entry.name)) continue
|
|
87
|
-
|
|
88
|
-
const dirPath = join(baseDir, entry.name)
|
|
192
|
+
for (const customPath of customScanPaths) {
|
|
193
|
+
if (existsSync(customPath)) scanDirs.add(customPath)
|
|
194
|
+
}
|
|
89
195
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (seen.has(realPath)) continue
|
|
93
|
-
seen.add(realPath)
|
|
196
|
+
const seen = new Set()
|
|
197
|
+
const projects = []
|
|
94
198
|
|
|
95
|
-
|
|
199
|
+
// Normalize cwd for dedup
|
|
200
|
+
let cwdReal
|
|
201
|
+
try { cwdReal = realpathSync(cwd) } catch { cwdReal = cwd }
|
|
202
|
+
seen.add(cwdReal.toLowerCase())
|
|
203
|
+
if (existsSync(join(cwd, '.sillyspec'))) {
|
|
204
|
+
projects.push({ name: basename(cwd), path: cwd })
|
|
205
|
+
}
|
|
96
206
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
name: entry.name,
|
|
100
|
-
path: dirPath
|
|
101
|
-
})
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
} catch (err) {
|
|
105
|
-
// Skip directories we can't read
|
|
106
|
-
continue
|
|
107
|
-
}
|
|
207
|
+
for (const baseDir of scanDirs) {
|
|
208
|
+
projects.push(...scanDirectory(baseDir, seen, 2, 0))
|
|
108
209
|
}
|
|
109
210
|
|
|
110
211
|
return projects
|
|
@@ -126,7 +227,14 @@ function handleCliExecute(ws, data) {
|
|
|
126
227
|
return
|
|
127
228
|
}
|
|
128
229
|
|
|
129
|
-
|
|
230
|
+
if (!isAllowedCliCommand(command)) {
|
|
231
|
+
ws.send(JSON.stringify({
|
|
232
|
+
type: 'cli:error',
|
|
233
|
+
data: { message: `Command not allowed: ${command}` }
|
|
234
|
+
}))
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
|
|
130
238
|
discoverProjects().then(projects => {
|
|
131
239
|
const project = projects.find(p => p.name === projectName)
|
|
132
240
|
if (!project) {
|
|
@@ -137,45 +245,29 @@ function handleCliExecute(ws, data) {
|
|
|
137
245
|
return
|
|
138
246
|
}
|
|
139
247
|
|
|
140
|
-
// Kill existing process for this project if any
|
|
141
248
|
const existingKill = activeProcesses.get(projectName)
|
|
142
|
-
if (existingKill)
|
|
143
|
-
existingKill()
|
|
144
|
-
}
|
|
249
|
+
if (existingKill) existingKill()
|
|
145
250
|
|
|
146
|
-
// Execute command
|
|
147
251
|
const kill = executeCommand(
|
|
148
252
|
project.path,
|
|
149
253
|
command,
|
|
150
254
|
(output) => {
|
|
151
255
|
broadcast({
|
|
152
256
|
type: 'cli:output',
|
|
153
|
-
data: {
|
|
154
|
-
projectName,
|
|
155
|
-
output: output.data,
|
|
156
|
-
outputType: output.type
|
|
157
|
-
}
|
|
257
|
+
data: { projectName, output: output.data, outputType: output.type }
|
|
158
258
|
})
|
|
159
259
|
},
|
|
160
260
|
(result) => {
|
|
161
261
|
activeProcesses.delete(projectName)
|
|
162
262
|
broadcast({
|
|
163
263
|
type: 'cli:complete',
|
|
164
|
-
data: {
|
|
165
|
-
projectName,
|
|
166
|
-
exitCode: result.code,
|
|
167
|
-
signal: result.signal
|
|
168
|
-
}
|
|
264
|
+
data: { projectName, exitCode: result.code, signal: result.signal }
|
|
169
265
|
})
|
|
170
266
|
}
|
|
171
267
|
)
|
|
172
268
|
|
|
173
269
|
activeProcesses.set(projectName, kill)
|
|
174
|
-
|
|
175
|
-
broadcast({
|
|
176
|
-
type: 'cli:started',
|
|
177
|
-
data: { projectName, command }
|
|
178
|
-
})
|
|
270
|
+
broadcast({ type: 'cli:started', data: { projectName, command } })
|
|
179
271
|
})
|
|
180
272
|
}
|
|
181
273
|
|
|
@@ -186,8 +278,14 @@ function handleCliExecute(ws, data) {
|
|
|
186
278
|
*/
|
|
187
279
|
function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
188
280
|
const server = createServer((req, res) => {
|
|
189
|
-
|
|
190
|
-
|
|
281
|
+
const origin = req.headers.origin
|
|
282
|
+
if (!isLocalOrigin(origin)) {
|
|
283
|
+
res.writeHead(403)
|
|
284
|
+
res.end('Forbidden')
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
res.setHeader('Access-Control-Allow-Origin', origin || `http://127.0.0.1:${port}`)
|
|
288
|
+
res.setHeader('Vary', 'Origin')
|
|
191
289
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
|
192
290
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
|
|
193
291
|
|
|
@@ -197,7 +295,6 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
|
197
295
|
return
|
|
198
296
|
}
|
|
199
297
|
|
|
200
|
-
// API: List all projects
|
|
201
298
|
if (req.url === '/api/projects') {
|
|
202
299
|
discoverProjects().then(projects => {
|
|
203
300
|
res.setHeader('Content-Type', 'application/json')
|
|
@@ -210,7 +307,44 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
|
210
307
|
return
|
|
211
308
|
}
|
|
212
309
|
|
|
213
|
-
// API
|
|
310
|
+
// Detail API
|
|
311
|
+
const detailMatch = req.url?.match(/^\/api\/projects\/(.+)\/detail(\?|$)/)
|
|
312
|
+
if (detailMatch) {
|
|
313
|
+
const projectPath = decodeURIComponent(detailMatch[1])
|
|
314
|
+
const url = new URL(req.url, `http://${req.headers.host}`)
|
|
315
|
+
const type = url.searchParams.get('type')
|
|
316
|
+
let data
|
|
317
|
+
try {
|
|
318
|
+
if (type === 'git') data = parseGitDetail(projectPath)
|
|
319
|
+
else if (type === 'tech') data = parseTechStackDetail(projectPath)
|
|
320
|
+
else if (type === 'docs') data = parseSillyspecDocsTree(projectPath).groups
|
|
321
|
+
else { res.writeHead(400); res.end(JSON.stringify({ error: 'Invalid type' })); return }
|
|
322
|
+
res.setHeader('Content-Type', 'application/json')
|
|
323
|
+
res.writeHead(200)
|
|
324
|
+
res.end(JSON.stringify(data))
|
|
325
|
+
} catch (err) {
|
|
326
|
+
res.writeHead(500)
|
|
327
|
+
res.end(JSON.stringify({ error: err.message }))
|
|
328
|
+
}
|
|
329
|
+
return
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Overview API
|
|
333
|
+
if (req.url?.startsWith('/api/projects/') && req.url.endsWith('/overview')) {
|
|
334
|
+
const parts = req.url.replace('/api/projects/', '').replace('/overview', '').split('/')
|
|
335
|
+
const projectPath = decodeURIComponent(parts.join('/'))
|
|
336
|
+
try {
|
|
337
|
+
const overview = parseProjectOverview(projectPath)
|
|
338
|
+
res.setHeader('Content-Type', 'application/json')
|
|
339
|
+
res.writeHead(200)
|
|
340
|
+
res.end(JSON.stringify(overview))
|
|
341
|
+
} catch (err) {
|
|
342
|
+
res.writeHead(500)
|
|
343
|
+
res.end(JSON.stringify({ error: err.message }))
|
|
344
|
+
}
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
|
|
214
348
|
if (req.url?.startsWith('/api/project/')) {
|
|
215
349
|
const projectName = decodeURIComponent(req.url.split('/').pop())
|
|
216
350
|
discoverProjects().then(projects => {
|
|
@@ -219,10 +353,7 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
|
219
353
|
const state = parseProjectState(project.path)
|
|
220
354
|
res.setHeader('Content-Type', 'application/json')
|
|
221
355
|
res.writeHead(200)
|
|
222
|
-
res.end(JSON.stringify({
|
|
223
|
-
...project,
|
|
224
|
-
state
|
|
225
|
-
}))
|
|
356
|
+
res.end(JSON.stringify({ ...project, state }))
|
|
226
357
|
} else {
|
|
227
358
|
res.writeHead(404)
|
|
228
359
|
res.end(JSON.stringify({ error: 'Project not found' }))
|
|
@@ -234,12 +365,67 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
|
234
365
|
return
|
|
235
366
|
}
|
|
236
367
|
|
|
368
|
+
// Docs API
|
|
369
|
+
if (req.url?.startsWith('/api/docs/content')) {
|
|
370
|
+
const url = new URL(req.url, `http://${req.headers.host}`)
|
|
371
|
+
const filePath = url.searchParams.get('path')
|
|
372
|
+
if (!filePath) {
|
|
373
|
+
res.writeHead(400)
|
|
374
|
+
res.end(JSON.stringify({ error: 'Missing path parameter' }))
|
|
375
|
+
return
|
|
376
|
+
}
|
|
377
|
+
// Security: only allow reading viewable text documents under a .sillyspec tree.
|
|
378
|
+
const normalizedPath = resolve(filePath)
|
|
379
|
+
if (!isSillyspecPath(normalizedPath) || !isViewableDocPath(normalizedPath)) {
|
|
380
|
+
res.writeHead(403)
|
|
381
|
+
res.end(JSON.stringify({ error: 'Access denied' }))
|
|
382
|
+
return
|
|
383
|
+
}
|
|
384
|
+
if (existsSync(normalizedPath)) {
|
|
385
|
+
try {
|
|
386
|
+
const content = readFileSync(normalizedPath, 'utf-8')
|
|
387
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8')
|
|
388
|
+
res.writeHead(200)
|
|
389
|
+
res.end(content)
|
|
390
|
+
} catch (err) {
|
|
391
|
+
res.writeHead(500)
|
|
392
|
+
res.end(JSON.stringify({ error: err.message }))
|
|
393
|
+
}
|
|
394
|
+
} else {
|
|
395
|
+
res.writeHead(404)
|
|
396
|
+
res.end(JSON.stringify({ error: 'File not found' }))
|
|
397
|
+
}
|
|
398
|
+
return
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (req.url?.startsWith('/api/docs')) {
|
|
402
|
+
const url = new URL(req.url, `http://${req.headers.host}`)
|
|
403
|
+
const projectPath = url.searchParams.get('project')
|
|
404
|
+
if (!projectPath) {
|
|
405
|
+
res.writeHead(400)
|
|
406
|
+
res.end(JSON.stringify({ error: 'Missing project parameter' }))
|
|
407
|
+
return
|
|
408
|
+
}
|
|
409
|
+
const docs = parseSillyspecDocsTree(projectPath)
|
|
410
|
+
res.setHeader('Content-Type', 'application/json')
|
|
411
|
+
res.writeHead(200)
|
|
412
|
+
res.end(JSON.stringify(docs))
|
|
413
|
+
return
|
|
414
|
+
}
|
|
415
|
+
|
|
237
416
|
// Serve static files (dist/)
|
|
238
417
|
const distDir = join(__dirname, '../dist')
|
|
239
418
|
const indexPath = join(distDir, 'index.html')
|
|
240
419
|
if (existsSync(distDir)) {
|
|
241
|
-
const
|
|
242
|
-
|
|
420
|
+
const url = new URL(req.url || '/', `http://${req.headers.host}`)
|
|
421
|
+
const requestPath = decodeURIComponent(url.pathname)
|
|
422
|
+
const filePath = resolve(distDir, requestPath === '/' ? 'index.html' : `.${requestPath}`)
|
|
423
|
+
if (!isInside(distDir, filePath)) {
|
|
424
|
+
res.writeHead(403)
|
|
425
|
+
res.end('Access denied')
|
|
426
|
+
return
|
|
427
|
+
}
|
|
428
|
+
if (existsSync(filePath)) {
|
|
243
429
|
const ext = filePath.split('.').pop()
|
|
244
430
|
const mimeTypes = { html: 'text/html', js: 'application/javascript', css: 'text/css', svg: 'image/svg+xml', png: 'image/png', jpg: 'image/jpeg' }
|
|
245
431
|
res.setHeader('Content-Type', mimeTypes[ext] || 'application/octet-stream')
|
|
@@ -247,7 +433,6 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
|
247
433
|
res.end(readFileSync(filePath))
|
|
248
434
|
return
|
|
249
435
|
}
|
|
250
|
-
// SPA fallback: serve index.html for unknown routes
|
|
251
436
|
if (existsSync(indexPath)) {
|
|
252
437
|
res.setHeader('Content-Type', 'text/html')
|
|
253
438
|
res.writeHead(200)
|
|
@@ -256,32 +441,46 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
|
256
441
|
}
|
|
257
442
|
}
|
|
258
443
|
|
|
259
|
-
// 404
|
|
260
444
|
res.writeHead(404)
|
|
261
445
|
res.end('Not found')
|
|
262
446
|
})
|
|
263
447
|
|
|
264
|
-
// WebSocket Server
|
|
265
448
|
wss = new WebSocketServer({ server })
|
|
266
449
|
|
|
267
450
|
wss.on('error', (err) => {
|
|
268
451
|
console.error('WebSocket server error:', err)
|
|
269
452
|
})
|
|
270
453
|
|
|
271
|
-
wss.on('connection', (ws) => {
|
|
454
|
+
wss.on('connection', (ws, req) => {
|
|
455
|
+
if (!isLocalOrigin(req.headers.origin)) {
|
|
456
|
+
ws.close(1008, 'Forbidden origin')
|
|
457
|
+
return
|
|
458
|
+
}
|
|
272
459
|
console.log('WebSocket client connected')
|
|
273
460
|
|
|
274
461
|
// Send initial projects list
|
|
275
462
|
discoverProjects().then(projects => {
|
|
276
463
|
const projectsWithState = projects.map(p => ({
|
|
277
464
|
...p,
|
|
278
|
-
state: parseProjectState(p.path)
|
|
465
|
+
state: parseProjectState(p.path),
|
|
466
|
+
overview: parseProjectOverview(p.path)
|
|
279
467
|
}))
|
|
280
468
|
|
|
281
469
|
ws.send(JSON.stringify({
|
|
282
470
|
type: 'projects:init',
|
|
283
471
|
data: projectsWithState
|
|
284
472
|
}))
|
|
473
|
+
|
|
474
|
+
// Start watching progress files for all discovered projects
|
|
475
|
+
for (const p of projectsWithState) {
|
|
476
|
+
startProgressWatch(p.path)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Send current scan paths
|
|
480
|
+
ws.send(JSON.stringify({
|
|
481
|
+
type: 'scan:paths',
|
|
482
|
+
data: getCustomScanPaths()
|
|
483
|
+
}))
|
|
285
484
|
}).catch(err => {
|
|
286
485
|
console.error('Error sending initial projects:', err)
|
|
287
486
|
})
|
|
@@ -299,12 +498,41 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
|
299
498
|
if (kill) {
|
|
300
499
|
kill()
|
|
301
500
|
activeProcesses.delete(data.data.projectName)
|
|
302
|
-
broadcast({
|
|
303
|
-
|
|
304
|
-
|
|
501
|
+
broadcast({ type: 'cli:killed', data: { projectName: data.data.projectName } })
|
|
502
|
+
}
|
|
503
|
+
break
|
|
504
|
+
case 'scan:add-path':
|
|
505
|
+
if (data.data?.path) {
|
|
506
|
+
addCustomScanPath(data.data.path)
|
|
507
|
+
broadcast({ type: 'scan:paths', data: getCustomScanPaths() })
|
|
508
|
+
// Resend projects after scan
|
|
509
|
+
discoverProjects().then(projects => {
|
|
510
|
+
broadcast({
|
|
511
|
+
type: 'projects:updated',
|
|
512
|
+
data: projects.map(p => ({
|
|
513
|
+
...p,
|
|
514
|
+
state: parseProjectState(p.path),
|
|
515
|
+
overview: parseProjectOverview(p.path)
|
|
516
|
+
}))
|
|
517
|
+
})
|
|
305
518
|
})
|
|
306
519
|
}
|
|
307
520
|
break
|
|
521
|
+
case 'scan:remove-path':
|
|
522
|
+
if (data.data?.path) {
|
|
523
|
+
removeCustomScanPath(data.data.path)
|
|
524
|
+
ws.send(JSON.stringify({ type: 'scan:paths', data: getCustomScanPaths() }))
|
|
525
|
+
}
|
|
526
|
+
break
|
|
527
|
+
case 'scan:get-paths':
|
|
528
|
+
ws.send(JSON.stringify({ type: 'scan:paths', data: getCustomScanPaths() }))
|
|
529
|
+
break
|
|
530
|
+
case 'docs:get':
|
|
531
|
+
if (data.data?.projectPath) {
|
|
532
|
+
const docs = parseSillyspecDocsTree(data.data.projectPath)
|
|
533
|
+
ws.send(JSON.stringify({ type: 'docs:tree', data: docs }))
|
|
534
|
+
}
|
|
535
|
+
break
|
|
308
536
|
default:
|
|
309
537
|
console.log('Unknown message type:', data.type)
|
|
310
538
|
}
|
|
@@ -315,6 +543,12 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
|
315
543
|
|
|
316
544
|
ws.on('close', () => {
|
|
317
545
|
console.log('WebSocket client disconnected')
|
|
546
|
+
// Decrement progress watchers — we track which projects this client was watching
|
|
547
|
+
// via the initial projects:init. For simplicity, stop all that we started.
|
|
548
|
+
// (Other clients will re-start via their connection if still active)
|
|
549
|
+
for (const [path] of progressWatchers) {
|
|
550
|
+
stopProgressWatch(path)
|
|
551
|
+
}
|
|
318
552
|
})
|
|
319
553
|
|
|
320
554
|
ws.on('error', (err) => {
|
|
@@ -322,27 +556,21 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
|
322
556
|
})
|
|
323
557
|
})
|
|
324
558
|
|
|
325
|
-
// Start file watcher (wrapped to avoid crashing server)
|
|
326
559
|
try {
|
|
327
560
|
startWatcher((projects) => {
|
|
328
|
-
broadcast({
|
|
329
|
-
type: 'projects:updated',
|
|
330
|
-
data: projects
|
|
331
|
-
})
|
|
561
|
+
broadcast({ type: 'projects:updated', data: projects })
|
|
332
562
|
})
|
|
333
563
|
} catch (err) {
|
|
334
564
|
console.error('Failed to start file watcher:', err)
|
|
335
565
|
}
|
|
336
566
|
|
|
337
|
-
server.listen(port, () => {
|
|
338
|
-
console.log(`Dashboard server running on http://
|
|
339
|
-
|
|
567
|
+
server.listen(port, '127.0.0.1', () => {
|
|
568
|
+
console.log(`Dashboard server running on http://127.0.0.1:${port}`)
|
|
340
569
|
if (openBrowser) {
|
|
341
|
-
open(`http://
|
|
570
|
+
open(`http://127.0.0.1:${port}`)
|
|
342
571
|
}
|
|
343
572
|
})
|
|
344
573
|
|
|
345
|
-
// Handle shutdown
|
|
346
574
|
const shutdown = () => {
|
|
347
575
|
stopWatcher()
|
|
348
576
|
activeProcesses.forEach(kill => kill())
|