sillyspec 3.7.9 → 3.7.11
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/package.json +5 -2
- package/packages/dashboard/dist/assets/index-DHbVrCnq.css +1 -0
- package/packages/dashboard/dist/assets/index-qx7yiwun.js +17 -0
- package/packages/dashboard/dist/index.html +2 -2
- package/packages/dashboard/server/index.js +111 -104
- package/packages/dashboard/server/watcher.js +203 -131
- package/packages/dashboard/src/App.vue +14 -3
- package/packages/dashboard/src/components/ActionBar.vue +6 -6
- package/packages/dashboard/src/components/CommandPalette.vue +5 -5
- package/packages/dashboard/src/components/DetailPanel.vue +10 -10
- package/packages/dashboard/src/components/LogStream.vue +5 -5
- package/packages/dashboard/src/components/PipelineView.vue +18 -10
- package/packages/dashboard/src/components/ProjectList.vue +94 -11
- package/packages/dashboard/src/composables/useDashboard.js +6 -4
- package/packages/dashboard/src/style.css +11 -11
- package/packages/dashboard/dist/assets/index-Bh-GPjKY.css +0 -1
- package/packages/dashboard/dist/assets/index-CrCn5Gg6.js +0 -17
|
@@ -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 } from 'fs'
|
|
4
|
+
import { join, dirname, basename, sep } from 'path'
|
|
5
5
|
import { fileURLToPath } from 'url'
|
|
6
|
+
import { homedir } from 'os'
|
|
6
7
|
import open from 'open'
|
|
7
8
|
import { parseProjectState } from './parser.js'
|
|
8
|
-
import { startWatcher, stopWatcher } from './watcher.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))
|
|
@@ -28,83 +29,96 @@ function broadcast(data) {
|
|
|
28
29
|
})
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
// --- Shared scan logic (aligned with watcher.js) ---
|
|
33
|
+
|
|
34
|
+
const excludeDirs = new Set([
|
|
35
|
+
'.Trash', '.cache', '.npm', '.local', '.vscode', 'Library',
|
|
36
|
+
'.git', 'node_modules', '.Trash-*', '.DS_Store', '.config',
|
|
37
|
+
'.cocoapods', '.gem', '.rvm', '.nvm', '.asdf', '.brew',
|
|
38
|
+
'AppData', 'Application Data', '.cargo', '.rustup',
|
|
39
|
+
'.nuget', '.android', '.gradle', '.m2', '.vscode-server'
|
|
40
|
+
])
|
|
41
|
+
|
|
42
|
+
function shouldExclude(name, cwd) {
|
|
43
|
+
if (excludeDirs.has(name)) return true
|
|
44
|
+
for (const pattern of excludeDirs) {
|
|
45
|
+
if (pattern.includes('*')) {
|
|
46
|
+
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$')
|
|
47
|
+
if (regex.test(name)) return true
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const cwdName = cwd.split(sep).pop() || cwd.split('/').pop() || ''
|
|
51
|
+
if (name.startsWith('.') && name !== cwdName) return true
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function scanDirectory(baseDir, seen, maxDepth = 2, currentDepth = 0) {
|
|
56
|
+
const cwd = process.cwd()
|
|
57
|
+
const projects = []
|
|
58
|
+
try {
|
|
59
|
+
const entries = readdirSync(baseDir, { withFileTypes: true })
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
if (!entry.isDirectory()) continue
|
|
62
|
+
if (shouldExclude(entry.name, cwd)) continue
|
|
63
|
+
const dirPath = join(baseDir, entry.name)
|
|
64
|
+
let realPath
|
|
65
|
+
try { realPath = realpathSync(dirPath) } catch { realPath = dirPath }
|
|
66
|
+
const normalizedPath = realPath.toLowerCase()
|
|
67
|
+
if (seen.has(normalizedPath)) continue
|
|
68
|
+
seen.add(normalizedPath)
|
|
69
|
+
if (existsSync(join(dirPath, '.sillyspec'))) {
|
|
70
|
+
projects.push({ name: entry.name, path: dirPath })
|
|
71
|
+
}
|
|
72
|
+
if (currentDepth < maxDepth) {
|
|
73
|
+
projects.push(...scanDirectory(dirPath, seen, maxDepth, currentDepth + 1))
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} catch (err) {}
|
|
77
|
+
return projects
|
|
78
|
+
}
|
|
79
|
+
|
|
31
80
|
/**
|
|
32
81
|
* Discover all SillySpec projects
|
|
33
82
|
* @returns {Promise<Array>} Array of project objects
|
|
34
83
|
*/
|
|
35
84
|
async function discoverProjects() {
|
|
36
|
-
const { homedir } = await import('os')
|
|
37
85
|
const home = homedir()
|
|
38
86
|
const cwd = process.cwd()
|
|
39
87
|
|
|
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
|
-
}
|
|
88
|
+
const scanDirs = new Set()
|
|
89
|
+
scanDirs.add(cwd)
|
|
90
|
+
scanDirs.add(dirname(cwd))
|
|
91
|
+
scanDirs.add(dirname(dirname(cwd)))
|
|
92
|
+
scanDirs.add(home)
|
|
64
93
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
94
|
+
const extraDirs = [
|
|
95
|
+
'Desktop', '桌面', 'Documents', '文档', 'Downloads', '下载',
|
|
96
|
+
'Projects', '项目', 'Work', '工作', 'Repos', 'Code', 'src', 'dev',
|
|
97
|
+
'workspace', '工作区'
|
|
98
|
+
]
|
|
68
99
|
|
|
69
100
|
for (const extra of extraDirs) {
|
|
70
101
|
const extraPath = join(home, extra)
|
|
71
|
-
if (existsSync(extraPath))
|
|
72
|
-
scanDirs.push(extraPath)
|
|
73
|
-
}
|
|
102
|
+
if (existsSync(extraPath)) scanDirs.add(extraPath)
|
|
74
103
|
}
|
|
75
104
|
|
|
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)
|
|
105
|
+
for (const customPath of customScanPaths) {
|
|
106
|
+
if (existsSync(customPath)) scanDirs.add(customPath)
|
|
107
|
+
}
|
|
89
108
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (seen.has(realPath)) continue
|
|
93
|
-
seen.add(realPath)
|
|
109
|
+
const seen = new Set()
|
|
110
|
+
const projects = []
|
|
94
111
|
|
|
95
|
-
|
|
112
|
+
// Normalize cwd for dedup
|
|
113
|
+
let cwdReal
|
|
114
|
+
try { cwdReal = realpathSync(cwd) } catch { cwdReal = cwd }
|
|
115
|
+
seen.add(cwdReal.toLowerCase())
|
|
116
|
+
if (existsSync(join(cwd, '.sillyspec'))) {
|
|
117
|
+
projects.push({ name: basename(cwd), path: cwd })
|
|
118
|
+
}
|
|
96
119
|
|
|
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
|
-
}
|
|
120
|
+
for (const baseDir of scanDirs) {
|
|
121
|
+
projects.push(...scanDirectory(baseDir, seen, 2, 0))
|
|
108
122
|
}
|
|
109
123
|
|
|
110
124
|
return projects
|
|
@@ -126,7 +140,6 @@ function handleCliExecute(ws, data) {
|
|
|
126
140
|
return
|
|
127
141
|
}
|
|
128
142
|
|
|
129
|
-
// Find project
|
|
130
143
|
discoverProjects().then(projects => {
|
|
131
144
|
const project = projects.find(p => p.name === projectName)
|
|
132
145
|
if (!project) {
|
|
@@ -137,45 +150,29 @@ function handleCliExecute(ws, data) {
|
|
|
137
150
|
return
|
|
138
151
|
}
|
|
139
152
|
|
|
140
|
-
// Kill existing process for this project if any
|
|
141
153
|
const existingKill = activeProcesses.get(projectName)
|
|
142
|
-
if (existingKill)
|
|
143
|
-
existingKill()
|
|
144
|
-
}
|
|
154
|
+
if (existingKill) existingKill()
|
|
145
155
|
|
|
146
|
-
// Execute command
|
|
147
156
|
const kill = executeCommand(
|
|
148
157
|
project.path,
|
|
149
158
|
command,
|
|
150
159
|
(output) => {
|
|
151
160
|
broadcast({
|
|
152
161
|
type: 'cli:output',
|
|
153
|
-
data: {
|
|
154
|
-
projectName,
|
|
155
|
-
output: output.data,
|
|
156
|
-
outputType: output.type
|
|
157
|
-
}
|
|
162
|
+
data: { projectName, output: output.data, outputType: output.type }
|
|
158
163
|
})
|
|
159
164
|
},
|
|
160
165
|
(result) => {
|
|
161
166
|
activeProcesses.delete(projectName)
|
|
162
167
|
broadcast({
|
|
163
168
|
type: 'cli:complete',
|
|
164
|
-
data: {
|
|
165
|
-
projectName,
|
|
166
|
-
exitCode: result.code,
|
|
167
|
-
signal: result.signal
|
|
168
|
-
}
|
|
169
|
+
data: { projectName, exitCode: result.code, signal: result.signal }
|
|
169
170
|
})
|
|
170
171
|
}
|
|
171
172
|
)
|
|
172
173
|
|
|
173
174
|
activeProcesses.set(projectName, kill)
|
|
174
|
-
|
|
175
|
-
broadcast({
|
|
176
|
-
type: 'cli:started',
|
|
177
|
-
data: { projectName, command }
|
|
178
|
-
})
|
|
175
|
+
broadcast({ type: 'cli:started', data: { projectName, command } })
|
|
179
176
|
})
|
|
180
177
|
}
|
|
181
178
|
|
|
@@ -186,7 +183,6 @@ function handleCliExecute(ws, data) {
|
|
|
186
183
|
*/
|
|
187
184
|
function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
188
185
|
const server = createServer((req, res) => {
|
|
189
|
-
// CORS headers
|
|
190
186
|
res.setHeader('Access-Control-Allow-Origin', '*')
|
|
191
187
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
|
192
188
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
|
|
@@ -197,7 +193,6 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
|
197
193
|
return
|
|
198
194
|
}
|
|
199
195
|
|
|
200
|
-
// API: List all projects
|
|
201
196
|
if (req.url === '/api/projects') {
|
|
202
197
|
discoverProjects().then(projects => {
|
|
203
198
|
res.setHeader('Content-Type', 'application/json')
|
|
@@ -210,7 +205,6 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
|
210
205
|
return
|
|
211
206
|
}
|
|
212
207
|
|
|
213
|
-
// API: Get project details with state
|
|
214
208
|
if (req.url?.startsWith('/api/project/')) {
|
|
215
209
|
const projectName = decodeURIComponent(req.url.split('/').pop())
|
|
216
210
|
discoverProjects().then(projects => {
|
|
@@ -219,10 +213,7 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
|
219
213
|
const state = parseProjectState(project.path)
|
|
220
214
|
res.setHeader('Content-Type', 'application/json')
|
|
221
215
|
res.writeHead(200)
|
|
222
|
-
res.end(JSON.stringify({
|
|
223
|
-
...project,
|
|
224
|
-
state
|
|
225
|
-
}))
|
|
216
|
+
res.end(JSON.stringify({ ...project, state }))
|
|
226
217
|
} else {
|
|
227
218
|
res.writeHead(404)
|
|
228
219
|
res.end(JSON.stringify({ error: 'Project not found' }))
|
|
@@ -247,7 +238,6 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
|
247
238
|
res.end(readFileSync(filePath))
|
|
248
239
|
return
|
|
249
240
|
}
|
|
250
|
-
// SPA fallback: serve index.html for unknown routes
|
|
251
241
|
if (existsSync(indexPath)) {
|
|
252
242
|
res.setHeader('Content-Type', 'text/html')
|
|
253
243
|
res.writeHead(200)
|
|
@@ -256,12 +246,10 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
|
256
246
|
}
|
|
257
247
|
}
|
|
258
248
|
|
|
259
|
-
// 404
|
|
260
249
|
res.writeHead(404)
|
|
261
250
|
res.end('Not found')
|
|
262
251
|
})
|
|
263
252
|
|
|
264
|
-
// WebSocket Server
|
|
265
253
|
wss = new WebSocketServer({ server })
|
|
266
254
|
|
|
267
255
|
wss.on('error', (err) => {
|
|
@@ -282,6 +270,12 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
|
282
270
|
type: 'projects:init',
|
|
283
271
|
data: projectsWithState
|
|
284
272
|
}))
|
|
273
|
+
|
|
274
|
+
// Send current scan paths
|
|
275
|
+
ws.send(JSON.stringify({
|
|
276
|
+
type: 'scan:paths',
|
|
277
|
+
data: getCustomScanPaths()
|
|
278
|
+
}))
|
|
285
279
|
}).catch(err => {
|
|
286
280
|
console.error('Error sending initial projects:', err)
|
|
287
281
|
})
|
|
@@ -299,12 +293,31 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
|
299
293
|
if (kill) {
|
|
300
294
|
kill()
|
|
301
295
|
activeProcesses.delete(data.data.projectName)
|
|
302
|
-
broadcast({
|
|
303
|
-
|
|
304
|
-
|
|
296
|
+
broadcast({ type: 'cli:killed', data: { projectName: data.data.projectName } })
|
|
297
|
+
}
|
|
298
|
+
break
|
|
299
|
+
case 'scan:add-path':
|
|
300
|
+
if (data.data?.path) {
|
|
301
|
+
addCustomScanPath(data.data.path)
|
|
302
|
+
broadcast({ type: 'scan:paths', data: getCustomScanPaths() })
|
|
303
|
+
// Resend projects after scan
|
|
304
|
+
discoverProjects().then(projects => {
|
|
305
|
+
broadcast({
|
|
306
|
+
type: 'projects:updated',
|
|
307
|
+
data: projects.map(p => ({ ...p, state: parseProjectState(p.path) }))
|
|
308
|
+
})
|
|
305
309
|
})
|
|
306
310
|
}
|
|
307
311
|
break
|
|
312
|
+
case 'scan:remove-path':
|
|
313
|
+
if (data.data?.path) {
|
|
314
|
+
removeCustomScanPath(data.data.path)
|
|
315
|
+
ws.send(JSON.stringify({ type: 'scan:paths', data: getCustomScanPaths() }))
|
|
316
|
+
}
|
|
317
|
+
break
|
|
318
|
+
case 'scan:get-paths':
|
|
319
|
+
ws.send(JSON.stringify({ type: 'scan:paths', data: getCustomScanPaths() }))
|
|
320
|
+
break
|
|
308
321
|
default:
|
|
309
322
|
console.log('Unknown message type:', data.type)
|
|
310
323
|
}
|
|
@@ -322,13 +335,9 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
|
322
335
|
})
|
|
323
336
|
})
|
|
324
337
|
|
|
325
|
-
// Start file watcher (wrapped to avoid crashing server)
|
|
326
338
|
try {
|
|
327
339
|
startWatcher((projects) => {
|
|
328
|
-
broadcast({
|
|
329
|
-
type: 'projects:updated',
|
|
330
|
-
data: projects
|
|
331
|
-
})
|
|
340
|
+
broadcast({ type: 'projects:updated', data: projects })
|
|
332
341
|
})
|
|
333
342
|
} catch (err) {
|
|
334
343
|
console.error('Failed to start file watcher:', err)
|
|
@@ -336,13 +345,11 @@ function startServer({ port = 3456, open: openBrowser = true } = {}) {
|
|
|
336
345
|
|
|
337
346
|
server.listen(port, () => {
|
|
338
347
|
console.log(`Dashboard server running on http://localhost:${port}`)
|
|
339
|
-
|
|
340
348
|
if (openBrowser) {
|
|
341
349
|
open(`http://localhost:${port}`)
|
|
342
350
|
}
|
|
343
351
|
})
|
|
344
352
|
|
|
345
|
-
// Handle shutdown
|
|
346
353
|
const shutdown = () => {
|
|
347
354
|
stopWatcher()
|
|
348
355
|
activeProcesses.forEach(kill => kill())
|