@dfosco/storyboard-core 4.0.0-beta.2 → 4.0.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.css +1 -1
- package/dist/storyboard-ui.js +11882 -11126
- package/dist/storyboard-ui.js.map +1 -1
- package/dist/tailwind.css +1 -1
- package/package.json +11 -3
- package/paste.config.json +54 -0
- package/scaffold/deploy.yml +101 -0
- package/scaffold/githooks/pre-push +114 -0
- package/scaffold/manifest.json +11 -0
- package/scaffold/storyboard.config.json +4 -1
- package/src/ActionMenuButton.svelte +12 -2
- package/src/CanvasCreateMenu.svelte +228 -10
- package/src/CanvasSnap.svelte +2 -0
- package/src/CoreUIBar.svelte +152 -3
- package/src/CreateMenuButton.svelte +4 -1
- package/src/InspectorPanel.svelte +2 -0
- package/src/PwaInstallBanner.svelte +124 -0
- package/src/autosync/server.js +99 -111
- package/src/autosync/server.test.js +0 -7
- package/src/canvas/collision.js +206 -0
- package/src/canvas/collision.test.js +271 -0
- package/src/canvas/deriveCanvasId.test.js +40 -0
- package/src/canvas/identity.js +107 -0
- package/src/canvas/identity.test.js +100 -0
- package/src/canvas/server.js +285 -31
- package/src/canvasConfig.js +56 -0
- package/src/canvasConfig.test.js +42 -0
- package/src/cli/canvasAdd.js +185 -0
- package/src/cli/canvasRead.js +208 -0
- package/src/cli/code.js +67 -0
- package/src/cli/create.js +339 -72
- package/src/cli/dev-helpers.js +53 -0
- package/src/cli/dev-helpers.test.js +53 -0
- package/src/cli/dev.js +245 -26
- package/src/cli/flags.js +174 -0
- package/src/cli/flags.test.js +155 -0
- package/src/cli/index.js +84 -13
- package/src/cli/intro.js +37 -0
- package/src/cli/proxy.js +127 -6
- package/src/cli/proxy.test.js +63 -0
- package/src/cli/schemas.js +200 -0
- package/src/cli/serverUrl.js +56 -0
- package/src/cli/setup.js +130 -20
- package/src/cli/snapshots.js +335 -0
- package/src/cli/updateVersion.js +54 -3
- package/src/configSchema.js +125 -0
- package/src/configSchema.test.js +68 -0
- package/src/index.js +5 -0
- package/src/inspector/highlighter.js +10 -2
- package/src/lib/components/ui/trigger-button/trigger-button.svelte +1 -1
- package/src/loader.js +21 -2
- package/src/loader.test.js +63 -1
- package/src/mobileViewport.js +57 -0
- package/src/mobileViewport.test.js +68 -0
- package/src/mountStoryboardCore.js +61 -7
- package/src/rename-watcher/config.json +23 -0
- package/src/rename-watcher/watcher.js +538 -0
- package/src/svelte-plugin-ui/components/Viewfinder.svelte +6 -17
- package/src/tools/handlers/flows.js +6 -7
- package/src/viewfinder.js +21 -9
- package/src/viewfinder.test.js +2 -2
- package/src/vite/server-plugin.js +150 -7
- package/src/workshop/features/createCanvas/CreateCanvasForm.svelte +8 -2
- package/src/workshop/features/createFlow/CreateFlowForm.svelte +1 -1
- package/src/workshop/features/createPage/CreatePageForm.svelte +1 -1
- package/src/workshop/features/createPrototype/CreatePrototypeForm.svelte +2 -2
- package/src/workshop/features/createStory/CreateStoryForm.svelte +160 -0
- package/src/workshop/features/createStory/index.js +14 -0
- package/src/workshop/features/registry.js +2 -0
- package/src/worktree/port.js +57 -1
- package/src/worktree/port.test.js +91 -1
- package/toolbar.config.json +3 -3
- package/widgets.config.json +132 -27
package/src/cli/index.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import * as p from '@clack/prompts'
|
|
17
17
|
import { readFileSync } from 'fs'
|
|
18
18
|
import { resolve } from 'path'
|
|
19
|
+
import { gettingStartedLines, dim, magenta, cyan, green, bold, yellow } from './intro.js'
|
|
19
20
|
|
|
20
21
|
function getVersion() {
|
|
21
22
|
try {
|
|
@@ -26,6 +27,67 @@ function getVersion() {
|
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
function helpScreen(version) {
|
|
31
|
+
const d = dim('·')
|
|
32
|
+
const b = dim
|
|
33
|
+
const f = magenta
|
|
34
|
+
|
|
35
|
+
const mascot = [
|
|
36
|
+
` ${b('╭─────────────────╮')}`,
|
|
37
|
+
` ${b('│')} ${d} ${f('◠')} ${f('◡')} ${f('◠')} ${d} ${b('│')} ${bold('storyboard')} ${dim(`v${version}`)}`,
|
|
38
|
+
` ${b('│')} ${d} ${d} ${d} ${d} ${d} ${b('│')} ${dim('A design tool for prototyping')}`,
|
|
39
|
+
` ${b('╰─────────────────╯')}`,
|
|
40
|
+
].join('\n')
|
|
41
|
+
|
|
42
|
+
const cmd = (name, desc) => ` ${green(name.padEnd(22))}${desc}`
|
|
43
|
+
|
|
44
|
+
const gettingStarted = [
|
|
45
|
+
'',
|
|
46
|
+
...gettingStartedLines(),
|
|
47
|
+
].join('\n')
|
|
48
|
+
|
|
49
|
+
const commands = [
|
|
50
|
+
'',
|
|
51
|
+
` ${bold('All commands:')}`,
|
|
52
|
+
'',
|
|
53
|
+
` ${bold(cyan('Development'))}`,
|
|
54
|
+
cmd('dev', 'Start Vite dev server + update proxy'),
|
|
55
|
+
cmd('dev [branch]', 'Start dev for a specific worktree/branch'),
|
|
56
|
+
cmd('code [branch]', 'Open a worktree in VS Code'),
|
|
57
|
+
cmd('exit', 'Stop all dev servers and proxy'),
|
|
58
|
+
'',
|
|
59
|
+
` ${bold(cyan('Create'))}`,
|
|
60
|
+
cmd('create', 'Interactive creation picker'),
|
|
61
|
+
cmd('create prototype', 'Create a prototype'),
|
|
62
|
+
cmd('create canvas', 'Create a canvas'),
|
|
63
|
+
cmd('create flow', 'Create a flow for a prototype'),
|
|
64
|
+
cmd('create page', 'Create a page in a prototype'),
|
|
65
|
+
'',
|
|
66
|
+
` ${bold(cyan('Canvas'))}`,
|
|
67
|
+
cmd('canvas add <type>', 'Add widget to a canvas'),
|
|
68
|
+
` ${dim('types: sticky-note, markdown, prototype')}`,
|
|
69
|
+
cmd('canvas read [name]', 'Read canvas state and list widgets'),
|
|
70
|
+
cmd('snapshots [name]', 'Generate preview snapshots for canvas widgets'),
|
|
71
|
+
` ${dim('--force to regenerate existing snapshots')}`,
|
|
72
|
+
'',
|
|
73
|
+
` ${bold(cyan('Setup'))}`,
|
|
74
|
+
cmd('setup', 'Install deps, Caddy proxy, start proxy'),
|
|
75
|
+
cmd('proxy', 'Generate Caddyfile + start/reload Caddy'),
|
|
76
|
+
'',
|
|
77
|
+
` ${bold(cyan('Updates'))}`,
|
|
78
|
+
cmd('update', 'Update storyboard packages to latest'),
|
|
79
|
+
cmd('update:<tag>', 'Update to a specific tag ' + dim('(beta, alpha, ...)')),
|
|
80
|
+
'',
|
|
81
|
+
` ${dim('All create commands accept --flags for non-interactive use.')}`,
|
|
82
|
+
` ${dim('Run')} ${yellow('npx storyboard create <type> --help')} ${dim('for flag details.')}`,
|
|
83
|
+
'',
|
|
84
|
+
` ${dim('Usage:')} ${yellow('npx storyboard')} ${dim('<command>')}`,
|
|
85
|
+
` ${dim('Alias:')} ${yellow('npx sb')} ${dim('<command>')}`,
|
|
86
|
+
].join('\n')
|
|
87
|
+
|
|
88
|
+
return `\n${mascot}\n${gettingStarted}\n${commands}\n`
|
|
89
|
+
}
|
|
90
|
+
|
|
29
91
|
const command = process.argv[2]
|
|
30
92
|
|
|
31
93
|
switch (command) {
|
|
@@ -41,31 +103,40 @@ switch (command) {
|
|
|
41
103
|
case 'create':
|
|
42
104
|
import('./create.js')
|
|
43
105
|
break
|
|
106
|
+
case 'canvas':
|
|
107
|
+
if (process.argv[3] === 'add') {
|
|
108
|
+
import('./canvasAdd.js')
|
|
109
|
+
} else if (process.argv[3] === 'read' || !process.argv[3]) {
|
|
110
|
+
import('./canvasRead.js')
|
|
111
|
+
} else {
|
|
112
|
+
const version = getVersion()
|
|
113
|
+
console.log(helpScreen(version))
|
|
114
|
+
p.log.error(`Unknown canvas subcommand: ${bold(process.argv[3] || '(none)')}`)
|
|
115
|
+
process.exit(1)
|
|
116
|
+
}
|
|
117
|
+
break
|
|
118
|
+
case 'snapshots':
|
|
119
|
+
import('./snapshots.js')
|
|
120
|
+
break
|
|
44
121
|
case 'exit':
|
|
45
122
|
import('./exit.js')
|
|
46
123
|
break
|
|
124
|
+
case 'code':
|
|
125
|
+
import('./code.js')
|
|
126
|
+
break
|
|
47
127
|
default: {
|
|
48
128
|
if (command === 'update' || (command && command.startsWith('update:'))) {
|
|
49
129
|
import('./updateVersion.js')
|
|
50
130
|
break
|
|
51
131
|
}
|
|
52
132
|
const version = getVersion()
|
|
53
|
-
p.intro(`storyboard v${version}`)
|
|
54
|
-
p.log.message(`Commands:
|
|
55
|
-
dev Start Vite dev server + update proxy
|
|
56
|
-
create Create a prototype or canvas
|
|
57
|
-
setup Install deps, Caddy proxy, start proxy
|
|
58
|
-
proxy Generate Caddyfile + start/reload Caddy
|
|
59
|
-
exit Stop all dev servers and proxy
|
|
60
|
-
update Update storyboard packages to latest
|
|
61
|
-
update:<version> Update to specific version
|
|
62
|
-
update:beta Update to latest beta
|
|
63
|
-
update:alpha Update to latest alpha`)
|
|
64
133
|
|
|
65
134
|
if (command) {
|
|
66
|
-
|
|
135
|
+
console.log(helpScreen(version))
|
|
136
|
+
p.log.error(`Unknown command: ${bold(command)}`)
|
|
67
137
|
process.exit(1)
|
|
68
138
|
}
|
|
69
|
-
|
|
139
|
+
|
|
140
|
+
console.log(helpScreen(version))
|
|
70
141
|
}
|
|
71
142
|
}
|
package/src/cli/intro.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for CLI getting-started content.
|
|
3
|
+
* Used by both `storyboard setup` and the help screen (`storyboard`).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ANSI helpers — shared across CLI modules
|
|
7
|
+
export const dim = (s) => `\x1b[2m${s}\x1b[0m`
|
|
8
|
+
export const magenta = (s) => `\x1b[35m${s}\x1b[0m`
|
|
9
|
+
export const cyan = (s) => `\x1b[36m${s}\x1b[0m`
|
|
10
|
+
export const green = (s) => `\x1b[32m${s}\x1b[0m`
|
|
11
|
+
export const bold = (s) => `\x1b[1m${s}\x1b[0m`
|
|
12
|
+
export const white = (s) => `\x1b[97m${s}\x1b[0m`
|
|
13
|
+
export const yellow = (s) => `\x1b[33m${s}\x1b[0m`
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Returns the getting-started intro lines as an array of strings.
|
|
17
|
+
* @param {object} [opts]
|
|
18
|
+
* @param {string} [opts.indent=' '] Prefix for each non-empty line
|
|
19
|
+
*/
|
|
20
|
+
export function gettingStartedLines({ indent = ' ' } = {}) {
|
|
21
|
+
const i = indent
|
|
22
|
+
return [
|
|
23
|
+
`${i}Welcome! Storyboard is a design tool to build and`,
|
|
24
|
+
`${i}collaborate on prototypes. Here's how to get started:`,
|
|
25
|
+
'',
|
|
26
|
+
`${i} ${green('npx storyboard dev')} Start developing locally`,
|
|
27
|
+
`${i} ${green('npx storyboard create prototype')} Create a prototype`,
|
|
28
|
+
`${i} ${green('npx storyboard create canvas')} Create a canvas`,
|
|
29
|
+
`${i} ${green('npx storyboard create component')} Create a component (.story.jsx)`,
|
|
30
|
+
`${i} ${green('npx storyboard canvas add sticky-note')} Add a widget to a canvas`,
|
|
31
|
+
'',
|
|
32
|
+
`${i} ${dim('Using an AI assistant? You can also ask it to')}`,
|
|
33
|
+
`${i} ${dim('"create a prototype", "create a canvas" or "create a component" for you!')}`,
|
|
34
|
+
'',
|
|
35
|
+
`${i} ${dim('Docs:')} ${cyan('https://github.com/dfosco/storyboard/blob/main/README.md')}`,
|
|
36
|
+
]
|
|
37
|
+
}
|
package/src/cli/proxy.js
CHANGED
|
@@ -14,9 +14,9 @@ import { execSync } from 'child_process'
|
|
|
14
14
|
import { dirname, resolve } from 'path'
|
|
15
15
|
import { portsFilePath } from '../worktree/port.js'
|
|
16
16
|
|
|
17
|
-
export function readDevDomain() {
|
|
17
|
+
export function readDevDomain(cwd) {
|
|
18
18
|
try {
|
|
19
|
-
const configPath = resolve(process.cwd(), 'storyboard.config.json')
|
|
19
|
+
const configPath = resolve(cwd || process.cwd(), 'storyboard.config.json')
|
|
20
20
|
const config = JSON.parse(readFileSync(configPath, 'utf8'))
|
|
21
21
|
return `${config.devDomain || 'storyboard'}.localhost`
|
|
22
22
|
} catch {
|
|
@@ -95,6 +95,124 @@ export function reloadCaddy(caddyfilePath) {
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
// ── Caddy admin API (additive route upsert) ──
|
|
99
|
+
|
|
100
|
+
const CADDY_ADMIN = 'http://localhost:2019'
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Build a Caddy JSON route object for this repo's devDomain.
|
|
104
|
+
* Tagged with @id so it can be upserted independently of other repos.
|
|
105
|
+
*/
|
|
106
|
+
export function generateRouteConfig(portOverrides = {}) {
|
|
107
|
+
const portsFile = portsFilePath()
|
|
108
|
+
let ports = { main: 1234 }
|
|
109
|
+
if (existsSync(portsFile)) {
|
|
110
|
+
try { ports = JSON.parse(readFileSync(portsFile, 'utf8')) } catch { /* use defaults */ }
|
|
111
|
+
}
|
|
112
|
+
Object.assign(ports, portOverrides)
|
|
113
|
+
|
|
114
|
+
const mainPort = ports.main || 1234
|
|
115
|
+
const branches = Object.entries(ports).filter(([name]) => name !== 'main')
|
|
116
|
+
|
|
117
|
+
// Branch subroutes first (more specific), then main fallback
|
|
118
|
+
const subroutes = branches.map(([name, port]) => ({
|
|
119
|
+
match: [{ path: [`/branch--${name}/*`] }],
|
|
120
|
+
handle: [{ handler: 'reverse_proxy', upstreams: [{ dial: `localhost:${port}` }] }],
|
|
121
|
+
}))
|
|
122
|
+
subroutes.push({
|
|
123
|
+
handle: [{ handler: 'reverse_proxy', upstreams: [{ dial: `localhost:${mainPort}` }] }],
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
'@id': DOMAIN.replace('.localhost', ''),
|
|
128
|
+
match: [{ host: [DOMAIN] }],
|
|
129
|
+
handle: [{ handler: 'subroute', routes: subroutes }],
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Find indices of stale routes that match the same host but lack an @id.
|
|
135
|
+
* Returns indices sorted descending (highest first) for safe deletion.
|
|
136
|
+
* Exported for testing.
|
|
137
|
+
*/
|
|
138
|
+
export function findStaleRouteIndices(routes, keepId, host) {
|
|
139
|
+
const indices = []
|
|
140
|
+
for (let i = 0; i < routes.length; i++) {
|
|
141
|
+
const route = routes[i]
|
|
142
|
+
if (route['@id'] === keepId) continue
|
|
143
|
+
if (route['@id']) continue // different intentional route — leave it
|
|
144
|
+
const routeHosts = route.match?.[0]?.host || []
|
|
145
|
+
if (routeHosts.includes(host)) {
|
|
146
|
+
indices.push(i)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return indices.reverse()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Remove stale routes that match the same host but lack an @id.
|
|
154
|
+
* These are leftovers from Caddyfile reloads that shadow admin-API routes.
|
|
155
|
+
* Deletes from highest index to lowest to preserve indices during removal.
|
|
156
|
+
* Best-effort — warns on failure but does not throw.
|
|
157
|
+
*/
|
|
158
|
+
function cleanupDuplicateRoutes(keepId, host) {
|
|
159
|
+
try {
|
|
160
|
+
const config = execSync(
|
|
161
|
+
`curl -sf '${CADDY_ADMIN}/config/apps/http/servers/srv0/routes'`,
|
|
162
|
+
{ encoding: 'utf-8', timeout: 5000 },
|
|
163
|
+
)
|
|
164
|
+
const routes = JSON.parse(config)
|
|
165
|
+
const staleIndices = findStaleRouteIndices(routes, keepId, host)
|
|
166
|
+
for (const idx of staleIndices) {
|
|
167
|
+
try {
|
|
168
|
+
execSync(
|
|
169
|
+
`curl -sf -X DELETE '${CADDY_ADMIN}/config/apps/http/servers/srv0/routes/${idx}'`,
|
|
170
|
+
{ encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] },
|
|
171
|
+
)
|
|
172
|
+
} catch {
|
|
173
|
+
console.warn(`[storyboard] failed to remove stale proxy route at index ${idx}`)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} catch {
|
|
177
|
+
console.warn('[storyboard] failed to clean up stale proxy routes — branch URLs may not work via proxy')
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Upsert this repo's route in the running Caddy instance via admin API.
|
|
183
|
+
* Uses PATCH if the @id exists, POST if it doesn't.
|
|
184
|
+
* After a successful upsert, removes any stale non-@id routes for the
|
|
185
|
+
* same host (leftovers from prior Caddyfile reloads).
|
|
186
|
+
* Returns true on success, false on failure.
|
|
187
|
+
*/
|
|
188
|
+
export function upsertCaddyRoute(routeConfig) {
|
|
189
|
+
const id = routeConfig['@id']
|
|
190
|
+
const host = routeConfig.match?.[0]?.host?.[0]
|
|
191
|
+
const payload = JSON.stringify(routeConfig)
|
|
192
|
+
|
|
193
|
+
// Try PATCH first (update existing route by @id)
|
|
194
|
+
try {
|
|
195
|
+
execSync(
|
|
196
|
+
`curl -sf -X PATCH '${CADDY_ADMIN}/id/${id}' -H 'Content-Type: application/json' -d '${payload.replace(/'/g, "'\\''")}'`,
|
|
197
|
+
{ encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] },
|
|
198
|
+
)
|
|
199
|
+
if (host) cleanupDuplicateRoutes(id, host)
|
|
200
|
+
return true
|
|
201
|
+
} catch {
|
|
202
|
+
// @id doesn't exist yet — POST a new route
|
|
203
|
+
try {
|
|
204
|
+
execSync(
|
|
205
|
+
`curl -sf -X POST '${CADDY_ADMIN}/config/apps/http/servers/srv0/routes' -H 'Content-Type: application/json' -d '${payload.replace(/'/g, "'\\''")}'`,
|
|
206
|
+
{ encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] },
|
|
207
|
+
)
|
|
208
|
+
if (host) cleanupDuplicateRoutes(id, host)
|
|
209
|
+
return true
|
|
210
|
+
} catch {
|
|
211
|
+
return false
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
98
216
|
export function startCaddy(caddyfilePath) {
|
|
99
217
|
try {
|
|
100
218
|
execSync(`sudo caddy start --config "${caddyfilePath}" >/dev/null 2>&1`, { stdio: ['inherit', 'pipe', 'pipe'] })
|
|
@@ -119,11 +237,14 @@ if (isDirectRun) {
|
|
|
119
237
|
|
|
120
238
|
const s = spinner()
|
|
121
239
|
if (isCaddyRunning()) {
|
|
122
|
-
s.start('
|
|
123
|
-
|
|
124
|
-
|
|
240
|
+
s.start('Updating proxy routes...')
|
|
241
|
+
const routeConfig = generateRouteConfig()
|
|
242
|
+
if (upsertCaddyRoute(routeConfig)) {
|
|
243
|
+
s.stop('Proxy routes updated (admin API)')
|
|
244
|
+
} else if (reloadCaddy(caddyfilePath)) {
|
|
245
|
+
s.stop('Proxy reloaded (Caddyfile fallback)')
|
|
125
246
|
} else {
|
|
126
|
-
s.stop('Failed to
|
|
247
|
+
s.stop('Failed to update proxy')
|
|
127
248
|
process.exit(1)
|
|
128
249
|
}
|
|
129
250
|
} else {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { findStaleRouteIndices } from './proxy.js'
|
|
3
|
+
|
|
4
|
+
describe('findStaleRouteIndices', () => {
|
|
5
|
+
it('identifies stale non-@id routes matching the same host', () => {
|
|
6
|
+
const routes = [
|
|
7
|
+
{ match: [{ host: ['storyboard.localhost'] }], terminal: true }, // index 0: stale
|
|
8
|
+
{ '@id': 'storyboard', match: [{ host: ['storyboard.localhost'] }] }, // index 1: ours
|
|
9
|
+
]
|
|
10
|
+
const indices = findStaleRouteIndices(routes, 'storyboard', 'storyboard.localhost')
|
|
11
|
+
expect(indices).toEqual([0])
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('preserves routes with a different @id for the same host', () => {
|
|
15
|
+
const routes = [
|
|
16
|
+
{ '@id': 'other-app', match: [{ host: ['storyboard.localhost'] }] },
|
|
17
|
+
{ '@id': 'storyboard', match: [{ host: ['storyboard.localhost'] }] },
|
|
18
|
+
]
|
|
19
|
+
const indices = findStaleRouteIndices(routes, 'storyboard', 'storyboard.localhost')
|
|
20
|
+
expect(indices).toEqual([])
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('returns multiple stale indices in descending order', () => {
|
|
24
|
+
const routes = [
|
|
25
|
+
{ match: [{ host: ['storyboard.localhost'] }] }, // index 0: stale
|
|
26
|
+
{ '@id': 'storyboard-core', match: [{ host: ['storyboard-core.localhost'] }] }, // index 1: different host
|
|
27
|
+
{ '@id': 'storyboard', match: [{ host: ['storyboard.localhost'] }] }, // index 2: ours
|
|
28
|
+
{ match: [{ host: ['storyboard.localhost'] }] }, // index 3: stale
|
|
29
|
+
]
|
|
30
|
+
const indices = findStaleRouteIndices(routes, 'storyboard', 'storyboard.localhost')
|
|
31
|
+
expect(indices).toEqual([3, 0]) // descending for safe deletion
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('ignores routes for a different host', () => {
|
|
35
|
+
const routes = [
|
|
36
|
+
{ match: [{ host: ['other.localhost'] }] },
|
|
37
|
+
{ '@id': 'storyboard', match: [{ host: ['storyboard.localhost'] }] },
|
|
38
|
+
]
|
|
39
|
+
const indices = findStaleRouteIndices(routes, 'storyboard', 'storyboard.localhost')
|
|
40
|
+
expect(indices).toEqual([])
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('handles routes with no match field', () => {
|
|
44
|
+
const routes = [
|
|
45
|
+
{ handle: [{ handler: 'reverse_proxy' }] }, // no match
|
|
46
|
+
{ '@id': 'storyboard', match: [{ host: ['storyboard.localhost'] }] },
|
|
47
|
+
]
|
|
48
|
+
const indices = findStaleRouteIndices(routes, 'storyboard', 'storyboard.localhost')
|
|
49
|
+
expect(indices).toEqual([])
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('returns empty array when no stale routes exist', () => {
|
|
53
|
+
const routes = [
|
|
54
|
+
{ '@id': 'storyboard', match: [{ host: ['storyboard.localhost'] }] },
|
|
55
|
+
]
|
|
56
|
+
const indices = findStaleRouteIndices(routes, 'storyboard', 'storyboard.localhost')
|
|
57
|
+
expect(indices).toEqual([])
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('handles empty routes array', () => {
|
|
61
|
+
expect(findStaleRouteIndices([], 'storyboard', 'storyboard.localhost')).toEqual([])
|
|
62
|
+
})
|
|
63
|
+
})
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creation schemas — define flags for each `storyboard create` subcommand.
|
|
3
|
+
*
|
|
4
|
+
* Each schema is a plain object mapping flag names to definitions.
|
|
5
|
+
* Schemas serve as validation, documentation, and help-text source.
|
|
6
|
+
*
|
|
7
|
+
* @typedef {import('./flags.js').FlagSchema} FlagSchema
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** @type {FlagSchema} */
|
|
11
|
+
export const prototypeSchema = {
|
|
12
|
+
name: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
required: true,
|
|
15
|
+
description: 'Prototype name (kebab-case)',
|
|
16
|
+
aliases: ['n'],
|
|
17
|
+
},
|
|
18
|
+
title: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
description: 'Display title',
|
|
21
|
+
aliases: ['t'],
|
|
22
|
+
},
|
|
23
|
+
folder: {
|
|
24
|
+
type: 'string',
|
|
25
|
+
description: 'Parent .folder directory',
|
|
26
|
+
aliases: ['f'],
|
|
27
|
+
},
|
|
28
|
+
partial: {
|
|
29
|
+
type: 'string',
|
|
30
|
+
description: 'Template/recipe name',
|
|
31
|
+
aliases: ['p'],
|
|
32
|
+
},
|
|
33
|
+
author: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
description: 'Author name(s)',
|
|
36
|
+
aliases: ['a'],
|
|
37
|
+
},
|
|
38
|
+
description: {
|
|
39
|
+
type: 'string',
|
|
40
|
+
description: 'Prototype description',
|
|
41
|
+
aliases: ['d'],
|
|
42
|
+
},
|
|
43
|
+
url: {
|
|
44
|
+
type: 'string',
|
|
45
|
+
description: 'External URL (makes it an external prototype)',
|
|
46
|
+
},
|
|
47
|
+
flow: {
|
|
48
|
+
type: 'boolean',
|
|
49
|
+
default: false,
|
|
50
|
+
description: 'Create a default flow file',
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** @type {FlagSchema} */
|
|
55
|
+
export const canvasSchema = {
|
|
56
|
+
name: {
|
|
57
|
+
type: 'string',
|
|
58
|
+
required: true,
|
|
59
|
+
description: 'Canvas name (kebab-case)',
|
|
60
|
+
aliases: ['n'],
|
|
61
|
+
},
|
|
62
|
+
title: {
|
|
63
|
+
type: 'string',
|
|
64
|
+
description: 'Display title',
|
|
65
|
+
aliases: ['t'],
|
|
66
|
+
},
|
|
67
|
+
folder: {
|
|
68
|
+
type: 'string',
|
|
69
|
+
description: 'Parent .folder directory',
|
|
70
|
+
aliases: ['f'],
|
|
71
|
+
},
|
|
72
|
+
grid: {
|
|
73
|
+
type: 'boolean',
|
|
74
|
+
default: true,
|
|
75
|
+
description: 'Show dot grid',
|
|
76
|
+
},
|
|
77
|
+
jsx: {
|
|
78
|
+
type: 'boolean',
|
|
79
|
+
default: false,
|
|
80
|
+
description: 'Include JSX companion file',
|
|
81
|
+
},
|
|
82
|
+
description: {
|
|
83
|
+
type: 'string',
|
|
84
|
+
description: 'Optional description',
|
|
85
|
+
aliases: ['d'],
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** @type {FlagSchema} */
|
|
90
|
+
export const flowSchema = {
|
|
91
|
+
name: {
|
|
92
|
+
type: 'string',
|
|
93
|
+
required: true,
|
|
94
|
+
description: 'Flow name (kebab-case)',
|
|
95
|
+
aliases: ['n'],
|
|
96
|
+
},
|
|
97
|
+
prototype: {
|
|
98
|
+
type: 'string',
|
|
99
|
+
required: true,
|
|
100
|
+
description: 'Target prototype name',
|
|
101
|
+
aliases: ['p'],
|
|
102
|
+
},
|
|
103
|
+
title: {
|
|
104
|
+
type: 'string',
|
|
105
|
+
description: 'Display title',
|
|
106
|
+
aliases: ['t'],
|
|
107
|
+
},
|
|
108
|
+
folder: {
|
|
109
|
+
type: 'string',
|
|
110
|
+
description: 'Parent .folder directory',
|
|
111
|
+
aliases: ['f'],
|
|
112
|
+
},
|
|
113
|
+
globals: {
|
|
114
|
+
type: 'array',
|
|
115
|
+
description: '$global object names',
|
|
116
|
+
aliases: ['g'],
|
|
117
|
+
},
|
|
118
|
+
'copy-from': {
|
|
119
|
+
type: 'string',
|
|
120
|
+
description: 'Existing flow to copy from',
|
|
121
|
+
},
|
|
122
|
+
author: {
|
|
123
|
+
type: 'string',
|
|
124
|
+
description: 'Author name(s)',
|
|
125
|
+
aliases: ['a'],
|
|
126
|
+
},
|
|
127
|
+
description: {
|
|
128
|
+
type: 'string',
|
|
129
|
+
description: 'Flow description',
|
|
130
|
+
aliases: ['d'],
|
|
131
|
+
},
|
|
132
|
+
'starting-page': {
|
|
133
|
+
type: 'string',
|
|
134
|
+
description: 'Starting page path',
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** @type {FlagSchema} */
|
|
139
|
+
export const pageSchema = {
|
|
140
|
+
prototype: {
|
|
141
|
+
type: 'string',
|
|
142
|
+
required: true,
|
|
143
|
+
description: 'Target prototype name',
|
|
144
|
+
aliases: ['p'],
|
|
145
|
+
},
|
|
146
|
+
path: {
|
|
147
|
+
type: 'string',
|
|
148
|
+
required: true,
|
|
149
|
+
description: 'Page path (e.g. settings/general)',
|
|
150
|
+
},
|
|
151
|
+
folder: {
|
|
152
|
+
type: 'string',
|
|
153
|
+
description: 'Parent .folder directory',
|
|
154
|
+
aliases: ['f'],
|
|
155
|
+
},
|
|
156
|
+
template: {
|
|
157
|
+
type: 'string',
|
|
158
|
+
description: 'Page template name',
|
|
159
|
+
aliases: ['t'],
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** @type {FlagSchema} */
|
|
164
|
+
export const widgetSchema = {
|
|
165
|
+
canvas: {
|
|
166
|
+
type: 'string',
|
|
167
|
+
required: true,
|
|
168
|
+
description: 'Target canvas name',
|
|
169
|
+
aliases: ['c'],
|
|
170
|
+
},
|
|
171
|
+
x: {
|
|
172
|
+
type: 'number',
|
|
173
|
+
default: 0,
|
|
174
|
+
description: 'X position',
|
|
175
|
+
},
|
|
176
|
+
y: {
|
|
177
|
+
type: 'number',
|
|
178
|
+
default: 0,
|
|
179
|
+
description: 'Y position',
|
|
180
|
+
},
|
|
181
|
+
props: {
|
|
182
|
+
type: 'string',
|
|
183
|
+
description: 'Widget props as JSON string',
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** @type {FlagSchema} */
|
|
188
|
+
export const componentSchema = {
|
|
189
|
+
name: {
|
|
190
|
+
type: 'string',
|
|
191
|
+
required: true,
|
|
192
|
+
description: 'Component name (kebab-case)',
|
|
193
|
+
aliases: ['n'],
|
|
194
|
+
},
|
|
195
|
+
directory: {
|
|
196
|
+
type: 'string',
|
|
197
|
+
description: 'Subdirectory inside src/components/',
|
|
198
|
+
aliases: ['d'],
|
|
199
|
+
},
|
|
200
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the dev server URL for the current worktree.
|
|
3
|
+
*
|
|
4
|
+
* Checks the Caddy admin API first to find the actual port mapped to
|
|
5
|
+
* this branch's route, since ports.json can drift from the running
|
|
6
|
+
* dev server. Falls back to ports.json if Caddy isn't reachable.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { detectWorktreeName, getPort } from '../worktree/port.js'
|
|
10
|
+
import { readDevDomain } from './proxy.js'
|
|
11
|
+
import { execSync } from 'child_process'
|
|
12
|
+
|
|
13
|
+
export function getServerUrl() {
|
|
14
|
+
const name = detectWorktreeName()
|
|
15
|
+
|
|
16
|
+
// Try Caddy admin API for the real port
|
|
17
|
+
try {
|
|
18
|
+
const raw = execSync(
|
|
19
|
+
'curl -sf http://localhost:2019/config/apps/http/servers/srv0/routes',
|
|
20
|
+
{ encoding: 'utf8', timeout: 2000, stdio: ['pipe', 'pipe', 'pipe'] }
|
|
21
|
+
)
|
|
22
|
+
const routes = JSON.parse(raw)
|
|
23
|
+
const domain = readDevDomain()
|
|
24
|
+
|
|
25
|
+
for (const route of routes) {
|
|
26
|
+
const hosts = route.match?.[0]?.host || []
|
|
27
|
+
if (!hosts.includes(domain)) continue
|
|
28
|
+
const subroutes = route.handle?.[0]?.routes || []
|
|
29
|
+
|
|
30
|
+
if (name === 'main') {
|
|
31
|
+
// Main is the fallback route (no match path)
|
|
32
|
+
const fallback = subroutes.find(r => !r.match)
|
|
33
|
+
if (fallback) {
|
|
34
|
+
const dial = fallback.handle?.[0]?.upstreams?.[0]?.dial
|
|
35
|
+
if (dial) return `http://${dial}`
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
// Branch route matches /branch--<name>/*
|
|
39
|
+
const branchRoute = subroutes.find(r => {
|
|
40
|
+
const paths = r.match?.[0]?.path || []
|
|
41
|
+
return paths.some(p => p === `/branch--${name}/*`)
|
|
42
|
+
})
|
|
43
|
+
if (branchRoute) {
|
|
44
|
+
const dial = branchRoute.handle?.[0]?.upstreams?.[0]?.dial
|
|
45
|
+
if (dial) return `http://${dial}`
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
// Caddy not running or not reachable — fall through
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Fallback to ports.json
|
|
54
|
+
const port = getPort(name)
|
|
55
|
+
return `http://localhost:${port}`
|
|
56
|
+
}
|