@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,674 @@
1
+ // JSON endpoint backers: comments, canvas state, index-data, system-data.
2
+ // Returns plain objects; http.ts wraps them in Response.json().
3
+
4
+ import crypto from 'node:crypto';
5
+ import type { Dirent } from 'node:fs';
6
+ import { readFile, readdir, stat as statp } from 'node:fs/promises';
7
+ import path from 'node:path';
8
+
9
+ import type { Context } from './context.ts';
10
+
11
+ const SKIP_DIRS = new Set([
12
+ 'node_modules',
13
+ '.git',
14
+ '.next',
15
+ '.turbo',
16
+ 'dist',
17
+ 'build',
18
+ '.expo',
19
+ 'coverage',
20
+ 'dev-server',
21
+ '_history',
22
+ ]);
23
+ const HIDDEN_OK = new Set(['.ai', '.claude', '.design']);
24
+
25
+ // ---------- File tree ----------
26
+
27
+ /**
28
+ * Find canvas files under a non-DS group root. Phase 3.6+ accepts both `.tsx`
29
+ * (current authoring format) and `.html` (legacy, pre-codemod) so the tree
30
+ * keeps rendering during the migration grace window. DS preview specimens
31
+ * (`system/<ds>/preview/*.html`) intentionally stay `.html` and travel via
32
+ * the DS-aware `findFiles()` path below.
33
+ */
34
+ export async function findHtmlFiles(absRoot: string, prefixUnderRepo: string): Promise<string[]> {
35
+ const out: string[] = [];
36
+ let entries: Dirent[];
37
+ try {
38
+ entries = await readdir(absRoot, { withFileTypes: true });
39
+ } catch {
40
+ return out;
41
+ }
42
+ entries.sort((a, b) => a.name.localeCompare(b.name));
43
+ for (const e of entries) {
44
+ if (e.name.startsWith('.') && !HIDDEN_OK.has(e.name) && !e.name.startsWith('_')) continue;
45
+ if (e.name.startsWith('_')) continue;
46
+ if (SKIP_DIRS.has(e.name)) continue;
47
+ const full = path.join(absRoot, e.name);
48
+ const rel = path.posix.join(prefixUnderRepo, e.name);
49
+ if (e.isDirectory()) out.push(...(await findHtmlFiles(full, rel)));
50
+ else {
51
+ const low = e.name.toLowerCase();
52
+ if (low.endsWith('.tsx') || low.endsWith('.html')) out.push(rel);
53
+ }
54
+ }
55
+ return out;
56
+ }
57
+
58
+ async function findFiles(absRoot: string, prefix: string, exts: string[]): Promise<string[]> {
59
+ const out: string[] = [];
60
+ let entries: Dirent[];
61
+ try {
62
+ entries = await readdir(absRoot, { withFileTypes: true });
63
+ } catch {
64
+ return out;
65
+ }
66
+ entries.sort((a, b) => a.name.localeCompare(b.name));
67
+ for (const e of entries) {
68
+ if (e.name.startsWith('.') && !HIDDEN_OK.has(e.name)) continue;
69
+ if (e.name.startsWith('_')) continue;
70
+ if (SKIP_DIRS.has(e.name)) continue;
71
+ const full = path.join(absRoot, e.name);
72
+ const rel = path.posix.join(prefix, e.name);
73
+ if (e.isDirectory()) out.push(...(await findFiles(full, rel, exts)));
74
+ else if (exts.some((x) => e.name.toLowerCase().endsWith(x))) out.push(rel);
75
+ }
76
+ return out;
77
+ }
78
+
79
+ // ---------- Comments ----------
80
+
81
+ export interface Comment {
82
+ id: string;
83
+ file: string;
84
+ selector: string;
85
+ dom_path: string[];
86
+ tag: string;
87
+ classes: string;
88
+ bounds: { x: number; y: number; w: number; h: number } | null;
89
+ html_excerpt: string;
90
+ text: string;
91
+ status: 'open' | 'resolved';
92
+ created: string;
93
+ resolved_at: string | null;
94
+ }
95
+
96
+ export interface Api {
97
+ // File tree
98
+ fileSlug(file: string): string;
99
+ loadCommentsForFile(file: string): Promise<Comment[]>;
100
+ saveCommentsForFile(file: string, list: Comment[]): Promise<void>;
101
+ loadAllComments(): Promise<Record<string, Comment[]>>;
102
+ commentsAdd(payload: Partial<Comment> & { file: string; text: string }): Promise<Comment | null>;
103
+ commentsPatch(id: string, patch: Partial<Comment>): Promise<Comment | null>;
104
+ commentsDelete(id: string): Promise<boolean>;
105
+ // Canvas state
106
+ loadCanvasState(file: string): Promise<Record<string, unknown> | null>;
107
+ saveCanvasState(file: string, state: Record<string, unknown>): Promise<void>;
108
+ // Canvas meta sidecar (Phase 4 T5 — .design/ui/<slug>.meta.json)
109
+ loadCanvasMeta(file: string): Promise<Record<string, unknown> | null>;
110
+ patchCanvasMeta(
111
+ file: string,
112
+ patch: Record<string, unknown>
113
+ ): Promise<Record<string, unknown> | null>;
114
+ // Annotations sidecar (Phase 5 — .design/<slug>.annotations.svg)
115
+ loadAnnotations(file: string): Promise<string | null>;
116
+ saveAnnotations(file: string, svg: string): Promise<boolean>;
117
+ // Aggregate data
118
+ buildIndexData(): Promise<unknown>;
119
+ buildSystemData(): Promise<unknown>;
120
+ }
121
+
122
+ export function createApi(ctx: Context, onCommentsChanged: (file: string) => void): Api {
123
+ const { paths, cfg } = ctx;
124
+
125
+ function fileSlug(file: string): string {
126
+ let p = String(file).replace(/^\/+|\/+$/g, '');
127
+ try {
128
+ p = decodeURIComponent(p);
129
+ } catch {
130
+ /* ignore */
131
+ }
132
+ const prefix = `${paths.designRel.replace(/^\/+|\/+$/g, '')}/`;
133
+ if (p.startsWith(prefix)) p = p.slice(prefix.length);
134
+ return p
135
+ .replace(/\//g, '-')
136
+ .replace(/\s+/g, '_')
137
+ .replace(/\.(tsx|html)$/i, '')
138
+ .replace(/^\.+/, '')
139
+ .toLowerCase();
140
+ }
141
+
142
+ function commentsPath(file: string): string {
143
+ return path.join(paths.commentsDir, `${fileSlug(file)}.json`);
144
+ }
145
+
146
+ async function loadCommentsForFile(file: string): Promise<Comment[]> {
147
+ try {
148
+ const raw = await Bun.file(commentsPath(file)).text();
149
+ const arr = JSON.parse(raw);
150
+ return Array.isArray(arr) ? arr : [];
151
+ } catch {
152
+ return [];
153
+ }
154
+ }
155
+
156
+ async function saveCommentsForFile(file: string, list: Comment[]) {
157
+ await Bun.write(commentsPath(file), JSON.stringify(list, null, 2));
158
+ }
159
+
160
+ async function loadAllComments(): Promise<Record<string, Comment[]>> {
161
+ const out: Record<string, Comment[]> = {};
162
+ let entries: Dirent[];
163
+ try {
164
+ entries = await readdir(paths.commentsDir, { withFileTypes: true });
165
+ } catch {
166
+ return out;
167
+ }
168
+ for (const e of entries) {
169
+ if (!e.isFile() || !e.name.endsWith('.json')) continue;
170
+ try {
171
+ const raw = await readFile(path.join(paths.commentsDir, e.name), 'utf8');
172
+ const arr = JSON.parse(raw);
173
+ if (!Array.isArray(arr) || arr.length === 0) continue;
174
+ const file = arr[0]?.file as string | undefined;
175
+ if (file) out[file] = arr;
176
+ } catch {
177
+ /* ignore */
178
+ }
179
+ }
180
+ return out;
181
+ }
182
+
183
+ function newCommentId(): string {
184
+ return `c_${crypto.randomBytes(6).toString('hex')}`;
185
+ }
186
+
187
+ async function commentsAdd(payload: Partial<Comment> & { file: string; text: string }) {
188
+ if (!payload || typeof payload.file !== 'string' || !payload.file) return null;
189
+ if (typeof payload.text !== 'string' || !payload.text.trim()) return null;
190
+ const list = await loadCommentsForFile(payload.file);
191
+ const c: Comment = {
192
+ id: newCommentId(),
193
+ file: payload.file,
194
+ selector: String(payload.selector || ''),
195
+ dom_path: Array.isArray(payload.dom_path) ? payload.dom_path.slice(0, 16) : [],
196
+ tag: String(payload.tag || ''),
197
+ classes: String(payload.classes || ''),
198
+ bounds: payload.bounds ?? null,
199
+ html_excerpt: String(payload.html_excerpt || '').slice(0, 2000),
200
+ text: String(payload.text).trim().slice(0, 4000),
201
+ status: 'open',
202
+ created: new Date().toISOString(),
203
+ resolved_at: null,
204
+ };
205
+ list.push(c);
206
+ await saveCommentsForFile(payload.file, list);
207
+ onCommentsChanged(payload.file);
208
+ return c;
209
+ }
210
+
211
+ async function commentsPatch(id: string, patch: Partial<Comment>) {
212
+ const all = await loadAllComments();
213
+ for (const [file, list] of Object.entries(all)) {
214
+ const i = list.findIndex((c) => c.id === id);
215
+ if (i < 0) continue;
216
+ const entry = list[i];
217
+ if (!entry) continue;
218
+ if (patch.status === 'resolved' || patch.status === 'open') {
219
+ entry.status = patch.status;
220
+ entry.resolved_at = patch.status === 'resolved' ? new Date().toISOString() : null;
221
+ }
222
+ if (typeof patch.text === 'string' && patch.text.trim()) {
223
+ entry.text = patch.text.trim().slice(0, 4000);
224
+ }
225
+ await saveCommentsForFile(file, list);
226
+ onCommentsChanged(file);
227
+ return entry;
228
+ }
229
+ return null;
230
+ }
231
+
232
+ async function commentsDelete(id: string): Promise<boolean> {
233
+ const all = await loadAllComments();
234
+ for (const [file, list] of Object.entries(all)) {
235
+ const i = list.findIndex((c) => c.id === id);
236
+ if (i < 0) continue;
237
+ list.splice(i, 1);
238
+ await saveCommentsForFile(file, list);
239
+ onCommentsChanged(file);
240
+ return true;
241
+ }
242
+ return false;
243
+ }
244
+
245
+ // ---------- Canvas state ----------
246
+
247
+ function canvasStatePath(file: string): string {
248
+ return path.join(paths.canvasStateDir, `${fileSlug(file)}.json`);
249
+ }
250
+
251
+ async function loadCanvasState(file: string) {
252
+ try {
253
+ const raw = await Bun.file(canvasStatePath(file)).text();
254
+ const obj = JSON.parse(raw);
255
+ return obj && typeof obj === 'object' ? obj : null;
256
+ } catch {
257
+ return null;
258
+ }
259
+ }
260
+
261
+ // ---------- Canvas meta sidecar (Phase 4 T5) ----------
262
+ //
263
+ // Each canvas under `<designRoot>/ui/<name>.tsx` has a sibling
264
+ // `<name>.meta.json`. Phase 4 stores `layout` (per-artboard world-coord
265
+ // rects) and `viewport` (last pan/zoom) inside that file so the canvas
266
+ // runtime can restore state on reload. The PATCH path is intentionally
267
+ // merge-shallow on top-level keys — never clobber `title`, `sections`,
268
+ // `ai_context`, or any other authoring metadata.
269
+
270
+ /**
271
+ * Resolve `file` (a path relative to repoRoot like `.design/ui/Foo.tsx`)
272
+ * into the absolute path of its sibling `.meta.json` sidecar. Refuses
273
+ * paths that escape repoRoot.
274
+ */
275
+ function canvasMetaPath(file: string): string | null {
276
+ let p = String(file).replace(/^\/+/, '');
277
+ try {
278
+ p = decodeURIComponent(p);
279
+ } catch {
280
+ /* ignore */
281
+ }
282
+ if (p.includes('..')) return null;
283
+ const abs = path.join(paths.repoRoot, p);
284
+ if (!abs.startsWith(`${paths.repoRoot}/`)) return null;
285
+ const ext = path.extname(abs).toLowerCase();
286
+ if (ext !== '.tsx' && ext !== '.html') return null;
287
+ return abs.replace(/\.(tsx|html)$/i, '.meta.json');
288
+ }
289
+
290
+ async function loadCanvasMeta(file: string): Promise<Record<string, unknown> | null> {
291
+ const metaAbs = canvasMetaPath(file);
292
+ if (!metaAbs) return null;
293
+ try {
294
+ const raw = await Bun.file(metaAbs).text();
295
+ const obj = JSON.parse(raw);
296
+ return obj && typeof obj === 'object' && !Array.isArray(obj)
297
+ ? (obj as Record<string, unknown>)
298
+ : null;
299
+ } catch {
300
+ return null;
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Shallow-merge `patch` onto the existing meta sidecar and write back. Only
306
+ * the Phase 4 keys `layout` + `viewport` are accepted from untrusted clients;
307
+ * the rest of meta (title, sections, brief, ai_context, …) is preserved.
308
+ * Returns the merged meta on success, null when the canvas has no meta or
309
+ * the patch is rejected.
310
+ */
311
+ async function patchCanvasMeta(
312
+ file: string,
313
+ patch: Record<string, unknown>
314
+ ): Promise<Record<string, unknown> | null> {
315
+ const metaAbs = canvasMetaPath(file);
316
+ if (!metaAbs) return null;
317
+ if (!patch || typeof patch !== 'object' || Array.isArray(patch)) return null;
318
+ let current: Record<string, unknown> = {};
319
+ try {
320
+ const raw = await Bun.file(metaAbs).text();
321
+ const parsed = JSON.parse(raw);
322
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
323
+ current = parsed as Record<string, unknown>;
324
+ }
325
+ } catch {
326
+ // No existing meta — create one with just the Phase 4 keys.
327
+ }
328
+ const next = { ...current };
329
+ // Whitelist of patchable top-level keys.
330
+ if (patch.layout !== undefined) {
331
+ if (patch.layout === null) {
332
+ next.layout = undefined;
333
+ } else if (typeof patch.layout === 'object' && !Array.isArray(patch.layout)) {
334
+ next.layout = patch.layout;
335
+ }
336
+ }
337
+ if (patch.viewport !== undefined) {
338
+ if (patch.viewport === null) {
339
+ next.viewport = undefined;
340
+ } else if (typeof patch.viewport === 'object' && !Array.isArray(patch.viewport)) {
341
+ const v = patch.viewport as { x?: unknown; y?: unknown; zoom?: unknown };
342
+ if (
343
+ Number.isFinite(v.x as number) &&
344
+ Number.isFinite(v.y as number) &&
345
+ Number.isFinite(v.zoom as number)
346
+ ) {
347
+ const zoom = Math.min(4, Math.max(0.1, v.zoom as number));
348
+ next.viewport = { x: v.x as number, y: v.y as number, zoom };
349
+ }
350
+ }
351
+ }
352
+ next.last_modified = new Date().toISOString();
353
+ await Bun.write(metaAbs, JSON.stringify(next, null, 2));
354
+ return next;
355
+ }
356
+
357
+ // ---------- Annotations sidecar (Phase 5) ----------
358
+ //
359
+ // Each canvas keeps a single `.annotations.svg` file under `<designRoot>/`
360
+ // named by the canonical `fileSlug()`. The client posts the full SVG string
361
+ // on every stroke commit; the server overwrites the file. SVG is bounded at
362
+ // 1 MB (rejects larger bodies) — well above realistic annotation sizes for
363
+ // hundreds of strokes but small enough that a malicious POST can't fill the
364
+ // disk in one round-trip.
365
+
366
+ function annotationsPath(file: string): string {
367
+ return path.join(paths.designRoot, `${fileSlug(file)}.annotations.svg`);
368
+ }
369
+
370
+ async function loadAnnotations(file: string): Promise<string | null> {
371
+ try {
372
+ return await Bun.file(annotationsPath(file)).text();
373
+ } catch {
374
+ return null;
375
+ }
376
+ }
377
+
378
+ async function saveAnnotations(file: string, svg: string): Promise<boolean> {
379
+ if (typeof svg !== 'string') return false;
380
+ if (svg.length > 1024 * 1024) return false;
381
+ // Cheap content gate — must look like an <svg> document. Avoids accidental
382
+ // writes of arbitrary blobs through this endpoint. The client controls the
383
+ // content fully, so we don't try to sanitize beyond a tag check.
384
+ if (!/^\s*<svg[\s>]/i.test(svg)) return false;
385
+ await Bun.write(annotationsPath(file), svg);
386
+ return true;
387
+ }
388
+
389
+ async function saveCanvasState(file: string, state: Record<string, unknown>) {
390
+ if (!state || typeof state !== 'object') return;
391
+ const safe: Record<string, unknown> = {};
392
+ if (state.sections && typeof state.sections === 'object') safe.sections = state.sections;
393
+ if (state.viewport && typeof state.viewport === 'object') {
394
+ const v = state.viewport as { x?: number; y?: number; scale?: number };
395
+ safe.viewport = {
396
+ x: Number.isFinite(v.x) ? v.x : 0,
397
+ y: Number.isFinite(v.y) ? v.y : 0,
398
+ scale: Number.isFinite(v.scale) ? Math.min(8, Math.max(0.05, v.scale as number)) : 1,
399
+ };
400
+ }
401
+ await Bun.write(canvasStatePath(file), JSON.stringify(safe, null, 2));
402
+ }
403
+
404
+ // ---------- Index data + System data ----------
405
+
406
+ async function buildIndexData() {
407
+ const groups = [];
408
+
409
+ // PROJECT — top-level .design/ files (README.md, INDEX.md, config.json, …).
410
+ // These are non-HTML so they're listed but not openable in the iframe; the
411
+ // sidebar can still display them as context (mirrors CV-08 mock).
412
+ const projectFiles: string[] = [];
413
+ try {
414
+ const entries = await readdir(paths.designRoot, { withFileTypes: true });
415
+ entries.sort((a, b) => a.name.localeCompare(b.name));
416
+ for (const e of entries) {
417
+ if (!e.isFile()) continue;
418
+ if (e.name.startsWith('_')) continue;
419
+ if (e.name.startsWith('.')) continue;
420
+ if (!/\.(md|json|txt|yml|yaml|css)$/i.test(e.name)) continue;
421
+ projectFiles.push(path.posix.join(paths.designRel, e.name));
422
+ }
423
+ } catch {}
424
+ if (projectFiles.length) {
425
+ groups.push({
426
+ label: 'Project',
427
+ paths: projectFiles,
428
+ fullPath: paths.designRel,
429
+ // Strip `.design/` — the section header already names the parent;
430
+ // the chain `▾ .design → file` was redundant.
431
+ stripPrefix: `${paths.designRel}/`,
432
+ kind: 'project' as const,
433
+ });
434
+ }
435
+
436
+ // Canvas groups — strip just `.design/` so the immediate subdir
437
+ // (`system`, `ui`, …) shows up as a dir wrapper in the tree (mirrors
438
+ // CV-08's `▾ ui` / `▾ system/project` headers).
439
+ //
440
+ // For DS groups (label === 'Design system' OR path starts with `system`)
441
+ // we also include sibling .md / .css / .json so README, SKILL, and the
442
+ // tokens CSS file render in the tree per CV-08 mock. Inert at click time
443
+ // (FileRow non-HTML branch).
444
+ for (const g of cfg.canvasGroups) {
445
+ const groupAbs = path.join(paths.designRoot, g.path);
446
+ const groupRel = path.posix.join(paths.designRel, g.path);
447
+ const isDs = g.label === 'Design system' || /^system(\/|$)/.test(g.path);
448
+ // Always include canvas sidecars (`.meta.json`, `.css`, `.registry.json`)
449
+ // so the client can nest them under their primary `.tsx` / `.html`. DS
450
+ // groups additionally surface `.md` for README + SKILL docs.
451
+ const filePaths = isDs
452
+ ? await findFiles(groupAbs, groupRel, ['.tsx', '.html', '.md', '.css', '.json'])
453
+ : await findFiles(groupAbs, groupRel, ['.tsx', '.html', '.css', '.json']);
454
+ // Strip down to `g.path` so per-DS folders (`project`, `beta`, …) show
455
+ // up as the top-level dirs inside the DS section. Single-DS configs get
456
+ // a wrapper folder too — slightly more verbose, but consistent with
457
+ // multi-DS and gives the user one click-target per DS to open its
458
+ // SystemView.
459
+ const matchedDs =
460
+ g.label === 'Design system'
461
+ ? (cfg.designSystems ?? []).filter(
462
+ (d) => d.path === g.path || d.path.startsWith(`${g.path}/`)
463
+ )
464
+ : [];
465
+ const dsFolders = matchedDs.map((d) => ({
466
+ name: d.name,
467
+ path: d.path,
468
+ // Folder name relative to the group root — what the client will see as
469
+ // a top-level dir name inside the tree.
470
+ folder:
471
+ d.path === g.path
472
+ ? d.path.split('/').pop() || d.path
473
+ : d.path.slice(g.path.length + 1).split('/')[0],
474
+ }));
475
+ groups.push({
476
+ label: g.label,
477
+ paths: filePaths,
478
+ fullPath: groupRel,
479
+ stripPrefix: `${paths.designRel}/${g.path}/`,
480
+ kind: 'canvas' as const,
481
+ dsFolders: dsFolders.length ? dsFolders : undefined,
482
+ });
483
+ }
484
+
485
+ // RUNTIME — gitignored state files (_active.json, _server.json) +
486
+ // pointers to _history/ and _comments/ dirs. Visible but inert in the
487
+ // sidebar; matches the CV-08 mock's bottom section.
488
+ const runtimeFiles: string[] = [];
489
+ try {
490
+ const entries = await readdir(paths.designRoot, { withFileTypes: true });
491
+ entries.sort((a, b) => a.name.localeCompare(b.name));
492
+ for (const e of entries) {
493
+ if (!e.name.startsWith('_')) continue;
494
+ runtimeFiles.push(path.posix.join(paths.designRel, e.name));
495
+ }
496
+ } catch {}
497
+ if (runtimeFiles.length) {
498
+ groups.push({
499
+ label: 'Runtime',
500
+ paths: runtimeFiles,
501
+ fullPath: paths.designRel,
502
+ // Same as PROJECT — strip `.design/` so each row sits flat under the
503
+ // section header instead of nested under a redundant `.design` dir.
504
+ stripPrefix: `${paths.designRel}/`,
505
+ kind: 'runtime' as const,
506
+ });
507
+ }
508
+
509
+ return {
510
+ project: cfg.name,
511
+ projectLabel: ctx.projectLabel,
512
+ designRoot: paths.designRel,
513
+ groups,
514
+ };
515
+ }
516
+
517
+ function tokenKind(name: string, value: string): string {
518
+ const n = name.toLowerCase();
519
+ const v = String(value).trim();
520
+ if (/(color|fg|bg|border|accent|status|surface|text)/.test(n)) return 'color';
521
+ if (/^#[0-9a-f]{3,8}$/i.test(v)) return 'color';
522
+ if (/^(rgb|rgba|hsl|hsla|oklch|color)\(/i.test(v)) return 'color';
523
+ if (/(font-size|fs|text)/.test(n) && /\d/.test(v)) return 'fontsize';
524
+ if (/(font|family|display|sans|mono)/.test(n)) return 'font';
525
+ if (/(radius|r-)/.test(n)) return 'radius';
526
+ if (/(shadow|elev)/.test(n)) return 'shadow';
527
+ if (/(space|gap|s-|spacing)/.test(n)) return 'space';
528
+ if (/(weight|fw)/.test(n)) return 'weight';
529
+ if (/(line-height|lh|leading)/.test(n)) return 'leading';
530
+ if (/(duration|ease|motion)/.test(n)) return 'motion';
531
+ return 'other';
532
+ }
533
+
534
+ function parseTokens(css: string) {
535
+ const tokens: { name: string; value: string; kind: string }[] = [];
536
+ const re = /(--[a-z][a-z0-9-]*)\s*:\s*([^;}]+);/gi;
537
+ const seen = new Set<string>();
538
+ for (const m of css.matchAll(re)) {
539
+ const name = m[1]?.trim() ?? '';
540
+ const value = m[2]?.trim() ?? '';
541
+ const key = `${name}|${value}`;
542
+ if (seen.has(key)) continue;
543
+ seen.add(key);
544
+ tokens.push({ name, value, kind: tokenKind(name, value) });
545
+ }
546
+ return tokens;
547
+ }
548
+
549
+ async function buildSystemData() {
550
+ const sysAbs = path.join(paths.designRoot, paths.systemDirRel);
551
+ const sysRel = path.posix.join(paths.designRel, paths.systemDirRel);
552
+
553
+ let readme: string | null = null;
554
+ let readmePath: string | null = null;
555
+ const readmeCandidates = [
556
+ path.join(paths.designRoot, 'README.md'),
557
+ path.join(sysAbs, 'README.md'),
558
+ ];
559
+ try {
560
+ const subs = await readdir(sysAbs, { withFileTypes: true });
561
+ for (const s of subs)
562
+ if (s.isDirectory()) readmeCandidates.push(path.join(sysAbs, s.name, 'README.md'));
563
+ } catch {
564
+ /* ignore */
565
+ }
566
+ for (const c of readmeCandidates) {
567
+ try {
568
+ readme = await readFile(c, 'utf8');
569
+ readmePath = path.relative(paths.repoRoot, c);
570
+ break;
571
+ } catch {
572
+ /* ignore */
573
+ }
574
+ }
575
+
576
+ let tokens: ReturnType<typeof parseTokens> = [];
577
+ let tokensPath: string | null = null;
578
+ try {
579
+ const tokensAbs = path.join(paths.designRoot, cfg.tokensCssRel);
580
+ const css = await readFile(tokensAbs, 'utf8');
581
+ tokens = parseTokens(css);
582
+ tokensPath = path.relative(paths.repoRoot, tokensAbs);
583
+ } catch {
584
+ /* ignore */
585
+ }
586
+ const tokenGroups: Record<string, typeof tokens> = {};
587
+ for (const t of tokens) {
588
+ const group = tokenGroups[t.kind] ?? [];
589
+ group.push(t);
590
+ tokenGroups[t.kind] = group;
591
+ }
592
+
593
+ async function galleryFor(folderName: string) {
594
+ const matches: { abs: string; rel: string }[] = [];
595
+ try {
596
+ const subs = await readdir(sysAbs, { withFileTypes: true });
597
+ for (const s of subs) {
598
+ if (!s.isDirectory()) continue;
599
+ const candidate = path.join(sysAbs, s.name, folderName);
600
+ try {
601
+ const st = await statp(candidate);
602
+ if (st.isDirectory())
603
+ matches.push({ abs: candidate, rel: path.posix.join(sysRel, s.name, folderName) });
604
+ } catch {
605
+ /* ignore */
606
+ }
607
+ }
608
+ try {
609
+ const st = await statp(path.join(sysAbs, folderName));
610
+ if (st.isDirectory())
611
+ matches.push({
612
+ abs: path.join(sysAbs, folderName),
613
+ rel: path.posix.join(sysRel, folderName),
614
+ });
615
+ } catch {
616
+ /* ignore */
617
+ }
618
+ } catch {
619
+ /* ignore */
620
+ }
621
+ const items: { label: string; path: string; group: string }[] = [];
622
+ for (const m of matches) {
623
+ const files = await findFiles(m.abs, m.rel, ['.html']);
624
+ for (const f of files) {
625
+ const fname = f
626
+ .split('/')
627
+ .pop()
628
+ ?.replace(/\.html$/i, '');
629
+ const group = f.split('/').slice(-2, -1)[0] || folderName;
630
+ const label = fname.toLowerCase() === 'index' ? group : fname;
631
+ items.push({ label, path: f, group });
632
+ }
633
+ }
634
+ return items;
635
+ }
636
+
637
+ const previewGallery = await galleryFor('preview');
638
+ const uiKitsGallery = await galleryFor('ui_kits');
639
+
640
+ return {
641
+ project: cfg.name,
642
+ designRoot: paths.designRel,
643
+ systemDir: sysRel,
644
+ readme,
645
+ readmePath,
646
+ tokens,
647
+ tokenGroups,
648
+ tokensPath,
649
+ previewGallery,
650
+ uiKitsGallery,
651
+ rootClass: cfg.rootClass,
652
+ themeDefault: cfg.themeDefault,
653
+ teamAccentDefault: cfg.teamAccentDefault,
654
+ };
655
+ }
656
+
657
+ return {
658
+ fileSlug,
659
+ loadCommentsForFile,
660
+ saveCommentsForFile,
661
+ loadAllComments,
662
+ commentsAdd,
663
+ commentsPatch,
664
+ commentsDelete,
665
+ loadCanvasState,
666
+ saveCanvasState,
667
+ loadCanvasMeta,
668
+ patchCanvasMeta,
669
+ loadAnnotations,
670
+ saveAnnotations,
671
+ buildIndexData,
672
+ buildSystemData,
673
+ };
674
+ }