@1agh/maude 0.15.0

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 (211) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +166 -0
  3. package/cli/bin/maude.exe +15 -0
  4. package/cli/bin/maude.mjs +45 -0
  5. package/cli/bin/mdcc.exe +10 -0
  6. package/cli/bin/mdcc.mjs +7 -0
  7. package/cli/cli-wrapper.cjs +67 -0
  8. package/cli/commands/config.mjs +94 -0
  9. package/cli/commands/design.mjs +386 -0
  10. package/cli/commands/help.mjs +57 -0
  11. package/cli/commands/init.mjs +178 -0
  12. package/cli/commands/version.mjs +7 -0
  13. package/cli/install.cjs +113 -0
  14. package/cli/lib/argv.mjs +37 -0
  15. package/cli/lib/argv.test.mjs +46 -0
  16. package/cli/lib/copy-tree.mjs +78 -0
  17. package/package.json +94 -0
  18. package/plugins/design/dev-server/annotations-context-toolbar.tsx +397 -0
  19. package/plugins/design/dev-server/annotations-layer.tsx +1717 -0
  20. package/plugins/design/dev-server/api.ts +674 -0
  21. package/plugins/design/dev-server/bin/_screenshot-playwright.mjs +50 -0
  22. package/plugins/design/dev-server/bin/bootstrap-check.sh +83 -0
  23. package/plugins/design/dev-server/bin/canvas-edit.sh +48 -0
  24. package/plugins/design/dev-server/bin/handoff.sh +27 -0
  25. package/plugins/design/dev-server/bin/screenshot.sh +232 -0
  26. package/plugins/design/dev-server/bin/server-up.sh +135 -0
  27. package/plugins/design/dev-server/bin/slug.sh +22 -0
  28. package/plugins/design/dev-server/bin/smoke.sh +272 -0
  29. package/plugins/design/dev-server/build.ts +267 -0
  30. package/plugins/design/dev-server/canvas-build.ts +219 -0
  31. package/plugins/design/dev-server/canvas-edit.ts +388 -0
  32. package/plugins/design/dev-server/canvas-header.ts +165 -0
  33. package/plugins/design/dev-server/canvas-icons.tsx +131 -0
  34. package/plugins/design/dev-server/canvas-lib-inline.ts +260 -0
  35. package/plugins/design/dev-server/canvas-lib-resolver.ts +85 -0
  36. package/plugins/design/dev-server/canvas-lib.tsx +1995 -0
  37. package/plugins/design/dev-server/canvas-meta.schema.json +181 -0
  38. package/plugins/design/dev-server/canvas-pipeline.ts +270 -0
  39. package/plugins/design/dev-server/canvas-shell.tsx +813 -0
  40. package/plugins/design/dev-server/client/app.jsx +2027 -0
  41. package/plugins/design/dev-server/client/hmr.mjs +85 -0
  42. package/plugins/design/dev-server/client/iframe-lazy.mjs +121 -0
  43. package/plugins/design/dev-server/client/index.html +15 -0
  44. package/plugins/design/dev-server/client/styles/0-reset.css +18 -0
  45. package/plugins/design/dev-server/client/styles/1-tokens.css +297 -0
  46. package/plugins/design/dev-server/client/styles/2-layout.css +35 -0
  47. package/plugins/design/dev-server/client/styles/3-shell.css +906 -0
  48. package/plugins/design/dev-server/client/styles/4-components.css +1268 -0
  49. package/plugins/design/dev-server/client/styles/5-utilities.css +4 -0
  50. package/plugins/design/dev-server/client/styles/_index.css +24 -0
  51. package/plugins/design/dev-server/client/styles.css +1419 -0
  52. package/plugins/design/dev-server/config.schema.json +147 -0
  53. package/plugins/design/dev-server/context-menu.tsx +343 -0
  54. package/plugins/design/dev-server/context.ts +173 -0
  55. package/plugins/design/dev-server/dist/client.bundle.js +20323 -0
  56. package/plugins/design/dev-server/dist/styles.css +2875 -0
  57. package/plugins/design/dev-server/examples/README.md +9 -0
  58. package/plugins/design/dev-server/examples/perf-100-artboards.tsx +113 -0
  59. package/plugins/design/dev-server/fs-watch.ts +63 -0
  60. package/plugins/design/dev-server/handoff.ts +721 -0
  61. package/plugins/design/dev-server/history.ts +125 -0
  62. package/plugins/design/dev-server/hmr-broadcast.ts +114 -0
  63. package/plugins/design/dev-server/http.ts +413 -0
  64. package/plugins/design/dev-server/input-router.tsx +485 -0
  65. package/plugins/design/dev-server/inspect.ts +365 -0
  66. package/plugins/design/dev-server/locator.ts +159 -0
  67. package/plugins/design/dev-server/mem.ts +97 -0
  68. package/plugins/design/dev-server/runtime-bundle.ts +235 -0
  69. package/plugins/design/dev-server/server.mjs +1246 -0
  70. package/plugins/design/dev-server/server.ts +131 -0
  71. package/plugins/design/dev-server/test/_helpers.ts +81 -0
  72. package/plugins/design/dev-server/test/active-state.test.ts +145 -0
  73. package/plugins/design/dev-server/test/annotations-api.test.ts +146 -0
  74. package/plugins/design/dev-server/test/annotations-layer.test.ts +419 -0
  75. package/plugins/design/dev-server/test/binary-smoke.test.ts +47 -0
  76. package/plugins/design/dev-server/test/bundle-smoke.test.ts +29 -0
  77. package/plugins/design/dev-server/test/canvas-build.test.ts +78 -0
  78. package/plugins/design/dev-server/test/canvas-edit.test.ts +139 -0
  79. package/plugins/design/dev-server/test/canvas-header.test.ts +127 -0
  80. package/plugins/design/dev-server/test/canvas-lib-inline.test.ts +146 -0
  81. package/plugins/design/dev-server/test/canvas-lib-resolver.test.ts +112 -0
  82. package/plugins/design/dev-server/test/canvas-meta-api.test.ts +236 -0
  83. package/plugins/design/dev-server/test/canvas-pipeline.test.ts +180 -0
  84. package/plugins/design/dev-server/test/canvas-route.test.ts +176 -0
  85. package/plugins/design/dev-server/test/fs-watch.test.ts +41 -0
  86. package/plugins/design/dev-server/test/handoff-static-frames.test.ts +215 -0
  87. package/plugins/design/dev-server/test/handoff.test.ts +281 -0
  88. package/plugins/design/dev-server/test/history-rollback.test.ts +62 -0
  89. package/plugins/design/dev-server/test/hmr-broadcast.test.ts +108 -0
  90. package/plugins/design/dev-server/test/input-router.test.ts +316 -0
  91. package/plugins/design/dev-server/test/locator.test.ts +214 -0
  92. package/plugins/design/dev-server/test/perf-harness.ts +193 -0
  93. package/plugins/design/dev-server/test/phase-3.6-smoke.test.ts +77 -0
  94. package/plugins/design/dev-server/test/runtime-bundle.test.ts +69 -0
  95. package/plugins/design/dev-server/test/server-lifecycle.test.ts +28 -0
  96. package/plugins/design/dev-server/test/tool-palette.test.tsx +55 -0
  97. package/plugins/design/dev-server/test/use-annotation-selection.test.tsx +77 -0
  98. package/plugins/design/dev-server/test/use-artboard-drag.test.ts +325 -0
  99. package/plugins/design/dev-server/test/use-selection-set.test.tsx +166 -0
  100. package/plugins/design/dev-server/test/use-snap-guides.test.ts +190 -0
  101. package/plugins/design/dev-server/test/use-tool-mode.test.tsx +93 -0
  102. package/plugins/design/dev-server/test/ws-handshake.test.ts +33 -0
  103. package/plugins/design/dev-server/tool-palette.tsx +278 -0
  104. package/plugins/design/dev-server/tsconfig.json +26 -0
  105. package/plugins/design/dev-server/use-annotation-selection.tsx +92 -0
  106. package/plugins/design/dev-server/use-annotations-visibility.tsx +43 -0
  107. package/plugins/design/dev-server/use-artboard-drag.tsx +445 -0
  108. package/plugins/design/dev-server/use-selection-set.tsx +224 -0
  109. package/plugins/design/dev-server/use-snap-guides.tsx +215 -0
  110. package/plugins/design/dev-server/use-tool-mode.tsx +114 -0
  111. package/plugins/design/dev-server/ws.ts +90 -0
  112. package/plugins/design/templates/_shell.html +177 -0
  113. package/plugins/design/templates/canvas.tsx.template +54 -0
  114. package/plugins/design/templates/design-system-inspiration/_MAPPING.md +277 -0
  115. package/plugins/design/templates/design-system-inspiration/_README.md +71 -0
  116. package/plugins/design/templates/design-system-inspiration/audience-consumer/components-banner.html +68 -0
  117. package/plugins/design/templates/design-system-inspiration/audience-consumer/components-empty-state-generous.html +39 -0
  118. package/plugins/design/templates/design-system-inspiration/audience-consumer/components-feature-grid.html +62 -0
  119. package/plugins/design/templates/design-system-inspiration/audience-consumer/components-marketing-card.html +63 -0
  120. package/plugins/design/templates/design-system-inspiration/audience-consumer/components-testimonial.html +80 -0
  121. package/plugins/design/templates/design-system-inspiration/audience-developer/components-code-block.html +71 -0
  122. package/plugins/design/templates/design-system-inspiration/audience-developer/components-diff-view.html +65 -0
  123. package/plugins/design/templates/design-system-inspiration/audience-developer/components-log-stream.html +62 -0
  124. package/plugins/design/templates/design-system-inspiration/audience-developer/components-monospace-table.html +57 -0
  125. package/plugins/design/templates/design-system-inspiration/audience-developer/components-terminal-pane.html +67 -0
  126. package/plugins/design/templates/design-system-inspiration/audience-developer/type-mono.html +57 -0
  127. package/plugins/design/templates/design-system-inspiration/audience-pro/colors-presence.html +74 -0
  128. package/plugins/design/templates/design-system-inspiration/audience-pro/components-command-palette.html +90 -0
  129. package/plugins/design/templates/design-system-inspiration/audience-pro/components-keyboard.html +51 -0
  130. package/plugins/design/templates/design-system-inspiration/audience-pro/components-list.html +89 -0
  131. package/plugins/design/templates/design-system-inspiration/audience-pro/components-shortcuts-overlay.html +85 -0
  132. package/plugins/design/templates/design-system-inspiration/audience-pro/components-toast-menu.html +80 -0
  133. package/plugins/design/templates/design-system-inspiration/core/INDEX.md.tpl +25 -0
  134. package/plugins/design/templates/design-system-inspiration/core/README.orchestration.md.tpl +71 -0
  135. package/plugins/design/templates/design-system-inspiration/core/README.philosophy.md.tpl +77 -0
  136. package/plugins/design/templates/design-system-inspiration/core/SKILL.md.tpl +50 -0
  137. package/plugins/design/templates/design-system-inspiration/core/colors_and_type.css.tpl +111 -0
  138. package/plugins/design/templates/design-system-inspiration/core/config.json.tpl +28 -0
  139. package/plugins/design/templates/design-system-inspiration/core/preview/_layout.css +113 -0
  140. package/plugins/design/templates/design-system-inspiration/core/preview/colors-accent.html +48 -0
  141. package/plugins/design/templates/design-system-inspiration/core/preview/colors-surfaces.html +49 -0
  142. package/plugins/design/templates/design-system-inspiration/core/preview/colors-text.html +52 -0
  143. package/plugins/design/templates/design-system-inspiration/core/preview/components-buttons.html +83 -0
  144. package/plugins/design/templates/design-system-inspiration/core/preview/components-cards.html +79 -0
  145. package/plugins/design/templates/design-system-inspiration/core/preview/components-inputs.html +82 -0
  146. package/plugins/design/templates/design-system-inspiration/core/preview/motion.html +66 -0
  147. package/plugins/design/templates/design-system-inspiration/core/preview/spacing-scale.html +53 -0
  148. package/plugins/design/templates/design-system-inspiration/core/preview/type-scale.html +62 -0
  149. package/plugins/design/templates/design-system-inspiration/foundations/borders.html +39 -0
  150. package/plugins/design/templates/design-system-inspiration/foundations/elevation.html +46 -0
  151. package/plugins/design/templates/design-system-inspiration/foundations/focus.html +48 -0
  152. package/plugins/design/templates/design-system-inspiration/foundations/grid.html +51 -0
  153. package/plugins/design/templates/design-system-inspiration/foundations/iconography.html +73 -0
  154. package/plugins/design/templates/design-system-inspiration/foundations/opacity.html +45 -0
  155. package/plugins/design/templates/design-system-inspiration/foundations/radii.html +40 -0
  156. package/plugins/design/templates/design-system-inspiration/foundations/selection.html +45 -0
  157. package/plugins/design/templates/design-system-inspiration/meta/accessibility.html +62 -0
  158. package/plugins/design/templates/design-system-inspiration/meta/i18n.html +73 -0
  159. package/plugins/design/templates/design-system-inspiration/meta/presence-multiplayer.html +71 -0
  160. package/plugins/design/templates/design-system-inspiration/meta/tokens-index.html +80 -0
  161. package/plugins/design/templates/design-system-inspiration/patterns/patterns-auth.html +78 -0
  162. package/plugins/design/templates/design-system-inspiration/patterns/patterns-data-density.html +61 -0
  163. package/plugins/design/templates/design-system-inspiration/patterns/patterns-error-pages.html +70 -0
  164. package/plugins/design/templates/design-system-inspiration/patterns/patterns-form-layouts.html +70 -0
  165. package/plugins/design/templates/design-system-inspiration/patterns/patterns-onboarding.html +71 -0
  166. package/plugins/design/templates/design-system-inspiration/patterns/patterns-pricing.html +83 -0
  167. package/plugins/design/templates/design-system-inspiration/platform-desktop/components-resize-panels.html +63 -0
  168. package/plugins/design/templates/design-system-inspiration/platform-desktop/ui_kits-desktop-index.html +55 -0
  169. package/plugins/design/templates/design-system-inspiration/platform-desktop/ui_kits-desktop-showcase.html +302 -0
  170. package/plugins/design/templates/design-system-inspiration/platform-mobile/components-bottom-sheet.html +63 -0
  171. package/plugins/design/templates/design-system-inspiration/platform-mobile/components-pull-to-refresh.html +74 -0
  172. package/plugins/design/templates/design-system-inspiration/platform-mobile/components-segmented-control.html +51 -0
  173. package/plugins/design/templates/design-system-inspiration/platform-mobile/components-tab-bar.html +57 -0
  174. package/plugins/design/templates/design-system-inspiration/platform-mobile/ui_kits-mobile-index.html +58 -0
  175. package/plugins/design/templates/design-system-inspiration/platform-mobile/ui_kits-mobile-showcase.html +237 -0
  176. package/plugins/design/templates/design-system-inspiration/status/colors-status.html +49 -0
  177. package/plugins/design/templates/design-system-inspiration/status/components-status.html +63 -0
  178. package/plugins/design/templates/design-system-inspiration/status/skeletons.html +74 -0
  179. package/plugins/design/templates/design-system-inspiration/theme-both/colors-themes-side-by-side.html +59 -0
  180. package/plugins/design/templates/design-system-inspiration/universal/components-callout.html +74 -0
  181. package/plugins/design/templates/design-system-inspiration/universal/components-dialogs.html +81 -0
  182. package/plugins/design/templates/design-system-inspiration/universal/components-tables.html +101 -0
  183. package/plugins/design/templates/design-system-inspiration/universal/components-toggles.html +74 -0
  184. package/plugins/design/templates/design-system-inspiration/universal/components-tooltips.html +74 -0
  185. package/plugins/design/templates/design-system-inspiration/universal/empty-state.html.tpl +34 -0
  186. package/plugins/design/templates/design-system-inspiration/universal/logo.html +42 -0
  187. package/plugins/design/templates/ds-specimen.tsx.template +59 -0
  188. package/plugins/flow/.claude-plugin/config.schema.json +398 -0
  189. package/plugins/flow/README.md +45 -0
  190. package/plugins/flow/templates/ai-skeleton/INDEX.md +50 -0
  191. package/plugins/flow/templates/ai-skeleton/README.md +46 -0
  192. package/plugins/flow/templates/ai-skeleton/browser/har/.gitkeep +0 -0
  193. package/plugins/flow/templates/ai-skeleton/browser/snapshots/.gitkeep +0 -0
  194. package/plugins/flow/templates/ai-skeleton/business/README.md +12 -0
  195. package/plugins/flow/templates/ai-skeleton/context/README.md +12 -0
  196. package/plugins/flow/templates/ai-skeleton/decisions/README.md +20 -0
  197. package/plugins/flow/templates/ai-skeleton/design-import/.gitkeep +0 -0
  198. package/plugins/flow/templates/ai-skeleton/dev-logs/.gitkeep +0 -0
  199. package/plugins/flow/templates/ai-skeleton/device/.gitkeep +0 -0
  200. package/plugins/flow/templates/ai-skeleton/docs/README.md +5 -0
  201. package/plugins/flow/templates/ai-skeleton/logs/.gitkeep +0 -0
  202. package/plugins/flow/templates/ai-skeleton/logs/README.md +13 -0
  203. package/plugins/flow/templates/ai-skeleton/plans/README.md +16 -0
  204. package/plugins/flow/templates/ai-skeleton/plans/archive/.gitkeep +0 -0
  205. package/plugins/flow/templates/ai-skeleton/release-guide.md +35 -0
  206. package/plugins/flow/templates/ai-skeleton/reviews/README.md +15 -0
  207. package/plugins/flow/templates/ai-skeleton/scenarios/README.md +26 -0
  208. package/plugins/flow/templates/ai-skeleton/scenarios/_lib/.gitkeep +0 -0
  209. package/plugins/flow/templates/ai-skeleton/state/STATE.md +25 -0
  210. package/plugins/flow/templates/ai-skeleton/templates/HANDOFF.md +32 -0
  211. package/plugins/flow/templates/ai-skeleton/workflows.config.json +74 -0
@@ -0,0 +1,721 @@
1
+ // /design:handoff emitter — shadcn registry-item.json sidecar (Phase 3.6 Task 7 + 12b).
2
+ //
3
+ // Walks a canvas TSX file, classifies its imports, strips `data-cd-id` attrs
4
+ // (those are dev-time scaffolding the production drop has no business with),
5
+ // optionally bundles the actually-used subset of `_components.css` +
6
+ // `colors_and_type.css`, and emits a JSON sidecar conforming to
7
+ // https://ui.shadcn.com/schema/registry-item.json.
8
+ //
9
+ // Consumer pattern:
10
+ // bunx shadcn add file://./<Slug>.registry.json
11
+ //
12
+ // Same oxc-parser + magic-string + lightningcss toolchain as canvas-pipeline.ts
13
+ // and canvas-edit.ts — one mental model, three call sites.
14
+ //
15
+ // Strip semantics (Task 12):
16
+ // - data-cd-id → removed (pipeline-emitted; not source)
17
+ // - JSDoc header → kept (cold-read context for future Claude / human reader)
18
+ // - inline DesignCanvas/DCSection/DCArtboard chrome → kept as-is; the
19
+ // consumer's host page wraps or unwraps as needed.
20
+ //
21
+ // CSS bundling (Task 12b):
22
+ // - Scan canvas TSX for every string literal that appears as a `className`
23
+ // attribute value. Tokenise on whitespace → set of class names.
24
+ // - Parse `_components.css` with lightningcss; keep rules whose selector list
25
+ // contains any selector whose `base class` (the first class segment, after
26
+ // stripping pseudo + descendant tail) is in the set.
27
+ // - Walk the kept CSS for `var(--name)` references, intersect with
28
+ // `colors_and_type.css`, emit only the matched custom properties under
29
+ // cssVars.theme (the schema's first-class theme-token slot).
30
+
31
+ import path from 'node:path';
32
+
33
+ import MagicString from 'magic-string';
34
+ import { parseSync } from 'oxc-parser';
35
+
36
+ import { buildLibMap, inlineUsedExports } from './canvas-lib-inline.ts';
37
+ import { canvasLibPath } from './canvas-lib-resolver.ts';
38
+
39
+ // biome-ignore lint/suspicious/noExplicitAny: oxc AST nodes are heterogeneous.
40
+ type AnyNode = any;
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Public types — shaped to match shadcn's registry-item.json schema. Reference:
44
+ // https://ui.shadcn.com/schema/registry-item.json
45
+ // We don't ship every optional field — only the ones we can produce reliably.
46
+
47
+ export interface RegistryItemFile {
48
+ /** Relative path the file should land at in the consumer project. */
49
+ path: string;
50
+ /** File contents. */
51
+ content: string;
52
+ /** shadcn file-type discriminator. */
53
+ type:
54
+ | 'registry:component'
55
+ | 'registry:block'
56
+ | 'registry:ui'
57
+ | 'registry:style'
58
+ | 'registry:lib'
59
+ | 'registry:hook'
60
+ | 'registry:theme';
61
+ /** Optional override for where the file lands in the consumer project. */
62
+ target?: string;
63
+ }
64
+
65
+ export interface RegistryItem {
66
+ $schema: string;
67
+ name: string;
68
+ type: 'registry:block' | 'registry:component' | 'registry:ui';
69
+ title?: string;
70
+ description?: string;
71
+ /** npm package specifiers (e.g. ["react", "lucide-react"]). */
72
+ dependencies: string[];
73
+ /** Other registry items this depends on (e.g. ["button", "card"]). */
74
+ registryDependencies: string[];
75
+ /** Files to drop. Index 0 is conventionally the entry component. */
76
+ files: RegistryItemFile[];
77
+ /** CSS custom properties grouped by theme (light/dark). Empty when no tokens used. */
78
+ cssVars?: {
79
+ theme?: Record<string, string>;
80
+ light?: Record<string, string>;
81
+ dark?: Record<string, string>;
82
+ };
83
+ }
84
+
85
+ export interface EmitOptions {
86
+ /** Absolute path to canvas .tsx file. */
87
+ canvasAbsPath: string;
88
+ /** Pretty title from meta.json (becomes registry item `title`). */
89
+ title?: string;
90
+ /** One-line description from meta.json.subtitle (becomes `description`). */
91
+ description?: string;
92
+ /** Optional path to project's `_components.css` for CSS bundling (Task 12b). */
93
+ componentsCssPath?: string;
94
+ /** Optional path to project's tokens CSS for cssVars resolution (Task 12b). */
95
+ tokensCssPath?: string;
96
+ /**
97
+ * Absolute path to design root. When provided, `@maude/canvas-lib` imports
98
+ * in the canvas are inlined from the dev-server-bundled canvas-lib so the
99
+ * emitted drop is self-contained (Phase 3.6.1 Task 9; per DDR-025 the lib
100
+ * lives in the dev-server, not under designRoot). The argument is kept for
101
+ * back-compat with the CLI shape — handoff inlining no longer reads it.
102
+ */
103
+ designRoot?: string;
104
+ }
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Strip data-cd-id from source — the inverse of canvas-pipeline.ts pass 1.
108
+
109
+ /**
110
+ * Remove every ` data-cd-id="<hex>"` attribute from a TSX source string.
111
+ * Pure: caller persists. Uses the same oxc-parser + magic-string toolchain as
112
+ * the pipeline that emitted them.
113
+ */
114
+ export function stripDataCdId(canvasAbsPath: string, source: string): string {
115
+ const parsed = parseSync(canvasAbsPath, source, { sourceType: 'module' });
116
+ if (parsed.errors && parsed.errors.length > 0) {
117
+ const first = parsed.errors[0];
118
+ throw new Error(
119
+ `oxc-parser failed on ${canvasAbsPath} (${parsed.errors.length} errors). First: ${first?.message ?? 'unknown'}`
120
+ );
121
+ }
122
+ const s = new MagicString(source);
123
+
124
+ function visit(node: AnyNode): void {
125
+ if (!node || typeof node !== 'object') return;
126
+ if (Array.isArray(node)) {
127
+ for (const c of node) visit(c);
128
+ return;
129
+ }
130
+ if (typeof node.type !== 'string') return;
131
+
132
+ if (node.type === 'JSXOpeningElement') {
133
+ const attrs = node.attributes as AnyNode[] | undefined;
134
+ if (Array.isArray(attrs)) {
135
+ for (const a of attrs) {
136
+ if (
137
+ a?.type === 'JSXAttribute' &&
138
+ a.name?.type === 'JSXIdentifier' &&
139
+ a.name.name === 'data-cd-id' &&
140
+ typeof a.start === 'number' &&
141
+ typeof a.end === 'number'
142
+ ) {
143
+ // Trim the leading whitespace too — author-friendly output.
144
+ let from = a.start as number;
145
+ while (from > 0 && /\s/.test(source[from - 1] as string)) from--;
146
+ s.remove(from, a.end);
147
+ }
148
+ }
149
+ }
150
+ }
151
+
152
+ for (const k of Object.keys(node)) {
153
+ if (k === 'loc' || k === 'range' || k === 'start' || k === 'end' || k === 'type') continue;
154
+ visit(node[k]);
155
+ }
156
+ }
157
+
158
+ visit(parsed.program);
159
+ return s.toString();
160
+ }
161
+
162
+ // ---------------------------------------------------------------------------
163
+ // Import classification — npm specifier vs shadcn `@/components/ui/*`.
164
+
165
+ interface ClassifiedImports {
166
+ /** npm package names, deduped. */
167
+ dependencies: string[];
168
+ /** shadcn primitive names (e.g. `button` from `@/components/ui/button`). */
169
+ registryDependencies: string[];
170
+ }
171
+
172
+ /**
173
+ * Classify import specifiers in a TSX source. Heuristic:
174
+ * - `@/components/ui/<name>` → registry dependency `<name>`
175
+ * - bare specifier starting with letter / `@` not in the above pattern
176
+ * → npm dependency (the package portion)
177
+ * - relative imports (`./...`, `../...`) → ignored (consumer ships its own)
178
+ */
179
+ export function classifyImports(canvasAbsPath: string, source: string): ClassifiedImports {
180
+ const deps = new Set<string>();
181
+ const regDeps = new Set<string>();
182
+
183
+ // Bun.Transpiler.scanImports() is the documented fast path; pulls every
184
+ // ImportDeclaration / dynamic import / require call.
185
+ const scanner = new Bun.Transpiler({ loader: 'tsx' });
186
+ const imports = scanner.scanImports(source);
187
+ for (const imp of imports) {
188
+ const spec = imp.path;
189
+ if (!spec) continue;
190
+ if (spec.startsWith('./') || spec.startsWith('../') || spec.startsWith('/')) continue;
191
+ if (spec.startsWith('@/components/ui/')) {
192
+ const name = spec.slice('@/components/ui/'.length).split('/')[0];
193
+ if (name) regDeps.add(name);
194
+ continue;
195
+ }
196
+ deps.add(packageNameOf(spec));
197
+ }
198
+
199
+ void canvasAbsPath;
200
+ return {
201
+ dependencies: [...deps].sort(),
202
+ registryDependencies: [...regDeps].sort(),
203
+ };
204
+ }
205
+
206
+ /**
207
+ * Extract the npm package name from a bare specifier. Handles scoped packages
208
+ * (`@scope/name`) and subpath imports (`react-dom/client` → `react-dom`).
209
+ */
210
+ function packageNameOf(spec: string): string {
211
+ if (spec.startsWith('@')) {
212
+ const parts = spec.split('/');
213
+ return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : spec;
214
+ }
215
+ const slash = spec.indexOf('/');
216
+ return slash > 0 ? spec.slice(0, slash) : spec;
217
+ }
218
+
219
+ // ---------------------------------------------------------------------------
220
+ // className harvester — collect every class name string appearing as a JSX
221
+ // className attribute value (`className="a b"` or `className={'a b'}`).
222
+
223
+ export function collectClassNames(canvasAbsPath: string, source: string): Set<string> {
224
+ const parsed = parseSync(canvasAbsPath, source, { sourceType: 'module' });
225
+ const out = new Set<string>();
226
+
227
+ function pushTokens(str: string): void {
228
+ for (const tok of str.split(/\s+/)) {
229
+ if (tok) out.add(tok);
230
+ }
231
+ }
232
+
233
+ function visit(node: AnyNode): void {
234
+ if (!node || typeof node !== 'object') return;
235
+ if (Array.isArray(node)) {
236
+ for (const c of node) visit(c);
237
+ return;
238
+ }
239
+ if (typeof node.type !== 'string') return;
240
+
241
+ if (
242
+ node.type === 'JSXAttribute' &&
243
+ node.name?.type === 'JSXIdentifier' &&
244
+ node.name.name === 'className'
245
+ ) {
246
+ // Walk the value subtree and harvest every string-literal / template
247
+ // quasi we find. Covers literal, JSXExpressionContainer(Literal),
248
+ // TemplateLiteral, BinaryExpression of literals, conditional with
249
+ // literal branches, clsx/cn calls with literal args, etc.
250
+ harvestStrings(node.value, pushTokens);
251
+ return;
252
+ }
253
+
254
+ for (const k of Object.keys(node)) {
255
+ if (k === 'loc' || k === 'range' || k === 'start' || k === 'end' || k === 'type') continue;
256
+ visit(node[k]);
257
+ }
258
+ }
259
+
260
+ visit(parsed.program);
261
+ return out;
262
+ }
263
+
264
+ function harvestStrings(node: AnyNode, sink: (s: string) => void): void {
265
+ if (!node || typeof node !== 'object') return;
266
+ if (Array.isArray(node)) {
267
+ for (const c of node) harvestStrings(c, sink);
268
+ return;
269
+ }
270
+ const t = node.type;
271
+ if (typeof t !== 'string') return;
272
+ if (t === 'Literal' || t === 'StringLiteral') {
273
+ if (typeof node.value === 'string') sink(node.value);
274
+ return;
275
+ }
276
+ if (t === 'TemplateLiteral') {
277
+ for (const q of node.quasis ?? []) {
278
+ const raw = q?.value?.cooked ?? q?.value?.raw ?? '';
279
+ if (typeof raw === 'string') sink(raw);
280
+ }
281
+ for (const e of node.expressions ?? []) harvestStrings(e, sink);
282
+ return;
283
+ }
284
+ for (const k of Object.keys(node)) {
285
+ if (k === 'loc' || k === 'range' || k === 'start' || k === 'end' || k === 'type') continue;
286
+ harvestStrings(node[k], sink);
287
+ }
288
+ }
289
+
290
+ // ---------------------------------------------------------------------------
291
+ // CSS subset extraction — keep rules whose base class is in the harvested set,
292
+ // plus the var(--*) references inside those rules.
293
+
294
+ interface CssBundleResult {
295
+ /** CSS source containing only used rules. May be empty string. */
296
+ css: string;
297
+ /** Names of CSS custom properties referenced by the kept rules. */
298
+ tokens: Set<string>;
299
+ }
300
+
301
+ /**
302
+ * Filter a CSS file to only the rules whose first class selector is in `keep`.
303
+ * Naive but reliable parser: walks top-level rule blocks via balanced braces.
304
+ * Avoids pulling lightningcss into the visitor path for the v1 of this feature.
305
+ */
306
+ export function filterComponentsCss(cssSource: string, keep: Set<string>): CssBundleResult {
307
+ const out: string[] = [];
308
+ const tokens = new Set<string>();
309
+
310
+ let i = 0;
311
+ const n = cssSource.length;
312
+ while (i < n) {
313
+ // Skip whitespace.
314
+ while (i < n && /\s/.test(cssSource[i] as string)) i++;
315
+ if (i >= n) break;
316
+
317
+ // Handle comments at top level — preserve them if attached to a kept rule;
318
+ // for simplicity, drop them all (they're documentation, not necessary).
319
+ if (cssSource[i] === '/' && cssSource[i + 1] === '*') {
320
+ const end = cssSource.indexOf('*/', i + 2);
321
+ i = end < 0 ? n : end + 2;
322
+ continue;
323
+ }
324
+
325
+ // @-rules: @media { ... }, @keyframes { ... }, etc. Recurse into @media,
326
+ // emit @keyframes wholesale if any class inside its body is referenced
327
+ // (animations are class-attached; we keep them when their owning class
328
+ // survives). Simpler v1: keep every @-rule whose body, when filtered, has
329
+ // content; emit the filtered body.
330
+ if (cssSource[i] === '@') {
331
+ const ruleStart = i;
332
+ // Find prelude end at next `{` or `;`.
333
+ let j = i;
334
+ while (j < n && cssSource[j] !== '{' && cssSource[j] !== ';') j++;
335
+ if (j >= n) break;
336
+ if (cssSource[j] === ';') {
337
+ // Naked @import / @charset / @namespace — drop.
338
+ i = j + 1;
339
+ continue;
340
+ }
341
+ // Body starts at j. Scan to matching `}`.
342
+ const bodyEnd = matchBrace(cssSource, j);
343
+ if (bodyEnd < 0) {
344
+ i = n;
345
+ continue;
346
+ }
347
+ const prelude = cssSource.slice(ruleStart, j).trim();
348
+ const body = cssSource.slice(j + 1, bodyEnd);
349
+ const isKeyframes = /^@(-\w+-)?keyframes\b/.test(prelude);
350
+ if (isKeyframes) {
351
+ // Keep keyframes wholesale if its name shows up via animation refs in
352
+ // the kept ruleset. For v1 we keep all @keyframes; size impact is
353
+ // small (canvases rarely define many).
354
+ const block = cssSource.slice(ruleStart, bodyEnd + 1);
355
+ out.push(block);
356
+ collectVars(block, tokens);
357
+ i = bodyEnd + 1;
358
+ continue;
359
+ }
360
+ // Recurse — filter inside @media.
361
+ const inner = filterComponentsCss(body, keep);
362
+ if (inner.css.trim().length > 0) {
363
+ out.push(`${prelude} {\n${inner.css.trimEnd()}\n}`);
364
+ for (const t of inner.tokens) tokens.add(t);
365
+ }
366
+ i = bodyEnd + 1;
367
+ continue;
368
+ }
369
+
370
+ // Selector rule: read prelude up to `{`.
371
+ const ruleStart = i;
372
+ let j = i;
373
+ while (j < n && cssSource[j] !== '{') j++;
374
+ if (j >= n) break;
375
+ const bodyEnd = matchBrace(cssSource, j);
376
+ if (bodyEnd < 0) break;
377
+ const prelude = cssSource.slice(ruleStart, j).trim();
378
+ if (selectorListIntersects(prelude, keep)) {
379
+ const block = cssSource.slice(ruleStart, bodyEnd + 1);
380
+ out.push(block);
381
+ collectVars(block, tokens);
382
+ }
383
+ i = bodyEnd + 1;
384
+ }
385
+
386
+ return { css: out.join('\n\n'), tokens };
387
+ }
388
+
389
+ function matchBrace(s: string, openIdx: number): number {
390
+ let depth = 0;
391
+ for (let i = openIdx; i < s.length; i++) {
392
+ const c = s[i];
393
+ if (c === '{') depth++;
394
+ else if (c === '}') {
395
+ depth--;
396
+ if (depth === 0) return i;
397
+ }
398
+ }
399
+ return -1;
400
+ }
401
+
402
+ /**
403
+ * True if the comma-separated selector list contains any selector whose
404
+ * first class segment is in `keep`. Splits on commas at depth 0 (won't fail on
405
+ * `:is(.a,.b)` and similar but stays naive for v1 — the canvases this serves
406
+ * don't use complex CSSWG selectors).
407
+ */
408
+ function selectorListIntersects(prelude: string, keep: Set<string>): boolean {
409
+ for (const sel of prelude.split(',')) {
410
+ const cls = firstClass(sel.trim());
411
+ if (!cls) continue;
412
+ // Direct hit, or BEM-base hit (.btn--ghost survives when `btn` is kept,
413
+ // .card__title survives when `card` is kept). Matches author intent —
414
+ // canvases that already opt into BEM modifiers expect the whole family to
415
+ // travel with the base class.
416
+ if (keep.has(cls)) return true;
417
+ const bem = cls.split(/(?:--|__)/, 1)[0];
418
+ if (bem && bem !== cls && keep.has(bem)) return true;
419
+ }
420
+ return false;
421
+ }
422
+
423
+ function firstClass(selector: string): string | null {
424
+ // Scan for `.name` — pick the first one. Stops at descendant combinators.
425
+ const m = selector.match(/\.([A-Za-z_-][A-Za-z0-9_-]*)/);
426
+ return m?.[1] ? m[1] : null;
427
+ }
428
+
429
+ function collectVars(css: string, tokens: Set<string>): void {
430
+ for (const m of css.matchAll(/var\(\s*(--[A-Za-z0-9_-]+)/g)) {
431
+ if (m[1]) tokens.add(m[1]);
432
+ }
433
+ }
434
+
435
+ // ---------------------------------------------------------------------------
436
+ // Token resolution — read `colors_and_type.css`, pluck only the custom
437
+ // properties listed in `tokens`. Output is keyed by token-without-`--`.
438
+
439
+ export function filterTokensCss(
440
+ cssSource: string,
441
+ tokens: Set<string>
442
+ ): { theme: Record<string, string>; usedCss: string } {
443
+ const theme: Record<string, string> = {};
444
+ // The expectation here is that the tokens CSS declares each `--foo: value;`
445
+ // inside `:root` or theme-scoped blocks. We keep things simple: regex over
446
+ // top-level `--name: value;` declarations. Multiple themes get merged into
447
+ // `theme` (consumer can split later); a richer light/dark scheme is a
448
+ // later iteration.
449
+ const re = /(--[A-Za-z0-9_-]+)\s*:\s*([^;]+);/g;
450
+ const usedDeclarations: string[] = [];
451
+ let m: RegExpExecArray | null = re.exec(cssSource);
452
+ while (m !== null) {
453
+ const name = m[1] as string;
454
+ const value = (m[2] as string).trim();
455
+ if (tokens.has(name)) {
456
+ theme[name.slice(2)] = value;
457
+ usedDeclarations.push(`${name}: ${value};`);
458
+ }
459
+ m = re.exec(cssSource);
460
+ }
461
+ // Build a minimal :root usedCss block — useful when the consumer wants the
462
+ // raw declarations rather than the shadcn cssVars sugar.
463
+ const usedCss =
464
+ usedDeclarations.length > 0 ? `:root {\n ${usedDeclarations.join('\n ')}\n}\n` : '';
465
+ return { theme, usedCss };
466
+ }
467
+
468
+ // ---------------------------------------------------------------------------
469
+ // Phase 4 T7 — handoff-static frame overrides.
470
+ //
471
+ // Dev-time canvas-lib carries the full infinite-canvas engine: DesignCanvas
472
+ // runs `useViewportController`, mounts `DCMiniMap` + `DCZoomToolbar`, walks
473
+ // children via `harvestArtboards`, etc. None of that belongs in a shadcn
474
+ // registry item — the consumer of a handed-off canvas wants the design as
475
+ // rendered, not the authoring engine.
476
+ //
477
+ // `applyHandoffStaticOverrides` rewrites the three frame functions in the
478
+ // libMap to minimal static variants with empty `deps`. When `inlineUsedExports`
479
+ // then BFS-resolves what the user canvas imports (`DesignCanvas`, `DCSection`,
480
+ // `DCArtboard`), it finds these stub bodies and never reaches the engine code
481
+ // (`useViewportController`, `DCMiniMap`, `DCZoomToolbar`, `WorldContext`,
482
+ // `harvestArtboards`, `synthDefaultGrid`, `computeFit`, ...).
483
+ //
484
+ // The static frames intentionally mirror the standalone-mode rendering branch
485
+ // of the dev-time components — same DOM, same classes, same data attributes —
486
+ // so the DS's `_components.css` rules still apply 1:1.
487
+
488
+ const STATIC_DESIGN_CANVAS = `function DesignCanvas({ children }) {
489
+ return <div className="dc-canvas">{children}</div>;
490
+ }`;
491
+
492
+ const STATIC_DC_SECTION = `function DCSection({ id, title, subtitle, children }) {
493
+ return (
494
+ <section className="dc-section" data-dc-section={id}>
495
+ <header>
496
+ <h2>{title}</h2>
497
+ {subtitle ? <p className="sku">{subtitle}</p> : null}
498
+ </header>
499
+ <div className="dc-section-body">{children}</div>
500
+ </section>
501
+ );
502
+ }`;
503
+
504
+ const STATIC_DC_ARTBOARD = `function DCArtboard({ id, label, width, height, children }) {
505
+ return (
506
+ <article className="dc-artboard" data-dc-screen={id} style={{ width, height }}>
507
+ <header className="dc-artboard-label sku">{label}</header>
508
+ <div className="dc-artboard-body">{children}</div>
509
+ </article>
510
+ );
511
+ }`;
512
+
513
+ /**
514
+ * Names this routine overrides. Exported so tests can pin the list. Adding
515
+ * a new engine-bearing top-level export to canvas-lib that the canvas might
516
+ * import requires either (a) extending this map with a static variant, or
517
+ * (b) extending `inlineUsedExports`'s skip-set.
518
+ */
519
+ export const HANDOFF_STATIC_FRAME_EXPORTS = ['DesignCanvas', 'DCSection', 'DCArtboard'] as const;
520
+
521
+ export function applyHandoffStaticOverrides(
522
+ libMap: Map<string, { name: string; source: string; deps: string[] }>
523
+ ): void {
524
+ if (libMap.has('DesignCanvas')) {
525
+ libMap.set('DesignCanvas', { name: 'DesignCanvas', source: STATIC_DESIGN_CANVAS, deps: [] });
526
+ }
527
+ if (libMap.has('DCSection')) {
528
+ libMap.set('DCSection', { name: 'DCSection', source: STATIC_DC_SECTION, deps: [] });
529
+ }
530
+ if (libMap.has('DCArtboard')) {
531
+ libMap.set('DCArtboard', { name: 'DCArtboard', source: STATIC_DC_ARTBOARD, deps: [] });
532
+ }
533
+ }
534
+
535
+ // ---------------------------------------------------------------------------
536
+ // Main entry — emit the registry-item.json structure.
537
+
538
+ export async function emitRegistryItem(opts: EmitOptions): Promise<RegistryItem> {
539
+ const canvasFile = Bun.file(opts.canvasAbsPath);
540
+ if (!(await canvasFile.exists())) {
541
+ throw new Error(`Canvas not found: ${opts.canvasAbsPath}`);
542
+ }
543
+ const rawTsx = await canvasFile.text();
544
+
545
+ // Strip dev-time scaffolding.
546
+ let tsx = stripDataCdId(opts.canvasAbsPath, rawTsx);
547
+
548
+ // Inline canvas-lib helpers — when the canvas imports from @maude/canvas-lib,
549
+ // we splice the resolved exports + their transitive deps into the canvas
550
+ // source and strip the specifier. Phase 3.6.1 Task 9.
551
+ //
552
+ // Phase 4 T7 — engine exports (useViewportController, DCMiniMap,
553
+ // DCZoomToolbar, ...) MUST NOT travel into a handed-off registry item.
554
+ // The trick: replace `DesignCanvas`, `DCArtboard`, `DCSection` in the
555
+ // libMap with their static-frame variants before BFS. The static variants
556
+ // have empty deps, so the transitive walk never reaches the engine code.
557
+ if (opts.designRoot) {
558
+ const libPath = canvasLibPath(opts.designRoot);
559
+ const libFile = Bun.file(libPath);
560
+ if (await libFile.exists()) {
561
+ const libSource = await libFile.text();
562
+ const libMap = buildLibMap(libPath, libSource);
563
+ applyHandoffStaticOverrides(libMap);
564
+ const inlined = inlineUsedExports(tsx, libMap);
565
+ tsx = inlined.content;
566
+ }
567
+ }
568
+
569
+ // Classify imports.
570
+ const { dependencies, registryDependencies } = classifyImports(opts.canvasAbsPath, tsx);
571
+ // @maude/canvas-lib is a dev-time virtual specifier — never ship as dep.
572
+ const depsFiltered = dependencies.filter((d) => d !== '@maude/canvas-lib');
573
+
574
+ // React + ReactDOM always shipped as runtime deps — the canvas authoring
575
+ // contract requires React 19 (DDR-012). scanImports already finds `react`
576
+ // when JSX is present (Bun.Transpiler tracks jsx-runtime usage), but it
577
+ // doesn't surface react-dom unless explicitly imported. Force-include both.
578
+ const depSet = new Set(depsFiltered);
579
+ depSet.add('react');
580
+ depSet.add('react-dom');
581
+ const finalDeps = [...depSet].sort();
582
+
583
+ // Compute slug for `name` field — kebab-case of the file stem.
584
+ const slug = kebabCase(path.basename(opts.canvasAbsPath, path.extname(opts.canvasAbsPath)));
585
+
586
+ // Files: index 0 is the canvas TSX (always). 1+ are CSS bundles (optional —
587
+ // when componentsCssPath / tokensCssPath are passed).
588
+ const files: RegistryItemFile[] = [
589
+ {
590
+ path: `components/${slug}.tsx`,
591
+ content: tsx,
592
+ type: 'registry:component',
593
+ },
594
+ ];
595
+
596
+ let cssVars: RegistryItem['cssVars'] | undefined;
597
+
598
+ if (opts.componentsCssPath) {
599
+ const componentsCss = await Bun.file(opts.componentsCssPath)
600
+ .text()
601
+ .catch(() => '');
602
+ if (componentsCss) {
603
+ const classNames = collectClassNames(opts.canvasAbsPath, tsx);
604
+ const { css, tokens } = filterComponentsCss(componentsCss, classNames);
605
+ if (css.trim().length > 0) {
606
+ files.push({
607
+ path: `styles/${slug}.css`,
608
+ content: `${css}\n`,
609
+ type: 'registry:style',
610
+ });
611
+ }
612
+ if (opts.tokensCssPath && tokens.size > 0) {
613
+ const tokensCss = await Bun.file(opts.tokensCssPath)
614
+ .text()
615
+ .catch(() => '');
616
+ if (tokensCss) {
617
+ const { theme, usedCss } = filterTokensCss(tokensCss, tokens);
618
+ if (Object.keys(theme).length > 0) {
619
+ cssVars = { theme };
620
+ }
621
+ // The consumer will graft cssVars into globals.css via shadcn's CLI;
622
+ // we also include the raw token block as a fallback for non-shadcn
623
+ // consumers.
624
+ if (usedCss.length > 0) {
625
+ files.push({
626
+ path: `styles/${slug}.tokens.css`,
627
+ content: usedCss,
628
+ type: 'registry:theme',
629
+ });
630
+ }
631
+ }
632
+ }
633
+ }
634
+ }
635
+
636
+ const item: RegistryItem = {
637
+ $schema: 'https://ui.shadcn.com/schema/registry-item.json',
638
+ name: slug,
639
+ type: 'registry:block',
640
+ title: opts.title,
641
+ description: opts.description,
642
+ dependencies: finalDeps,
643
+ registryDependencies,
644
+ files,
645
+ ...(cssVars ? { cssVars } : {}),
646
+ };
647
+
648
+ // Drop undefined keys for a clean JSON.
649
+ if (!item.title) (item as Partial<RegistryItem>).title = undefined;
650
+ if (!item.description) (item as Partial<RegistryItem>).description = undefined;
651
+
652
+ return item;
653
+ }
654
+
655
+ function kebabCase(s: string): string {
656
+ return s
657
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
658
+ .replace(/[\s_]+/g, '-')
659
+ .toLowerCase()
660
+ .replace(/[^a-z0-9-]+/g, '-')
661
+ .replace(/-+/g, '-')
662
+ .replace(/^-|-$/g, '');
663
+ }
664
+
665
+ /**
666
+ * Write the registry-item.json sidecar next to a canvas. Caller picks the
667
+ * destination path; conventional default is `<canvas-dir>/<Slug>.registry.json`.
668
+ */
669
+ export async function writeRegistryItem(destPath: string, item: RegistryItem): Promise<void> {
670
+ const json = `${JSON.stringify(item, null, 2)}\n`;
671
+ const tmp = `${destPath}.tmp.${Math.random().toString(36).slice(2, 10)}`;
672
+ await Bun.write(tmp, json);
673
+ const { rename } = await import('node:fs/promises');
674
+ await rename(tmp, destPath);
675
+ }
676
+
677
+ // ---------------------------------------------------------------------------
678
+ // CLI entry — invoked from bin/handoff.sh (the orchestrator wrapper) when
679
+ // /design:handoff shells out. Keeps Bun-startup costs off the hot path.
680
+
681
+ if (import.meta.main) {
682
+ const argv = process.argv.slice(2);
683
+ if (argv[0] === '--emit' && argv.length >= 2) {
684
+ const canvas = argv[1] as string;
685
+ const designRoot = argv[2];
686
+ const opts: EmitOptions = { canvasAbsPath: canvas };
687
+ if (designRoot) {
688
+ opts.designRoot = designRoot;
689
+ opts.componentsCssPath = path.join(designRoot, 'system/project/preview/_components.css');
690
+ opts.tokensCssPath = path.join(designRoot, 'system/project/colors_and_type.css');
691
+ }
692
+ // Try to read meta.json sidecar for title/description.
693
+ const metaPath = canvas.replace(/\.tsx$/, '.meta.json');
694
+ try {
695
+ const metaFile = Bun.file(metaPath);
696
+ if (await metaFile.exists()) {
697
+ const meta = (await metaFile.json()) as { title?: string; subtitle?: string };
698
+ opts.title = meta.title;
699
+ opts.description = meta.subtitle;
700
+ }
701
+ } catch {
702
+ // ignore — meta is optional
703
+ }
704
+ try {
705
+ const item = await emitRegistryItem(opts);
706
+ const dest = canvas.replace(/\.tsx$/, '.registry.json');
707
+ await writeRegistryItem(dest, item);
708
+ console.log(
709
+ JSON.stringify({ dest, files: item.files.length, deps: item.dependencies.length })
710
+ );
711
+ process.exit(0);
712
+ } catch (err) {
713
+ const msg = err instanceof Error ? err.message : String(err);
714
+ console.error(`handoff: ${msg}`);
715
+ process.exit(2);
716
+ }
717
+ } else {
718
+ console.error('Usage: bun run handoff.ts --emit <canvas-abs-path> [designRoot]');
719
+ process.exit(2);
720
+ }
721
+ }