@dfosco/storyboard 0.6.0-beta.2 → 0.6.0-beta.21
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/dist/storyboard-ui.js +3112 -3098
- package/dist/storyboard-ui.js.map +1 -1
- package/mascot/frame-01-peek-left.txt +4 -0
- package/mascot/frame-02-eyes-open.txt +4 -0
- package/mascot/frame-03-peek-right.txt +4 -0
- package/mascot/frame-04-eyes-open.txt +4 -0
- package/mascot/frame-05-eyes-closed.txt +4 -0
- package/mascot/frame-06-eyes-open.txt +4 -0
- package/mascot.config.json +13 -0
- package/package.json +5 -2
- package/scaffold/AGENTS.md +1 -0
- package/scaffold/gitignore +12 -2
- package/scaffold/skills/design-system-catalog/SKILL.md +98 -0
- package/scaffold/skills/design-system-catalog/extract-components.mjs +441 -0
- package/scaffold/skills/design-system-catalog/generate-catalog.sh +255 -0
- package/scaffold/skills/migrate/SKILL.md +72 -50
- package/scaffold/terminal-agent.agent.md +8 -1
- package/src/core/canvas/agent-session.js +103 -17
- package/src/core/canvas/agent-session.test.js +29 -1
- package/src/core/canvas/collision.js +54 -45
- package/src/core/canvas/collision.test.js +39 -0
- package/src/core/canvas/configReader.js +110 -0
- package/src/core/canvas/hot-pool.js +5 -3
- package/src/core/canvas/server.js +32 -13
- package/src/core/canvas/terminal-server.js +156 -91
- package/src/core/cli/agent.js +86 -33
- package/src/core/cli/dev.js +303 -17
- package/src/core/cli/server.js +1 -1
- package/src/core/cli/setup.js +203 -60
- package/src/core/cli/terminal-welcome.js +5 -6
- package/src/core/cli/userState.js +63 -0
- package/src/core/stores/configSchema.js +1 -0
- package/src/core/stores/themeStore.ts +24 -0
- package/src/core/tools/handlers/devtools.test.js +1 -1
- package/src/core/vite/server-plugin.js +107 -10
- package/src/internals/CommandPalette/CommandPalette.jsx +1 -1
- package/src/internals/Viewfinder.jsx +10 -2
- package/src/internals/canvas/CanvasPage.jsx +30 -9
- package/src/internals/canvas/WebGLContextPool.jsx +6 -7
- package/src/internals/canvas/componentIsolate.jsx +7 -8
- package/src/internals/canvas/componentSetIsolate.jsx +7 -8
- package/src/internals/canvas/widgets/PrototypeEmbed.jsx +3 -1
- package/src/internals/canvas/widgets/StorySetWidget.jsx +19 -7
- package/src/internals/canvas/widgets/StoryWidget.jsx +9 -3
- package/src/internals/canvas/widgets/TerminalWidget.jsx +74 -13
- package/src/internals/canvas/widgets/expandUtils.js +4 -2
- package/src/internals/hooks/usePrototypeReloadGuard.js +9 -5
- package/src/internals/vite/data-plugin.js +126 -3
- package/terminal.config.json +66 -0
package/src/core/cli/dev.js
CHANGED
|
@@ -15,15 +15,134 @@
|
|
|
15
15
|
|
|
16
16
|
import * as p from '@clack/prompts'
|
|
17
17
|
import { spawn } from 'node:child_process'
|
|
18
|
-
import { resolve } from 'node:path'
|
|
18
|
+
import { resolve, join } from 'node:path'
|
|
19
19
|
import { readFileSync, existsSync } from 'node:fs'
|
|
20
20
|
import { detectWorktreeName, getPort, releasePort } from '../worktree/port.js'
|
|
21
21
|
import { startRenameWatcher } from '../rename-watcher/watcher.js'
|
|
22
22
|
import { compactAll } from '../canvas/compact.js'
|
|
23
23
|
import { parseFlags } from './flags.js'
|
|
24
|
+
import { setupNeeded, writeUserState, getInstalledStoryboardVersion } from './userState.js'
|
|
25
|
+
import { dim, magenta, bold } from './intro.js'
|
|
26
|
+
import { rmSync } from 'node:fs'
|
|
27
|
+
|
|
28
|
+
/** Find the mascot directory shipped with the storyboard package. */
|
|
29
|
+
function mascotPaths(targetCwd) {
|
|
30
|
+
// Prefer a user override at project root, fall back to the library dir.
|
|
31
|
+
const userConfig = join(targetCwd, 'mascot.config.json')
|
|
32
|
+
const userDir = join(targetCwd, 'mascot')
|
|
33
|
+
if (existsSync(userConfig) && existsSync(userDir)) {
|
|
34
|
+
return { configPath: userConfig, framesDir: userDir }
|
|
35
|
+
}
|
|
36
|
+
// dev.js → src/core/cli/dev.js → package root is 3 dirs up.
|
|
37
|
+
const libRoot = resolve(import.meta.dirname, '..', '..', '..')
|
|
38
|
+
return {
|
|
39
|
+
configPath: join(libRoot, 'mascot.config.json'),
|
|
40
|
+
framesDir: join(libRoot, 'mascot'),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Apply magenta to the mascot's eye glyphs, dim to the dots/frame. */
|
|
45
|
+
function colorizeMascot(text) {
|
|
46
|
+
const eyes = /[●◠◡]/g
|
|
47
|
+
return text
|
|
48
|
+
.split('\n')
|
|
49
|
+
.map((line) => line.replace(eyes, (m) => magenta(m)).replace(/[·│╭╮╰╯─]/g, (m) => dim(m)))
|
|
50
|
+
.join('\n')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Render the mascot with an in-place loop animation, then settle on the
|
|
55
|
+
* configured final frame with the URL beside it.
|
|
56
|
+
*
|
|
57
|
+
* Called AFTER Vite prints "ready in Xms" and the storyboard-server plugin
|
|
58
|
+
* has suppressed Vite's own URL block. From this moment, Vite is in idle
|
|
59
|
+
* watch mode and won't print again unless code changes — so our cursor-up
|
|
60
|
+
* redraws can safely own the bottom of the screen.
|
|
61
|
+
*/
|
|
62
|
+
function renderMascot({ configPath, framesDir }, urlLine, stopLine) {
|
|
63
|
+
if (!existsSync(configPath)) return false
|
|
64
|
+
let config
|
|
65
|
+
try { config = JSON.parse(readFileSync(configPath, 'utf8')) } catch { return false }
|
|
66
|
+
if (config.enabled === false) return false
|
|
67
|
+
const rawEntries = Array.isArray(config.frames) ? config.frames : []
|
|
68
|
+
if (rawEntries.length === 0) return false
|
|
69
|
+
const defaultDuration = Number(config.frameDurationMs) || 180
|
|
70
|
+
const loops = Math.max(1, Number(config.loops) || 1)
|
|
71
|
+
|
|
72
|
+
// Each entry is either a string filename or a [filename, delayMs] tuple.
|
|
73
|
+
// Per-frame delay falls back to frameDurationMs when missing.
|
|
74
|
+
const entries = rawEntries.map((e) => {
|
|
75
|
+
if (Array.isArray(e)) return { name: String(e[0]), delay: Number(e[1]) || defaultDuration }
|
|
76
|
+
return { name: String(e), delay: defaultDuration }
|
|
77
|
+
})
|
|
78
|
+
const settleName = config.settleFrame || entries[entries.length - 1].name
|
|
79
|
+
|
|
80
|
+
let rawFrames
|
|
81
|
+
try { rawFrames = entries.map((e) => readFileSync(join(framesDir, e.name), 'utf8')) } catch { return false }
|
|
82
|
+
let settleRaw
|
|
83
|
+
try { settleRaw = readFileSync(join(framesDir, settleName), 'utf8') } catch { settleRaw = rawFrames[rawFrames.length - 1] }
|
|
84
|
+
|
|
85
|
+
// Pad every frame to the same height/width so cursor-up redraws fully
|
|
86
|
+
// overwrite the previous frame.
|
|
87
|
+
const trim = (s) => s.replace(/\n+$/, '')
|
|
88
|
+
const split = [...rawFrames, settleRaw].map((f) => trim(f).split('\n'))
|
|
89
|
+
const maxLines = Math.max(...split.map((l) => l.length))
|
|
90
|
+
const maxCols = Math.max(...split.flatMap((lines) => lines.map((l) => l.length)))
|
|
91
|
+
const normalized = split.map((lines) => {
|
|
92
|
+
while (lines.length < maxLines) lines.push('')
|
|
93
|
+
return lines.map((l) => l.padEnd(maxCols, ' ')).join('\n')
|
|
94
|
+
})
|
|
95
|
+
const loopFrames = normalized.slice(0, -1)
|
|
96
|
+
const settleFrame = normalized[normalized.length - 1]
|
|
97
|
+
const lineCount = maxLines
|
|
98
|
+
|
|
99
|
+
const composeSettle = () => {
|
|
100
|
+
const lines = colorizeMascot(settleFrame).split('\n')
|
|
101
|
+
if (lines[1] != null) lines[1] = lines[1] + ' ' + urlLine
|
|
102
|
+
if (lines[2] != null) lines[2] = lines[2] + ' ' + stopLine
|
|
103
|
+
return lines.join('\n')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!process.stdout.isTTY) {
|
|
107
|
+
process.stdout.write(composeSettle() + '\n')
|
|
108
|
+
return true
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Reserve vertical space with blank lines so cursor-up redraws have a
|
|
112
|
+
// stable region to overwrite.
|
|
113
|
+
process.stdout.write('\n'.repeat(lineCount))
|
|
114
|
+
const draw = (frame) => {
|
|
115
|
+
process.stdout.write(`\x1b[${lineCount}A`)
|
|
116
|
+
for (const line of frame.split('\n')) {
|
|
117
|
+
process.stdout.write('\x1b[2K' + line + '\n')
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return new Promise((resolveAnim) => {
|
|
122
|
+
let loopIdx = 0
|
|
123
|
+
let frameIdx = 0
|
|
124
|
+
// Recursive setTimeout so each frame can have its own delay.
|
|
125
|
+
const step = () => {
|
|
126
|
+
if (loopIdx >= loops) {
|
|
127
|
+
draw(composeSettle())
|
|
128
|
+
resolveAnim(true)
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
draw(colorizeMascot(loopFrames[frameIdx]))
|
|
132
|
+
const thisDelay = entries[frameIdx].delay
|
|
133
|
+
frameIdx++
|
|
134
|
+
if (frameIdx >= loopFrames.length) { frameIdx = 0; loopIdx++ }
|
|
135
|
+
const t = setTimeout(step, thisDelay)
|
|
136
|
+
if (typeof t.unref === 'function') t.unref()
|
|
137
|
+
}
|
|
138
|
+
step()
|
|
139
|
+
})
|
|
140
|
+
}
|
|
24
141
|
|
|
25
142
|
const flagSchema = {
|
|
26
143
|
port: { type: 'number', description: 'Override dev server port' },
|
|
144
|
+
'no-buddy': { type: 'boolean', default: false, description: 'Omit the storyboard mascot' },
|
|
145
|
+
verbose: { type: 'boolean', default: false, description: 'Show full setup/Vite output' },
|
|
27
146
|
}
|
|
28
147
|
|
|
29
148
|
/** Read the fixed port from storyboard.config.json, if any. */
|
|
@@ -39,6 +158,28 @@ function readConfiguredPort(cwd) {
|
|
|
39
158
|
}
|
|
40
159
|
}
|
|
41
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Resolve the Vite base path for URL display. Priority:
|
|
163
|
+
* 1. `VITE_BASE_PATH` env var (matches vite.config.js convention)
|
|
164
|
+
* 2. `basePath` key in storyboard.config.json (if user wants to override)
|
|
165
|
+
* 3. `/` (Vite default)
|
|
166
|
+
*
|
|
167
|
+
* Trailing slash is normalized so the rendered URL never double-slashes.
|
|
168
|
+
*/
|
|
169
|
+
function resolveBasePath(cwd) {
|
|
170
|
+
let base = process.env.VITE_BASE_PATH || null
|
|
171
|
+
if (!base) {
|
|
172
|
+
try {
|
|
173
|
+
const cfg = JSON.parse(readFileSync(resolve(cwd, 'storyboard.config.json'), 'utf8'))
|
|
174
|
+
if (typeof cfg.basePath === 'string' && cfg.basePath) base = cfg.basePath
|
|
175
|
+
} catch { /* empty */ }
|
|
176
|
+
}
|
|
177
|
+
base = base || '/'
|
|
178
|
+
if (!base.startsWith('/')) base = '/' + base
|
|
179
|
+
if (!base.endsWith('/')) base = base + '/'
|
|
180
|
+
return base
|
|
181
|
+
}
|
|
182
|
+
|
|
42
183
|
async function main() {
|
|
43
184
|
const { flags } = parseFlags(process.argv.slice(3), flagSchema)
|
|
44
185
|
const worktreeName = detectWorktreeName()
|
|
@@ -51,46 +192,191 @@ async function main() {
|
|
|
51
192
|
const configuredPort = readConfiguredPort(targetCwd)
|
|
52
193
|
const strictPort = flags.port == null && configuredPort != null
|
|
53
194
|
const port = flags.port || configuredPort || getPort(worktreeName)
|
|
195
|
+
const basePath = resolveBasePath(targetCwd)
|
|
196
|
+
const devUrl = `http://localhost:${port}${basePath}`
|
|
54
197
|
|
|
55
|
-
|
|
56
|
-
|
|
198
|
+
const verbose = flags.verbose
|
|
199
|
+
|
|
200
|
+
// Quiet header: just `worktree: …` and `port: …`. Everything else
|
|
201
|
+
// (setup logs, compaction, "strict from storyboard.config.json",
|
|
202
|
+
// intro/outro frames) is hidden unless --verbose.
|
|
203
|
+
if (verbose) {
|
|
204
|
+
p.intro('storyboard dev')
|
|
205
|
+
p.log.info(`worktree: ${worktreeName}`)
|
|
206
|
+
} else {
|
|
207
|
+
console.log(` ${dim('worktree:')} ${bold(worktreeName)}`)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Re-run setup automatically if it has never run here, or if the installed
|
|
211
|
+
// @dfosco/storyboard version no longer matches the one setup was last run
|
|
212
|
+
// against. This lets `npm install` upgrades trigger fresh scaffolding
|
|
213
|
+
// without requiring `npx storyboard update`.
|
|
214
|
+
{
|
|
215
|
+
const need = setupNeeded(targetCwd)
|
|
216
|
+
if (need) {
|
|
217
|
+
const why = need.reason === 'first-run'
|
|
218
|
+
? 'first run in this repo'
|
|
219
|
+
: `version changed ${need.from} → ${need.to}`
|
|
220
|
+
if (verbose) p.log.info(`Running setup (${why})…`)
|
|
221
|
+
|
|
222
|
+
// Invalidate Vite's optimize-deps cache when the storyboard version
|
|
223
|
+
// changes. Otherwise the browser hits 504 Outdated Optimize Dep
|
|
224
|
+
// because the dep graph IDs no longer match the cached chunks.
|
|
225
|
+
try { rmSync(join(targetCwd, 'node_modules', '.vite'), { recursive: true, force: true }) } catch { /* empty */ }
|
|
226
|
+
await new Promise((resolveSetup) => {
|
|
227
|
+
const setupChild = spawn(
|
|
228
|
+
process.platform === 'win32' ? 'npx.cmd' : 'npx',
|
|
229
|
+
['storyboard', 'setup', '--skip-branch', '--no-buddy'],
|
|
230
|
+
{
|
|
231
|
+
cwd: targetCwd,
|
|
232
|
+
// In quiet mode swallow the spawned-setup output; verbose passes through.
|
|
233
|
+
stdio: verbose ? 'inherit' : 'ignore',
|
|
234
|
+
env: { ...process.env, STORYBOARD_NO_BUDDY: '1' },
|
|
235
|
+
}
|
|
236
|
+
)
|
|
237
|
+
setupChild.on('exit', () => resolveSetup())
|
|
238
|
+
setupChild.on('error', () => resolveSetup())
|
|
239
|
+
})
|
|
240
|
+
const version = getInstalledStoryboardVersion(targetCwd)
|
|
241
|
+
if (version) writeUserState({ setupVersion: version, setupRanAt: new Date().toISOString() }, targetCwd)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
57
244
|
|
|
58
245
|
// Compact bloated canvas JSONL files before booting Vite.
|
|
59
246
|
const compacted = compactAll(targetCwd)
|
|
60
|
-
|
|
61
|
-
|
|
247
|
+
if (verbose) {
|
|
248
|
+
for (const r of compacted) {
|
|
249
|
+
p.log.info(`[compact] ${r.name}: ${(r.before / 1024).toFixed(0)}KB → ${(r.after / 1024).toFixed(0)}KB`)
|
|
250
|
+
}
|
|
62
251
|
}
|
|
63
252
|
|
|
64
253
|
const renameWatcher = startRenameWatcher(targetCwd)
|
|
65
254
|
const compactInterval = setInterval(() => {
|
|
66
255
|
try {
|
|
67
256
|
const r = compactAll(targetCwd)
|
|
68
|
-
|
|
257
|
+
if (verbose) {
|
|
258
|
+
for (const x of r) p.log.info(`[compact] ${x.name}: ${(x.before / 1024).toFixed(0)}KB → ${(x.after / 1024).toFixed(0)}KB`)
|
|
259
|
+
}
|
|
69
260
|
} catch { /* non-critical */ }
|
|
70
261
|
}, 15 * 60 * 1000)
|
|
71
262
|
|
|
72
263
|
const npmBin = process.platform === 'win32' ? 'npx.cmd' : 'npx'
|
|
73
|
-
// Without --strictPort: if the requested port is taken, Vite picks the next
|
|
74
|
-
// free one. The server-plugin captures the actual port via
|
|
75
|
-
// server.httpServer.address() and self-registers in .storyboard/servers.json.
|
|
76
|
-
// With --strictPort (config.port is set): Vite exits if the port is taken,
|
|
77
|
-
// honoring the user's intent that this instance owns that exact port.
|
|
78
264
|
const viteArgs = ['vite', '--port', String(port)]
|
|
79
265
|
if (strictPort) viteArgs.push('--strictPort')
|
|
80
|
-
if (
|
|
266
|
+
if (verbose) {
|
|
267
|
+
console.log(` ${dim('port:')} ${bold(port)} ${strictPort ? dim('(strict — from storyboard.config.json)') : ''}`)
|
|
268
|
+
} else {
|
|
269
|
+
console.log(` ${dim('port:')} ${bold(port)}`)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const showBuddy = !flags['no-buddy'] && process.env.STORYBOARD_NO_BUDDY !== '1'
|
|
273
|
+
|
|
274
|
+
// Spawn Vite with piped stdio so we can:
|
|
275
|
+
// - In quiet mode: suppress noisy plugin chatter ([storyboard]/[generouted]/etc)
|
|
276
|
+
// until Vite prints "ready in", then render the mascot + URL, then
|
|
277
|
+
// stream the rest through unchanged.
|
|
278
|
+
// - In verbose mode: stream everything through unchanged from the start.
|
|
81
279
|
const child = spawn(npmBin, viteArgs, {
|
|
82
280
|
cwd: targetCwd,
|
|
83
|
-
stdio: 'inherit',
|
|
84
|
-
env: {
|
|
281
|
+
stdio: verbose ? 'inherit' : ['inherit', 'pipe', 'pipe'],
|
|
282
|
+
env: {
|
|
283
|
+
...process.env,
|
|
284
|
+
STORYBOARD_WORKTREE: worktreeName,
|
|
285
|
+
// Tells the storyboard-server vite plugin to suppress its default
|
|
286
|
+
// "➜ Local:" URL block — we render our own URL beside the mascot.
|
|
287
|
+
...(verbose ? {} : { STORYBOARD_QUIET_VITE: '1' }),
|
|
288
|
+
},
|
|
85
289
|
})
|
|
86
290
|
|
|
87
|
-
|
|
88
|
-
|
|
291
|
+
if (!verbose) {
|
|
292
|
+
let mascotShown = false
|
|
293
|
+
let mascotDone = false
|
|
294
|
+
const queued = [] // [sink, line] pairs queued during animation
|
|
295
|
+
const flushQueue = () => {
|
|
296
|
+
while (queued.length) {
|
|
297
|
+
const [s, l] = queued.shift()
|
|
298
|
+
s.write(l + '\n')
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const renderOnce = async () => {
|
|
302
|
+
if (mascotShown) return
|
|
303
|
+
mascotShown = true
|
|
304
|
+
console.log()
|
|
305
|
+
const animated = showBuddy && renderMascot(
|
|
306
|
+
mascotPaths(targetCwd),
|
|
307
|
+
bold(devUrl),
|
|
308
|
+
dim('Stop with Ctrl+C'),
|
|
309
|
+
)
|
|
310
|
+
if (!animated) {
|
|
311
|
+
console.log(` ${bold(devUrl)}`)
|
|
312
|
+
console.log(` ${dim('Stop with Ctrl+C')}`)
|
|
313
|
+
}
|
|
314
|
+
// Wait for animation to settle, then flush anything Vite emitted
|
|
315
|
+
// during the animation window so it doesn't shift the cursor mid-frame.
|
|
316
|
+
const isPromise = animated && typeof animated.then === 'function'
|
|
317
|
+
if (isPromise) await animated
|
|
318
|
+
mascotDone = true
|
|
319
|
+
console.log()
|
|
320
|
+
flushQueue()
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const makeFilter = (sink) => {
|
|
324
|
+
let buf = ''
|
|
325
|
+
return (chunk) => {
|
|
326
|
+
buf += chunk.toString()
|
|
327
|
+
const lines = buf.split('\n')
|
|
328
|
+
buf = lines.pop()
|
|
329
|
+
for (const line of lines) {
|
|
330
|
+
if (mascotDone) {
|
|
331
|
+
sink.write(line + '\n')
|
|
332
|
+
continue
|
|
333
|
+
}
|
|
334
|
+
if (mascotShown) {
|
|
335
|
+
// Animation in flight — buffer subsequent Vite output so it
|
|
336
|
+
// can't shift our cursor mid-redraw.
|
|
337
|
+
queued.push([sink, line])
|
|
338
|
+
continue
|
|
339
|
+
}
|
|
340
|
+
// Pre-ready: only let the "ready in" line through, then start
|
|
341
|
+
// the mascot animation.
|
|
342
|
+
if (/ready in \d/.test(line)) {
|
|
343
|
+
sink.write(line + '\n')
|
|
344
|
+
renderOnce()
|
|
345
|
+
}
|
|
346
|
+
// else: swallow pre-ready chatter
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
child.stdout?.on('data', makeFilter(process.stdout))
|
|
351
|
+
child.stderr?.on('data', makeFilter(process.stderr))
|
|
352
|
+
// Safety net: if Vite never prints "ready in" within 8s, render anyway.
|
|
353
|
+
setTimeout(() => { renderOnce() }, 8000).unref?.()
|
|
354
|
+
}
|
|
89
355
|
|
|
356
|
+
let shuttingDown = false
|
|
90
357
|
function shutdown() {
|
|
358
|
+
if (shuttingDown) {
|
|
359
|
+
// Second Ctrl+C → hard exit, kill child with SIGKILL.
|
|
360
|
+
try { child.kill('SIGKILL') } catch { /* empty */ }
|
|
361
|
+
process.exit(130)
|
|
362
|
+
}
|
|
363
|
+
shuttingDown = true
|
|
91
364
|
clearInterval(compactInterval)
|
|
92
365
|
renameWatcher.close()
|
|
93
|
-
|
|
366
|
+
// Suppress Vite's shutdown-time esbuild noise ("Pre-transform error:
|
|
367
|
+
// The service was stopped" for every in-flight transform) AND the
|
|
368
|
+
// orphan-archive log spam from the storyboard-server plugin teardown.
|
|
369
|
+
try { child.stdout?.removeAllListeners('data') } catch { /* empty */ }
|
|
370
|
+
try { child.stderr?.removeAllListeners('data') } catch { /* empty */ }
|
|
371
|
+
try { child.stdout?.destroy() } catch { /* empty */ }
|
|
372
|
+
try { child.stderr?.destroy() } catch { /* empty */ }
|
|
373
|
+
// SIGINT first (clean esbuild shutdown), then SIGTERM after 2s if
|
|
374
|
+
// Vite is still alive (handles plugins that loop on session teardown),
|
|
375
|
+
// then SIGKILL after 5s as last resort.
|
|
376
|
+
try { child.kill('SIGINT') } catch { /* already dead */ }
|
|
377
|
+
const term = setTimeout(() => { try { child.kill('SIGTERM') } catch { /* empty */ } }, 2000)
|
|
378
|
+
const kill = setTimeout(() => { try { child.kill('SIGKILL') } catch { /* empty */ } }, 5000)
|
|
379
|
+
term.unref?.(); kill.unref?.()
|
|
94
380
|
releasePort(worktreeName)
|
|
95
381
|
}
|
|
96
382
|
process.on('SIGINT', shutdown)
|
package/src/core/cli/server.js
CHANGED
|
@@ -16,7 +16,7 @@ import * as p from '@clack/prompts'
|
|
|
16
16
|
import { spawn } from 'node:child_process'
|
|
17
17
|
import { existsSync } from 'node:fs'
|
|
18
18
|
import { resolve } from 'node:path'
|
|
19
|
-
import { detectWorktreeName, repoRoot, worktreeDir,
|
|
19
|
+
import { detectWorktreeName, repoRoot, worktreeDir, releasePort } from '../worktree/port.js'
|
|
20
20
|
import { list, findByWorktree, findById, unregister } from '../worktree/serverRegistry.js'
|
|
21
21
|
import { parseFlags } from './flags.js'
|
|
22
22
|
import { dim, green } from './intro.js'
|