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