@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.
Files changed (73) hide show
  1. package/dist/storyboard-ui.css +1 -1
  2. package/dist/storyboard-ui.js +11882 -11126
  3. package/dist/storyboard-ui.js.map +1 -1
  4. package/dist/tailwind.css +1 -1
  5. package/package.json +11 -3
  6. package/paste.config.json +54 -0
  7. package/scaffold/deploy.yml +101 -0
  8. package/scaffold/githooks/pre-push +114 -0
  9. package/scaffold/manifest.json +11 -0
  10. package/scaffold/storyboard.config.json +4 -1
  11. package/src/ActionMenuButton.svelte +12 -2
  12. package/src/CanvasCreateMenu.svelte +228 -10
  13. package/src/CanvasSnap.svelte +2 -0
  14. package/src/CoreUIBar.svelte +152 -3
  15. package/src/CreateMenuButton.svelte +4 -1
  16. package/src/InspectorPanel.svelte +2 -0
  17. package/src/PwaInstallBanner.svelte +124 -0
  18. package/src/autosync/server.js +99 -111
  19. package/src/autosync/server.test.js +0 -7
  20. package/src/canvas/collision.js +206 -0
  21. package/src/canvas/collision.test.js +271 -0
  22. package/src/canvas/deriveCanvasId.test.js +40 -0
  23. package/src/canvas/identity.js +107 -0
  24. package/src/canvas/identity.test.js +100 -0
  25. package/src/canvas/server.js +285 -31
  26. package/src/canvasConfig.js +56 -0
  27. package/src/canvasConfig.test.js +42 -0
  28. package/src/cli/canvasAdd.js +185 -0
  29. package/src/cli/canvasRead.js +208 -0
  30. package/src/cli/code.js +67 -0
  31. package/src/cli/create.js +339 -72
  32. package/src/cli/dev-helpers.js +53 -0
  33. package/src/cli/dev-helpers.test.js +53 -0
  34. package/src/cli/dev.js +245 -26
  35. package/src/cli/flags.js +174 -0
  36. package/src/cli/flags.test.js +155 -0
  37. package/src/cli/index.js +84 -13
  38. package/src/cli/intro.js +37 -0
  39. package/src/cli/proxy.js +127 -6
  40. package/src/cli/proxy.test.js +63 -0
  41. package/src/cli/schemas.js +200 -0
  42. package/src/cli/serverUrl.js +56 -0
  43. package/src/cli/setup.js +130 -20
  44. package/src/cli/snapshots.js +335 -0
  45. package/src/cli/updateVersion.js +54 -3
  46. package/src/configSchema.js +125 -0
  47. package/src/configSchema.test.js +68 -0
  48. package/src/index.js +5 -0
  49. package/src/inspector/highlighter.js +10 -2
  50. package/src/lib/components/ui/trigger-button/trigger-button.svelte +1 -1
  51. package/src/loader.js +21 -2
  52. package/src/loader.test.js +63 -1
  53. package/src/mobileViewport.js +57 -0
  54. package/src/mobileViewport.test.js +68 -0
  55. package/src/mountStoryboardCore.js +61 -7
  56. package/src/rename-watcher/config.json +23 -0
  57. package/src/rename-watcher/watcher.js +538 -0
  58. package/src/svelte-plugin-ui/components/Viewfinder.svelte +6 -17
  59. package/src/tools/handlers/flows.js +6 -7
  60. package/src/viewfinder.js +21 -9
  61. package/src/viewfinder.test.js +2 -2
  62. package/src/vite/server-plugin.js +150 -7
  63. package/src/workshop/features/createCanvas/CreateCanvasForm.svelte +8 -2
  64. package/src/workshop/features/createFlow/CreateFlowForm.svelte +1 -1
  65. package/src/workshop/features/createPage/CreatePageForm.svelte +1 -1
  66. package/src/workshop/features/createPrototype/CreatePrototypeForm.svelte +2 -2
  67. package/src/workshop/features/createStory/CreateStoryForm.svelte +160 -0
  68. package/src/workshop/features/createStory/index.js +14 -0
  69. package/src/workshop/features/registry.js +2 -0
  70. package/src/worktree/port.js +57 -1
  71. package/src/worktree/port.test.js +91 -1
  72. package/toolbar.config.json +3 -3
  73. 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
- p.log.error(`Unknown command: ${command}`)
135
+ console.log(helpScreen(version))
136
+ p.log.error(`Unknown command: ${bold(command)}`)
67
137
  process.exit(1)
68
138
  }
69
- p.outro('Run: npx storyboard <command>')
139
+
140
+ console.log(helpScreen(version))
70
141
  }
71
142
  }
@@ -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('Reloading proxy...')
123
- if (reloadCaddy(caddyfilePath)) {
124
- s.stop('Proxy reloaded')
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 reload')
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
+ }