@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/setup.js
CHANGED
|
@@ -12,18 +12,25 @@
|
|
|
12
12
|
import * as p from '@clack/prompts'
|
|
13
13
|
import { existsSync, writeFileSync, readFileSync, mkdirSync, readdirSync, symlinkSync } from 'fs'
|
|
14
14
|
import path from 'path'
|
|
15
|
-
import { execSync } from 'child_process'
|
|
15
|
+
import { execSync, spawn } from 'child_process'
|
|
16
16
|
import { gettingStartedLines, dim, magenta, bold, yellow, green } from './intro.js'
|
|
17
17
|
import { parseFlags } from './flags.js'
|
|
18
|
+
import { readUserState, writeUserState, getInstalledStoryboardVersion } from './userState.js'
|
|
18
19
|
|
|
19
20
|
const flagSchema = {
|
|
20
21
|
'skip-branch': { type: 'boolean', default: false, description: 'Skip the branch prompt at the end' },
|
|
21
22
|
branch: { type: 'string', description: 'Switch to a branch after setup (non-interactive)' },
|
|
23
|
+
'no-buddy': { type: 'boolean', default: false, description: 'Omit the storyboard mascot from setup output' },
|
|
22
24
|
'nuke': { type: 'boolean', default: false }, // undocumented: output uninstall command
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
const { flags } = parseFlags(process.argv.slice(3), flagSchema)
|
|
26
28
|
|
|
29
|
+
// Suppress the mascot when piped into another storyboard command that
|
|
30
|
+
// renders its own mascot (e.g. `storyboard dev` auto-runs setup first).
|
|
31
|
+
// Respects either the --no-buddy flag or the STORYBOARD_NO_BUDDY env var.
|
|
32
|
+
const showMascot = !flags['no-buddy'] && process.env.STORYBOARD_NO_BUDDY !== '1'
|
|
33
|
+
|
|
27
34
|
// Hidden: output uninstall command for testing fresh setups
|
|
28
35
|
if (flags.nuke) {
|
|
29
36
|
const nukeCmd = [
|
|
@@ -42,23 +49,53 @@ if (flags.nuke) {
|
|
|
42
49
|
}
|
|
43
50
|
|
|
44
51
|
/**
|
|
45
|
-
*
|
|
46
|
-
|
|
52
|
+
* Async command runner — does NOT block the event loop, so spinners animate.
|
|
53
|
+
*/
|
|
54
|
+
function runAsync(cmd, args = [], opts = {}) {
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
const child = spawn(cmd, args, { stdio: 'ignore', ...opts })
|
|
57
|
+
child.on('error', reject)
|
|
58
|
+
child.on('exit', (code) => {
|
|
59
|
+
if (code === 0) resolve()
|
|
60
|
+
else reject(new Error(`${cmd} exited with code ${code}`))
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Install a brew package with an animated spinner.
|
|
47
67
|
*/
|
|
48
|
-
async function
|
|
68
|
+
async function brewInstall(pkg, label) {
|
|
49
69
|
const spin = p.spinner()
|
|
50
|
-
|
|
70
|
+
spin.start(`Installing ${label}`)
|
|
51
71
|
try {
|
|
52
|
-
await
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
} catch
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
72
|
+
await runAsync('brew', ['install', pkg])
|
|
73
|
+
spin.stop(`${label} installed`)
|
|
74
|
+
return true
|
|
75
|
+
} catch {
|
|
76
|
+
spin.stop(`Failed to install ${label}`)
|
|
77
|
+
p.log.warning(`Install manually: brew install ${pkg}`)
|
|
78
|
+
return false
|
|
59
79
|
}
|
|
60
80
|
}
|
|
61
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Quick network probe — alerts the user if GitHub looks unreachable.
|
|
84
|
+
* We don't block on failure; downstream installers will surface their own errors.
|
|
85
|
+
*/
|
|
86
|
+
async function checkNetwork() {
|
|
87
|
+
const http = await import('node:https')
|
|
88
|
+
return new Promise((resolve) => {
|
|
89
|
+
const req = http.request('https://api.github.com', { method: 'HEAD', timeout: 4000 }, (res) => {
|
|
90
|
+
resolve(res.statusCode != null && res.statusCode < 500)
|
|
91
|
+
res.resume()
|
|
92
|
+
})
|
|
93
|
+
req.on('error', () => resolve(false))
|
|
94
|
+
req.on('timeout', () => { req.destroy(); resolve(false) })
|
|
95
|
+
req.end()
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
62
99
|
function mascot() {
|
|
63
100
|
const d = dim('·')
|
|
64
101
|
const f = magenta
|
|
@@ -87,13 +124,20 @@ function isInstalled(cmd) {
|
|
|
87
124
|
|
|
88
125
|
p.intro('storyboard setup')
|
|
89
126
|
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
127
|
+
// 0. Network probe — alert if GitHub is unreachable; don't block.
|
|
128
|
+
{
|
|
129
|
+
const online = await checkNetwork()
|
|
130
|
+
if (!online) {
|
|
131
|
+
p.log.warning('Network looks offline — installers/downloads may fail')
|
|
132
|
+
p.log.info(dim(' Tried HEAD https://api.github.com; check VPN/proxy/DNS'))
|
|
133
|
+
} else {
|
|
134
|
+
p.log.success('Network reachable')
|
|
135
|
+
}
|
|
95
136
|
}
|
|
96
137
|
|
|
138
|
+
// Node is assumed to be present (you already ran `npx storyboard setup`).
|
|
139
|
+
// node_modules will be installed at the end if missing.
|
|
140
|
+
|
|
97
141
|
// 2. Homebrew
|
|
98
142
|
let hasBrew = isInstalled('brew')
|
|
99
143
|
if (!hasBrew) {
|
|
@@ -121,35 +165,43 @@ if (hasBrew) {
|
|
|
121
165
|
if (isInstalled('git')) {
|
|
122
166
|
p.log.success('Git installed')
|
|
123
167
|
} else {
|
|
124
|
-
|
|
125
|
-
gitSpin.start('Installing Git')
|
|
126
|
-
try {
|
|
127
|
-
run('brew install git')
|
|
128
|
-
gitSpin.stop('Git installed')
|
|
129
|
-
} catch {
|
|
130
|
-
gitSpin.stop('Failed to install Git')
|
|
131
|
-
p.log.warning('Install manually: brew install git')
|
|
132
|
-
}
|
|
168
|
+
await brewInstall('git', 'Git')
|
|
133
169
|
}
|
|
134
170
|
}
|
|
135
171
|
|
|
136
172
|
// 4. Caddy is no longer used. Worktrees run their own Vite directly on
|
|
137
|
-
// `http://localhost:<port>/storyboard/`.
|
|
138
|
-
// skips the previous Caddy install + start steps.
|
|
173
|
+
// `http://localhost:<port>/storyboard/`.
|
|
139
174
|
|
|
140
175
|
if (hasBrew) {
|
|
141
|
-
// 5. GitHub CLI
|
|
176
|
+
// 5. GitHub CLI — required for `gh auth`, `gh pr`, `gh issue` in agents.
|
|
177
|
+
let ghNewlyInstalled = false
|
|
142
178
|
if (isInstalled('gh')) {
|
|
143
179
|
p.log.success('GitHub CLI installed')
|
|
144
180
|
} else {
|
|
145
|
-
|
|
146
|
-
|
|
181
|
+
ghNewlyInstalled = await brewInstall('gh', 'GitHub CLI')
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 5a. tmux (required for headless agent sessions)
|
|
185
|
+
if (isInstalled('tmux')) {
|
|
186
|
+
p.log.success('tmux installed')
|
|
187
|
+
} else {
|
|
188
|
+
await brewInstall('tmux', 'tmux')
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 5b. Surface gh auth status. Even if gh was already installed, we should
|
|
192
|
+
// prompt the user to log in — agents that shell out to `gh` will fail
|
|
193
|
+
// silently otherwise.
|
|
194
|
+
if (isInstalled('gh')) {
|
|
195
|
+
let authed = false
|
|
147
196
|
try {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
} catch {
|
|
151
|
-
|
|
152
|
-
p.log.
|
|
197
|
+
execSync('gh auth status', { stdio: 'ignore' })
|
|
198
|
+
authed = true
|
|
199
|
+
} catch { /* not authed */ }
|
|
200
|
+
if (authed) {
|
|
201
|
+
p.log.success('GitHub CLI authenticated')
|
|
202
|
+
} else {
|
|
203
|
+
p.log.warning(ghNewlyInstalled ? 'GitHub CLI installed but not logged in' : 'GitHub CLI is not logged in')
|
|
204
|
+
p.log.info(` Run ${yellow('gh auth login')} to authenticate`)
|
|
153
205
|
}
|
|
154
206
|
}
|
|
155
207
|
}
|
|
@@ -195,25 +247,102 @@ if (isInstalled('code')) {
|
|
|
195
247
|
}
|
|
196
248
|
}
|
|
197
249
|
|
|
198
|
-
// 6a. Copilot CLI
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
250
|
+
// 6a. Coding agents (Copilot CLI / Claude Code / Codex CLI).
|
|
251
|
+
// On first-time setup, ask the user which agents they want installed.
|
|
252
|
+
// On subsequent runs, only install the previously-opted-in agents that
|
|
253
|
+
// went missing — never re-prompt to avoid being annoying after upgrades.
|
|
254
|
+
{
|
|
255
|
+
const priorState = readUserState()
|
|
256
|
+
const priorAgents = priorState.agents || null
|
|
257
|
+
const firstRun = !priorState.setupVersion
|
|
258
|
+
|
|
259
|
+
const ensureLocalBinOnPath = () => {
|
|
207
260
|
const localBin = `${process.env.HOME}/.local/bin`
|
|
208
261
|
if (!process.env.PATH.includes(localBin)) {
|
|
209
262
|
process.env.PATH = `${localBin}:${process.env.PATH}`
|
|
210
263
|
}
|
|
211
|
-
copilotSpin.stop('Copilot CLI installed')
|
|
212
|
-
p.log.info(dim(' Note: You may need to restart your terminal or add ~/.local/bin to PATH'))
|
|
213
|
-
} catch {
|
|
214
|
-
copilotSpin.stop('Failed to install Copilot CLI')
|
|
215
|
-
p.log.warning('Install manually: curl -fsSL https://gh.io/copilot-install | bash')
|
|
216
264
|
}
|
|
265
|
+
|
|
266
|
+
const installers = {
|
|
267
|
+
copilot: {
|
|
268
|
+
label: 'Copilot CLI',
|
|
269
|
+
bin: 'copilot',
|
|
270
|
+
install: async () => {
|
|
271
|
+
await runAsync('bash', ['-c', 'curl -fsSL https://gh.io/copilot-install | bash'])
|
|
272
|
+
ensureLocalBinOnPath()
|
|
273
|
+
},
|
|
274
|
+
manualHint: 'curl -fsSL https://gh.io/copilot-install | bash',
|
|
275
|
+
authHint: `Auth is separate from gh — run ${yellow('copilot')} then ${yellow('/login')}`,
|
|
276
|
+
},
|
|
277
|
+
claude: {
|
|
278
|
+
label: 'Claude Code',
|
|
279
|
+
bin: 'claude',
|
|
280
|
+
install: async () => {
|
|
281
|
+
await runAsync('bash', ['-c', 'curl -fsSL https://claude.ai/install.sh | bash'])
|
|
282
|
+
ensureLocalBinOnPath()
|
|
283
|
+
},
|
|
284
|
+
manualHint: 'curl -fsSL https://claude.ai/install.sh | bash',
|
|
285
|
+
authHint: `Run ${yellow('claude')} once to authenticate`,
|
|
286
|
+
},
|
|
287
|
+
codex: {
|
|
288
|
+
label: 'Codex CLI',
|
|
289
|
+
bin: 'codex',
|
|
290
|
+
install: async () => {
|
|
291
|
+
await runAsync('npm', ['install', '-g', '@openai/codex'])
|
|
292
|
+
},
|
|
293
|
+
manualHint: 'npm i -g @openai/codex',
|
|
294
|
+
authHint: `Run ${yellow('codex login')} to authenticate`,
|
|
295
|
+
},
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
let chosen
|
|
299
|
+
if (firstRun) {
|
|
300
|
+
const selection = await p.multiselect({
|
|
301
|
+
message: 'Which coding agents do you want installed?',
|
|
302
|
+
options: [
|
|
303
|
+
{ value: 'copilot', label: 'Copilot CLI', hint: 'recommended' },
|
|
304
|
+
{ value: 'claude', label: 'Claude Code' },
|
|
305
|
+
{ value: 'codex', label: 'Codex CLI' },
|
|
306
|
+
],
|
|
307
|
+
initialValues: ['copilot'],
|
|
308
|
+
required: false,
|
|
309
|
+
})
|
|
310
|
+
if (p.isCancel(selection)) {
|
|
311
|
+
chosen = { copilot: true, claude: false, codex: false }
|
|
312
|
+
} else {
|
|
313
|
+
chosen = Object.fromEntries(Object.keys(installers).map((k) => [k, selection.includes(k)]))
|
|
314
|
+
}
|
|
315
|
+
} else {
|
|
316
|
+
// Returning user — install only what they previously opted into and is
|
|
317
|
+
// currently missing. If we have no record (older setup), default to
|
|
318
|
+
// re-installing copilot only when missing.
|
|
319
|
+
chosen = priorAgents || { copilot: true, claude: false, codex: false }
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
for (const [key, agent] of Object.entries(installers)) {
|
|
323
|
+
if (!chosen[key]) continue
|
|
324
|
+
if (isInstalled(agent.bin)) {
|
|
325
|
+
p.log.success(`${agent.label} installed`)
|
|
326
|
+
p.log.info(` ${agent.authHint}`)
|
|
327
|
+
continue
|
|
328
|
+
}
|
|
329
|
+
const spin = p.spinner()
|
|
330
|
+
spin.start(`Installing ${agent.label}`)
|
|
331
|
+
try {
|
|
332
|
+
await agent.install()
|
|
333
|
+
spin.stop(`${agent.label} installed`)
|
|
334
|
+
if (!isInstalled(agent.bin)) {
|
|
335
|
+
p.log.warning(`${agent.bin} not on PATH — add ${yellow('export PATH="$HOME/.local/bin:$PATH"')} to your shell rc`)
|
|
336
|
+
}
|
|
337
|
+
p.log.info(` ${agent.authHint}`)
|
|
338
|
+
} catch {
|
|
339
|
+
spin.stop(`Failed to install ${agent.label}`)
|
|
340
|
+
p.log.warning(`Install manually: ${agent.manualHint}`)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Persist the choice so future setups know what to keep installed.
|
|
345
|
+
writeUserState({ agents: chosen })
|
|
217
346
|
}
|
|
218
347
|
|
|
219
348
|
// 8. Git hooks
|
|
@@ -283,6 +412,8 @@ if (isInstalled('copilot')) {
|
|
|
283
412
|
'src/canvas/~*/',
|
|
284
413
|
'src/prototypes/~*/',
|
|
285
414
|
'src/prototypes/**/~*.{flow,object,record,prototype,folder}.json',
|
|
415
|
+
// Per-user local state (setup version marker, agent prefs, future onboarding state)
|
|
416
|
+
'.storyboard/.user.json',
|
|
286
417
|
]
|
|
287
418
|
if (existsSync(gitignorePath)) {
|
|
288
419
|
try {
|
|
@@ -352,18 +483,26 @@ if (isInstalled('copilot')) {
|
|
|
352
483
|
|
|
353
484
|
// 10. Install / sync dependencies
|
|
354
485
|
{
|
|
486
|
+
const installSpin = p.spinner()
|
|
487
|
+
installSpin.start('Installing dependencies')
|
|
355
488
|
try {
|
|
356
|
-
await
|
|
357
|
-
|
|
358
|
-
'Dependencies installed',
|
|
359
|
-
() => { run('npm install', { stdio: 'ignore' }) }
|
|
360
|
-
)
|
|
489
|
+
await runAsync('npm', ['install'])
|
|
490
|
+
installSpin.stop('Dependencies installed')
|
|
361
491
|
} catch {
|
|
362
|
-
|
|
492
|
+
installSpin.stop('npm install failed')
|
|
493
|
+
p.log.warning('Run it manually to see details:')
|
|
363
494
|
p.log.info(` ${dim('npm install')}`)
|
|
364
495
|
}
|
|
365
496
|
}
|
|
366
497
|
|
|
498
|
+
// 11. Stamp the user-state marker so `npm run dev` knows setup is fresh
|
|
499
|
+
// and won't re-run it until the storyboard package version changes.
|
|
500
|
+
{
|
|
501
|
+
const version = getInstalledStoryboardVersion() || 'unknown'
|
|
502
|
+
writeUserState({ setupVersion: version, setupRanAt: new Date().toISOString() })
|
|
503
|
+
p.log.success(`Setup marker written (.storyboard/.user.json @ ${version})`)
|
|
504
|
+
}
|
|
505
|
+
|
|
367
506
|
p.note(
|
|
368
507
|
[
|
|
369
508
|
...gettingStartedLines(),
|
|
@@ -380,8 +519,10 @@ p.note(
|
|
|
380
519
|
const { runBranchGuide } = await import('./branch.js')
|
|
381
520
|
await runBranchGuide(flags.branch)
|
|
382
521
|
} else if (flags['skip-branch']) {
|
|
383
|
-
|
|
384
|
-
|
|
522
|
+
if (showMascot) {
|
|
523
|
+
console.log()
|
|
524
|
+
console.log(mascot())
|
|
525
|
+
}
|
|
385
526
|
p.outro('')
|
|
386
527
|
} else {
|
|
387
528
|
// Interactive: ask the user
|
|
@@ -403,8 +544,10 @@ p.note(
|
|
|
403
544
|
const { runBranchGuide } = await import('./branch.js')
|
|
404
545
|
await runBranchGuide()
|
|
405
546
|
} else {
|
|
406
|
-
|
|
407
|
-
|
|
547
|
+
if (showMascot) {
|
|
548
|
+
console.log()
|
|
549
|
+
console.log(mascot())
|
|
550
|
+
}
|
|
408
551
|
p.log.info(`${dim('Non-interactive:')} ${green('npx sb setup --skip-branch')}`)
|
|
409
552
|
p.outro('')
|
|
410
553
|
}
|
|
@@ -17,11 +17,12 @@
|
|
|
17
17
|
|
|
18
18
|
import * as p from '@clack/prompts'
|
|
19
19
|
import { execSync, spawn } from 'node:child_process'
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
20
|
+
import { existsSync } from 'node:fs'
|
|
21
|
+
import { join } from 'node:path'
|
|
22
22
|
import { parseFlags } from './flags.js'
|
|
23
23
|
import { dim, bold } from './intro.js'
|
|
24
24
|
import { takePendingMessages } from '../canvas/terminal-config.js'
|
|
25
|
+
import { readAgentsConfig } from '../canvas/configReader.js'
|
|
25
26
|
|
|
26
27
|
const blue = (s) => `\x1b[34m${s}\x1b[0m`
|
|
27
28
|
const yellow = (s) => `\x1b[33m${s}\x1b[0m`
|
|
@@ -76,14 +77,12 @@ function agentEnv() {
|
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
/**
|
|
79
|
-
* Read agents config
|
|
80
|
+
* Read agents config (lib defaults + storyboard.config.json + terminal.config.json merged).
|
|
80
81
|
* Returns an array of { id, label, startupCommand, resumeCommand } entries.
|
|
81
82
|
*/
|
|
82
83
|
function loadAgents() {
|
|
83
84
|
try {
|
|
84
|
-
const
|
|
85
|
-
const config = JSON.parse(raw)
|
|
86
|
-
const agents = config?.canvas?.agents
|
|
85
|
+
const agents = readAgentsConfig()
|
|
87
86
|
if (!agents || typeof agents !== 'object') return []
|
|
88
87
|
return Object.entries(agents).map(([id, cfg]) => ({
|
|
89
88
|
id,
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* userState — Per-user local state for the current repo, stored in
|
|
3
|
+
* `.storyboard/.user.json` (gitignored).
|
|
4
|
+
*
|
|
5
|
+
* This is a free-form key/value bag for things that should persist across
|
|
6
|
+
* runs but never be committed:
|
|
7
|
+
* - setupVersion (string) — @dfosco/storyboard version setup was last
|
|
8
|
+
* run against. Compared against the installed version on `npm run dev`
|
|
9
|
+
* to decide whether scaffolding needs to re-run.
|
|
10
|
+
* - setupRanAt (ISO date) — last setup timestamp.
|
|
11
|
+
* - agents (object) — per-agent opt-in flags from the
|
|
12
|
+
* first-run install prompt (copilot/claude/codex booleans).
|
|
13
|
+
* - … future: onboarded, username, lastSeenChangelog, etc.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
17
|
+
import { resolve, dirname } from 'node:path'
|
|
18
|
+
|
|
19
|
+
const FILE = '.storyboard/.user.json'
|
|
20
|
+
|
|
21
|
+
export function userStatePath(cwd = process.cwd()) {
|
|
22
|
+
return resolve(cwd, FILE)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function readUserState(cwd = process.cwd()) {
|
|
26
|
+
const file = userStatePath(cwd)
|
|
27
|
+
if (!existsSync(file)) return {}
|
|
28
|
+
try { return JSON.parse(readFileSync(file, 'utf8')) } catch { return {} }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function writeUserState(patch, cwd = process.cwd()) {
|
|
32
|
+
const file = userStatePath(cwd)
|
|
33
|
+
const dir = dirname(file)
|
|
34
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
35
|
+
const next = { ...readUserState(cwd), ...patch }
|
|
36
|
+
writeFileSync(file, JSON.stringify(next, null, 2) + '\n', 'utf8')
|
|
37
|
+
return next
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Read the installed @dfosco/storyboard package version, or null if not
|
|
42
|
+
* resolvable (e.g. running from a worktree without node_modules).
|
|
43
|
+
*/
|
|
44
|
+
export function getInstalledStoryboardVersion(cwd = process.cwd()) {
|
|
45
|
+
try {
|
|
46
|
+
const pkgPath = resolve(cwd, 'node_modules', '@dfosco', 'storyboard', 'package.json')
|
|
47
|
+
return JSON.parse(readFileSync(pkgPath, 'utf8')).version || null
|
|
48
|
+
} catch { return null }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Decide whether `storyboard setup` should run before `dev`.
|
|
53
|
+
* Returns null when up-to-date, or { reason, ... } otherwise.
|
|
54
|
+
*/
|
|
55
|
+
export function setupNeeded(cwd = process.cwd()) {
|
|
56
|
+
const state = readUserState(cwd)
|
|
57
|
+
const current = getInstalledStoryboardVersion(cwd)
|
|
58
|
+
if (!state.setupVersion) return { reason: 'first-run', current }
|
|
59
|
+
if (current && state.setupVersion !== current) {
|
|
60
|
+
return { reason: 'version-changed', from: state.setupVersion, to: current }
|
|
61
|
+
}
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
* @property {string} [sessionIdEnv] — env var exposed in the agent's SessionStart hook payload that holds its session id (e.g. "COPILOT_AGENT_SESSION_ID"). When set, the server captures the id per-widget so cold restarts can auto-resume.
|
|
66
66
|
* @property {string} [sessionStateDir] — directory where the agent stores per-session state, used to pre-flight `--resume` (e.g. "~/.copilot/session-state"). Pass `null` to skip the fs check (UUID-only validation).
|
|
67
67
|
* @property {string} [sessionStateGlob] — alternative to sessionStateDir for agents that store sessions under a per-project subdir, with `{id}` placeholder (e.g. "~/.claude/projects/*/{id}.jsonl").
|
|
68
|
+
* @property {boolean} [resumeFallback] — when true (default), the resume command is shell-chained with `|| <startupCommand>` so a runtime resume failure falls through to a fresh session instead of leaving the widget with a dead terminal. Set false to opt out.
|
|
68
69
|
* @property {boolean} [resizable] — override terminal resizability for this agent
|
|
69
70
|
* @property {number} [defaultWidth] — override default width
|
|
70
71
|
* @property {number} [defaultHeight] — override default height
|
|
@@ -218,6 +218,30 @@ if (typeof window !== 'undefined') {
|
|
|
218
218
|
_applyToDOM('system', state.resolved)
|
|
219
219
|
_dispatchEvent('system', state.resolved)
|
|
220
220
|
})
|
|
221
|
+
|
|
222
|
+
// Cross-frame sync: the native `storage` event fires in OTHER windows
|
|
223
|
+
// when localStorage is mutated. Prototype iframes embed the same app on
|
|
224
|
+
// the same origin, so when the parent toolbar flips the theme we must
|
|
225
|
+
// pick that up here and re-apply the DOM attributes / dispatch the
|
|
226
|
+
// theme-changed event. Without this, Tailwind utilities (and Primer
|
|
227
|
+
// CSS vars) inside the iframe stay stuck on the load-time theme.
|
|
228
|
+
window.addEventListener('storage', (e) => {
|
|
229
|
+
if (e.storageArea !== localStorage) return
|
|
230
|
+
if (e.key === STORAGE_KEY || e.key === null) {
|
|
231
|
+
const next = readStoredTheme()
|
|
232
|
+
_current = next
|
|
233
|
+
const state = snapshot(next)
|
|
234
|
+
_store.set(state)
|
|
235
|
+
_applyToDOM(next, state.resolved)
|
|
236
|
+
_dispatchEvent(next, state.resolved)
|
|
237
|
+
} else if (e.key === SYNC_STORAGE_KEY) {
|
|
238
|
+
_syncTargets = readStoredSync()
|
|
239
|
+
_syncStore.set(_syncTargets)
|
|
240
|
+
const state = snapshot(_current)
|
|
241
|
+
_applyToDOM(_current, state.resolved)
|
|
242
|
+
_dispatchEvent(_current, state.resolved)
|
|
243
|
+
}
|
|
244
|
+
})
|
|
221
245
|
}
|
|
222
246
|
|
|
223
247
|
// ---------------------------------------------------------------------------
|
|
@@ -76,7 +76,7 @@ describe('devtools prototype auto-reload toggle', () => {
|
|
|
76
76
|
window.history.replaceState({}, '', '/')
|
|
77
77
|
})
|
|
78
78
|
|
|
79
|
-
it('shows the toggle as inactive by default (guard ON)', async () => {
|
|
79
|
+
it('shows the toggle as inactive by default (guard ON, auto-reload OFF)', async () => {
|
|
80
80
|
const devtools = await createDevtoolsHandler({})
|
|
81
81
|
const item = getPrototypeAutoReloadItem(devtools.getChildren())
|
|
82
82
|
|