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.
@@ -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
- // Directories to exclude (system junk, cache, etc.)
41
- const excludeDirs = new Set([
42
- '.Trash', '.cache', '.npm', '.local', '.vscode', 'Library',
43
- '.git', 'node_modules', '.Trash-*', '.DS_Store', '.config',
44
- '.cocoapods', '.gem', '.rvm', '.nvm', '.asdf', '.brew'
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
- // Build scan directories: cwd + home subdirs + common project locations
66
- const scanDirs = [cwd, home]
67
- const extraDirs = ['Desktop', 'Documents', 'Projects', 'Work', 'Repos', 'Code', 'src', 'dev']
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 projects = []
77
- const seen = new Set() // Dedupe by path
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
- // Skip if we've already seen this path (handles symlinks, dupes)
91
- const realPath = dirPath // Could add realpath for true deduping
92
- if (seen.has(realPath)) continue
93
- seen.add(realPath)
109
+ const seen = new Set()
110
+ const projects = []
94
111
 
95
- const sillyspecPath = join(dirPath, '.sillyspec')
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
- if (existsSync(sillyspecPath)) {
98
- projects.push({
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
- type: 'cli:killed',
304
- data: { projectName: data.data.projectName }
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())