@fragments-sdk/cli 0.2.2

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 (259) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/dist/bin.d.ts +1 -0
  4. package/dist/bin.js +4783 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/chunk-4FDQSGKX.js +786 -0
  7. package/dist/chunk-4FDQSGKX.js.map +1 -0
  8. package/dist/chunk-7H2MMGYG.js +369 -0
  9. package/dist/chunk-7H2MMGYG.js.map +1 -0
  10. package/dist/chunk-BSCG3IP7.js +619 -0
  11. package/dist/chunk-BSCG3IP7.js.map +1 -0
  12. package/dist/chunk-LY2CFFPY.js +898 -0
  13. package/dist/chunk-LY2CFFPY.js.map +1 -0
  14. package/dist/chunk-MUZ6CM66.js +6636 -0
  15. package/dist/chunk-MUZ6CM66.js.map +1 -0
  16. package/dist/chunk-OAENNG3G.js +1489 -0
  17. package/dist/chunk-OAENNG3G.js.map +1 -0
  18. package/dist/chunk-XHNKNI6J.js +235 -0
  19. package/dist/chunk-XHNKNI6J.js.map +1 -0
  20. package/dist/core-DWKLGY4N.js +68 -0
  21. package/dist/core-DWKLGY4N.js.map +1 -0
  22. package/dist/generate-4LQNJ7SX.js +249 -0
  23. package/dist/generate-4LQNJ7SX.js.map +1 -0
  24. package/dist/index.d.ts +775 -0
  25. package/dist/index.js +41 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/init-EMVI47QG.js +416 -0
  28. package/dist/init-EMVI47QG.js.map +1 -0
  29. package/dist/mcp-bin.d.ts +1 -0
  30. package/dist/mcp-bin.js +1117 -0
  31. package/dist/mcp-bin.js.map +1 -0
  32. package/dist/scan-4YPRF7FV.js +12 -0
  33. package/dist/scan-4YPRF7FV.js.map +1 -0
  34. package/dist/service-QSZMZJBJ.js +208 -0
  35. package/dist/service-QSZMZJBJ.js.map +1 -0
  36. package/dist/static-viewer-MIPGZ4Z7.js +12 -0
  37. package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
  38. package/dist/test-SQ5ZHXWU.js +1067 -0
  39. package/dist/test-SQ5ZHXWU.js.map +1 -0
  40. package/dist/tokens-HSGMYK64.js +173 -0
  41. package/dist/tokens-HSGMYK64.js.map +1 -0
  42. package/dist/viewer-YRF4SQE4.js +11101 -0
  43. package/dist/viewer-YRF4SQE4.js.map +1 -0
  44. package/package.json +107 -0
  45. package/src/ai.ts +266 -0
  46. package/src/analyze.ts +265 -0
  47. package/src/bin.ts +916 -0
  48. package/src/build.ts +248 -0
  49. package/src/commands/a11y.ts +302 -0
  50. package/src/commands/add.ts +313 -0
  51. package/src/commands/audit.ts +195 -0
  52. package/src/commands/baseline.ts +221 -0
  53. package/src/commands/build.ts +144 -0
  54. package/src/commands/compare.ts +337 -0
  55. package/src/commands/context.ts +107 -0
  56. package/src/commands/dev.ts +107 -0
  57. package/src/commands/enhance.ts +858 -0
  58. package/src/commands/generate.ts +391 -0
  59. package/src/commands/init.ts +531 -0
  60. package/src/commands/link/figma.ts +645 -0
  61. package/src/commands/link/index.ts +10 -0
  62. package/src/commands/link/storybook.ts +267 -0
  63. package/src/commands/list.ts +49 -0
  64. package/src/commands/metrics.ts +114 -0
  65. package/src/commands/reset.ts +242 -0
  66. package/src/commands/scan.ts +537 -0
  67. package/src/commands/storygen.ts +207 -0
  68. package/src/commands/tokens.ts +251 -0
  69. package/src/commands/validate.ts +93 -0
  70. package/src/commands/verify.ts +215 -0
  71. package/src/core/composition.test.ts +262 -0
  72. package/src/core/composition.ts +255 -0
  73. package/src/core/config.ts +84 -0
  74. package/src/core/constants.ts +111 -0
  75. package/src/core/context.ts +380 -0
  76. package/src/core/defineSegment.ts +137 -0
  77. package/src/core/discovery.ts +337 -0
  78. package/src/core/figma.ts +263 -0
  79. package/src/core/fragment-types.ts +214 -0
  80. package/src/core/generators/context.ts +389 -0
  81. package/src/core/generators/index.ts +23 -0
  82. package/src/core/generators/registry.ts +364 -0
  83. package/src/core/generators/typescript-extractor.ts +374 -0
  84. package/src/core/importAnalyzer.ts +217 -0
  85. package/src/core/index.ts +149 -0
  86. package/src/core/loader.ts +155 -0
  87. package/src/core/node.ts +63 -0
  88. package/src/core/parser.ts +551 -0
  89. package/src/core/previewLoader.ts +172 -0
  90. package/src/core/schema/fragment.schema.json +189 -0
  91. package/src/core/schema/registry.schema.json +137 -0
  92. package/src/core/schema.ts +182 -0
  93. package/src/core/storyAdapter.test.ts +571 -0
  94. package/src/core/storyAdapter.ts +761 -0
  95. package/src/core/token-types.ts +287 -0
  96. package/src/core/types.ts +754 -0
  97. package/src/diff.ts +323 -0
  98. package/src/index.ts +43 -0
  99. package/src/mcp/__tests__/projectFields.test.ts +130 -0
  100. package/src/mcp/bin.ts +36 -0
  101. package/src/mcp/index.ts +8 -0
  102. package/src/mcp/server.ts +1310 -0
  103. package/src/mcp/utils.ts +54 -0
  104. package/src/mcp-bin.ts +36 -0
  105. package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
  106. package/src/migrate/__tests__/args/args.test.ts +452 -0
  107. package/src/migrate/__tests__/meta/meta.test.ts +198 -0
  108. package/src/migrate/__tests__/stories/stories.test.ts +278 -0
  109. package/src/migrate/__tests__/utils/utils.test.ts +371 -0
  110. package/src/migrate/__tests__/values/values.test.ts +303 -0
  111. package/src/migrate/bin.ts +108 -0
  112. package/src/migrate/converter.ts +658 -0
  113. package/src/migrate/detect.ts +196 -0
  114. package/src/migrate/index.ts +45 -0
  115. package/src/migrate/migrate.ts +163 -0
  116. package/src/migrate/parser.ts +1136 -0
  117. package/src/migrate/report.ts +624 -0
  118. package/src/migrate/types.ts +169 -0
  119. package/src/screenshot.ts +249 -0
  120. package/src/service/__tests__/ast-utils.test.ts +426 -0
  121. package/src/service/__tests__/enhance-scanner.test.ts +200 -0
  122. package/src/service/__tests__/figma/figma.test.ts +652 -0
  123. package/src/service/__tests__/metrics-store.test.ts +409 -0
  124. package/src/service/__tests__/patch-generator.test.ts +186 -0
  125. package/src/service/__tests__/props-extractor.test.ts +365 -0
  126. package/src/service/__tests__/token-registry.test.ts +267 -0
  127. package/src/service/analytics.ts +659 -0
  128. package/src/service/ast-utils.ts +444 -0
  129. package/src/service/browser-pool.ts +339 -0
  130. package/src/service/capture.ts +267 -0
  131. package/src/service/diff.ts +279 -0
  132. package/src/service/enhance/aggregator.ts +489 -0
  133. package/src/service/enhance/cache.ts +275 -0
  134. package/src/service/enhance/codebase-scanner.ts +357 -0
  135. package/src/service/enhance/context-generator.ts +529 -0
  136. package/src/service/enhance/doc-extractor.ts +523 -0
  137. package/src/service/enhance/index.ts +131 -0
  138. package/src/service/enhance/props-extractor.ts +665 -0
  139. package/src/service/enhance/scanner.ts +445 -0
  140. package/src/service/enhance/storybook-parser.ts +552 -0
  141. package/src/service/enhance/types.ts +346 -0
  142. package/src/service/enhance/variant-renderer.ts +479 -0
  143. package/src/service/figma.ts +1008 -0
  144. package/src/service/index.ts +249 -0
  145. package/src/service/metrics-store.ts +333 -0
  146. package/src/service/patch-generator.ts +349 -0
  147. package/src/service/report.ts +854 -0
  148. package/src/service/storage.ts +401 -0
  149. package/src/service/token-fixes.ts +281 -0
  150. package/src/service/token-parser.ts +504 -0
  151. package/src/service/token-registry.ts +721 -0
  152. package/src/service/utils.ts +172 -0
  153. package/src/setup.ts +241 -0
  154. package/src/shared/command-wrapper.ts +81 -0
  155. package/src/shared/dev-server-client.ts +199 -0
  156. package/src/shared/index.ts +8 -0
  157. package/src/shared/segment-loader.ts +59 -0
  158. package/src/shared/types.ts +147 -0
  159. package/src/static-viewer.ts +715 -0
  160. package/src/test/discovery.ts +172 -0
  161. package/src/test/index.ts +281 -0
  162. package/src/test/reporters/console.ts +194 -0
  163. package/src/test/reporters/json.ts +190 -0
  164. package/src/test/reporters/junit.ts +186 -0
  165. package/src/test/runner.ts +598 -0
  166. package/src/test/types.ts +245 -0
  167. package/src/test/watch.ts +200 -0
  168. package/src/validators.ts +152 -0
  169. package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
  170. package/src/viewer/__tests__/render-utils.test.ts +232 -0
  171. package/src/viewer/__tests__/style-utils.test.ts +404 -0
  172. package/src/viewer/bin.ts +86 -0
  173. package/src/viewer/cli/health.ts +256 -0
  174. package/src/viewer/cli/index.ts +33 -0
  175. package/src/viewer/cli/scan.ts +124 -0
  176. package/src/viewer/cli/utils.ts +174 -0
  177. package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
  178. package/src/viewer/components/ActionCapture.tsx +172 -0
  179. package/src/viewer/components/ActionsPanel.tsx +371 -0
  180. package/src/viewer/components/App.tsx +638 -0
  181. package/src/viewer/components/BottomPanel.tsx +224 -0
  182. package/src/viewer/components/CodePanel.tsx +589 -0
  183. package/src/viewer/components/CommandPalette.tsx +336 -0
  184. package/src/viewer/components/ComponentGraph.tsx +394 -0
  185. package/src/viewer/components/ComponentHeader.tsx +85 -0
  186. package/src/viewer/components/ContractPanel.tsx +234 -0
  187. package/src/viewer/components/ErrorBoundary.tsx +85 -0
  188. package/src/viewer/components/FigmaEmbed.tsx +231 -0
  189. package/src/viewer/components/FragmentEditor.tsx +485 -0
  190. package/src/viewer/components/HealthDashboard.tsx +452 -0
  191. package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
  192. package/src/viewer/components/Icons.tsx +417 -0
  193. package/src/viewer/components/InteractionsPanel.tsx +720 -0
  194. package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
  195. package/src/viewer/components/IsolatedRender.tsx +111 -0
  196. package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
  197. package/src/viewer/components/LandingPage.tsx +441 -0
  198. package/src/viewer/components/Layout.tsx +22 -0
  199. package/src/viewer/components/LeftSidebar.tsx +391 -0
  200. package/src/viewer/components/MultiViewportPreview.tsx +429 -0
  201. package/src/viewer/components/PreviewArea.tsx +404 -0
  202. package/src/viewer/components/PreviewFrameHost.tsx +310 -0
  203. package/src/viewer/components/PreviewPane.tsx +150 -0
  204. package/src/viewer/components/PreviewToolbar.tsx +176 -0
  205. package/src/viewer/components/PropsEditor.tsx +512 -0
  206. package/src/viewer/components/PropsTable.tsx +98 -0
  207. package/src/viewer/components/RelationsSection.tsx +57 -0
  208. package/src/viewer/components/ResizablePanel.tsx +328 -0
  209. package/src/viewer/components/RightSidebar.tsx +118 -0
  210. package/src/viewer/components/ScreenshotButton.tsx +90 -0
  211. package/src/viewer/components/Sidebar.tsx +169 -0
  212. package/src/viewer/components/SkeletonLoader.tsx +156 -0
  213. package/src/viewer/components/StoryRenderer.tsx +128 -0
  214. package/src/viewer/components/ThemeProvider.tsx +96 -0
  215. package/src/viewer/components/Toast.tsx +67 -0
  216. package/src/viewer/components/TokenStylePanel.tsx +708 -0
  217. package/src/viewer/components/UsageSection.tsx +95 -0
  218. package/src/viewer/components/VariantMatrix.tsx +350 -0
  219. package/src/viewer/components/VariantRenderer.tsx +131 -0
  220. package/src/viewer/components/VariantTabs.tsx +84 -0
  221. package/src/viewer/components/ViewportSelector.tsx +165 -0
  222. package/src/viewer/components/_future/CreatePage.tsx +836 -0
  223. package/src/viewer/composition-renderer.ts +381 -0
  224. package/src/viewer/constants/index.ts +1 -0
  225. package/src/viewer/constants/ui.ts +185 -0
  226. package/src/viewer/entry.tsx +299 -0
  227. package/src/viewer/hooks/index.ts +2 -0
  228. package/src/viewer/hooks/useA11yCache.ts +383 -0
  229. package/src/viewer/hooks/useA11yService.ts +498 -0
  230. package/src/viewer/hooks/useActions.ts +138 -0
  231. package/src/viewer/hooks/useAppState.ts +124 -0
  232. package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
  233. package/src/viewer/hooks/useHmrStatus.ts +109 -0
  234. package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
  235. package/src/viewer/hooks/usePreviewBridge.ts +347 -0
  236. package/src/viewer/hooks/useScrollSpy.ts +78 -0
  237. package/src/viewer/hooks/useUrlState.ts +330 -0
  238. package/src/viewer/hooks/useViewSettings.ts +125 -0
  239. package/src/viewer/index.html +28 -0
  240. package/src/viewer/index.ts +14 -0
  241. package/src/viewer/intelligence/healthReport.ts +505 -0
  242. package/src/viewer/intelligence/styleDrift.ts +340 -0
  243. package/src/viewer/intelligence/usageScanner.ts +309 -0
  244. package/src/viewer/jsx-parser.ts +485 -0
  245. package/src/viewer/postcss.config.js +6 -0
  246. package/src/viewer/preview-frame-entry.tsx +25 -0
  247. package/src/viewer/preview-frame.html +109 -0
  248. package/src/viewer/render-template.html +68 -0
  249. package/src/viewer/render-utils.ts +170 -0
  250. package/src/viewer/server.ts +276 -0
  251. package/src/viewer/style-utils.ts +414 -0
  252. package/src/viewer/styles/globals.css +355 -0
  253. package/src/viewer/tailwind.config.js +37 -0
  254. package/src/viewer/types/a11y.ts +197 -0
  255. package/src/viewer/utils/a11y-fixes.ts +471 -0
  256. package/src/viewer/utils/actionExport.ts +372 -0
  257. package/src/viewer/utils/colorSchemes.ts +201 -0
  258. package/src/viewer/utils/detectRelationships.ts +256 -0
  259. package/src/viewer/vite-plugin.ts +2143 -0
@@ -0,0 +1,221 @@
1
+ /**
2
+ * fragments baseline - Manage visual regression baselines
3
+ */
4
+
5
+ import { readdir, rm, mkdir } from 'node:fs/promises';
6
+ import { join, relative } from 'node:path';
7
+ import pc from 'picocolors';
8
+ import { BRAND, type Theme } from '../core/index.js';
9
+ import { loadConfig } from '../core/node.js';
10
+ import { runScreenshotCommand } from '../screenshot.js';
11
+ import { StorageManager } from '../service/index.js';
12
+
13
+ /**
14
+ * Options for baseline command
15
+ */
16
+ export interface BaselineOptions {
17
+ /** Path to config file */
18
+ config?: string;
19
+ /** Specific variant to update */
20
+ variant?: string;
21
+ /** Update/delete all baselines */
22
+ all?: boolean;
23
+ /** Theme for baseline (light/dark) */
24
+ theme?: Theme;
25
+ /** Dev server port */
26
+ port?: number | string;
27
+ }
28
+
29
+ /**
30
+ * Result of baseline command
31
+ */
32
+ export interface BaselineResult {
33
+ success: boolean;
34
+ action: 'update' | 'list' | 'delete';
35
+ count?: number;
36
+ }
37
+
38
+ /**
39
+ * Run the baseline command
40
+ */
41
+ export async function baseline(
42
+ action: string,
43
+ component: string | undefined,
44
+ options: BaselineOptions = {}
45
+ ): Promise<BaselineResult> {
46
+ const { config: configPath, variant, all = false, theme = 'light', port = 6006 } = options;
47
+
48
+ const { config, configDir } = await loadConfig(configPath);
49
+
50
+ // Initialize storage manager
51
+ const storage = new StorageManager({
52
+ projectRoot: configDir,
53
+ viewport: config.screenshots?.viewport,
54
+ });
55
+ await storage.initialize();
56
+
57
+ console.log(pc.cyan(`\n${BRAND.name} Baseline Manager\n`));
58
+
59
+ const baseUrl = `http://localhost:${port}`;
60
+
61
+ switch (action) {
62
+ case 'update':
63
+ return updateBaseline(component, options, config, configDir, baseUrl);
64
+
65
+ case 'list':
66
+ return listBaselines(configDir);
67
+
68
+ case 'delete':
69
+ return deleteBaseline(component, options, configDir);
70
+
71
+ default:
72
+ console.log(pc.red(`Unknown action: ${action}`));
73
+ console.log(pc.dim('Available actions: update, list, delete\n'));
74
+ process.exit(1);
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Update baselines
80
+ */
81
+ async function updateBaseline(
82
+ component: string | undefined,
83
+ options: BaselineOptions,
84
+ config: any,
85
+ configDir: string,
86
+ baseUrl: string
87
+ ): Promise<BaselineResult> {
88
+ const { variant, all = false, theme = 'light' as const } = options;
89
+
90
+ if (!component && !all) {
91
+ // Interactive mode - prompt for component selection
92
+ const { select } = await import('@inquirer/prompts');
93
+
94
+ // Fetch available components
95
+ const contextResp = await fetch(`${baseUrl}/fragments/context?format=json`);
96
+ if (!contextResp.ok) {
97
+ throw new Error('Failed to fetch fragments. Make sure dev server is running.');
98
+ }
99
+
100
+ const contextData = JSON.parse(await contextResp.text());
101
+ const segments = contextData.components || [];
102
+
103
+ if (segments.length === 0) {
104
+ console.log(pc.yellow('No components found.\n'));
105
+ return { success: true, action: 'update', count: 0 };
106
+ }
107
+
108
+ component = await select({
109
+ message: 'Select component to update:',
110
+ choices: segments.map((s: { name: string }) => ({
111
+ name: s.name,
112
+ value: s.name,
113
+ })),
114
+ });
115
+ }
116
+
117
+ if (all) {
118
+ // Update all baselines
119
+ console.log(pc.dim('Updating all baselines...\n'));
120
+
121
+ await runScreenshotCommand(config, configDir, {
122
+ theme,
123
+ });
124
+
125
+ console.log(pc.green('\n✓ All baselines updated\n'));
126
+ } else if (component) {
127
+ // Update specific component
128
+ console.log(pc.dim(`Updating baselines for ${component}...\n`));
129
+
130
+ await runScreenshotCommand(config, configDir, {
131
+ component,
132
+ variant,
133
+ theme,
134
+ });
135
+
136
+ console.log(pc.green(`\n✓ Baselines updated for ${component}\n`));
137
+ }
138
+
139
+ // Show file paths
140
+ const baselinesDir = join(configDir, BRAND.dataDir, 'baselines');
141
+ console.log(pc.dim(`Baselines directory: ${relative(process.cwd(), baselinesDir)}\n`));
142
+
143
+ return { success: true, action: 'update' };
144
+ }
145
+
146
+ /**
147
+ * List all baselines
148
+ */
149
+ async function listBaselines(configDir: string): Promise<BaselineResult> {
150
+ const baselinesDir = join(configDir, BRAND.dataDir, 'baselines');
151
+
152
+ try {
153
+ const files = await readdir(baselinesDir, { recursive: true });
154
+ const pngFiles = (files as string[]).filter((f) => f.endsWith('.png'));
155
+
156
+ if (pngFiles.length === 0) {
157
+ console.log(pc.yellow('No baselines found.\n'));
158
+ console.log(pc.dim(`Run ${pc.cyan(`${BRAND.cliCommand} screenshot`)} to capture baselines.\n`));
159
+ return { success: true, action: 'list', count: 0 };
160
+ }
161
+
162
+ console.log(pc.bold('Baselines:\n'));
163
+ for (const file of pngFiles) {
164
+ console.log(` ${file}`);
165
+ }
166
+ console.log();
167
+ console.log(pc.dim(`Total: ${pngFiles.length} baseline(s)\n`));
168
+
169
+ return { success: true, action: 'list', count: pngFiles.length };
170
+ } catch {
171
+ console.log(pc.yellow('No baselines directory found.\n'));
172
+ console.log(pc.dim(`Run ${pc.cyan(`${BRAND.cliCommand} screenshot`)} to capture baselines.\n`));
173
+ return { success: true, action: 'list', count: 0 };
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Delete baselines
179
+ */
180
+ async function deleteBaseline(
181
+ component: string | undefined,
182
+ options: BaselineOptions,
183
+ configDir: string
184
+ ): Promise<BaselineResult> {
185
+ const { all = false } = options;
186
+ const baselinesDir = join(configDir, BRAND.dataDir, 'baselines');
187
+
188
+ if (all) {
189
+ // Delete all baselines
190
+ const { confirm } = await import('@inquirer/prompts');
191
+
192
+ const confirmed = await confirm({
193
+ message: 'Delete ALL baselines? This cannot be undone.',
194
+ default: false,
195
+ });
196
+
197
+ if (!confirmed) {
198
+ console.log(pc.dim('\nNo changes made.\n'));
199
+ return { success: true, action: 'delete', count: 0 };
200
+ }
201
+
202
+ await rm(baselinesDir, { recursive: true, force: true });
203
+ console.log(pc.green('\n✓ All baselines deleted\n'));
204
+ return { success: true, action: 'delete' };
205
+ } else if (component) {
206
+ // Delete specific component baselines
207
+ const componentDir = join(baselinesDir, component);
208
+
209
+ try {
210
+ await rm(componentDir, { recursive: true, force: true });
211
+ console.log(pc.green(`✓ Baselines deleted for ${component}\n`));
212
+ return { success: true, action: 'delete' };
213
+ } catch {
214
+ console.log(pc.yellow(`No baselines found for ${component}.\n`));
215
+ return { success: true, action: 'delete', count: 0 };
216
+ }
217
+ } else {
218
+ console.log(pc.yellow('Specify a component name or use --all flag.\n'));
219
+ return { success: false, action: 'delete' };
220
+ }
221
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * fragments build - Build compiled fragments.json and .fragments/ directory
3
+ */
4
+
5
+ import pc from 'picocolors';
6
+ import { BRAND } from '../core/index.js';
7
+ import { loadConfig } from '../core/node.js';
8
+ import { buildSegments, buildFragmentsDir } from '../build.js';
9
+ import { scan } from './scan.js';
10
+
11
+ /**
12
+ * Options for build command
13
+ */
14
+ export interface BuildOptions {
15
+ /** Path to config file */
16
+ config?: string;
17
+ /** Output file path */
18
+ output?: string;
19
+ /** Also generate .fragments/registry.json and context.md */
20
+ registry?: boolean;
21
+ /** Only generate .fragments/ directory (skip fragments.json) */
22
+ registryOnly?: boolean;
23
+ /** Build from source code (zero-config, no fragment files needed) */
24
+ fromSource?: boolean;
25
+ /** Skip usage analysis when building from source */
26
+ skipUsage?: boolean;
27
+ /** Skip Storybook parsing when building from source */
28
+ skipStorybook?: boolean;
29
+ /** Verbose output */
30
+ verbose?: boolean;
31
+ }
32
+
33
+ /**
34
+ * Result of build command
35
+ */
36
+ export interface BuildResult {
37
+ success: boolean;
38
+ segmentCount?: number;
39
+ outputPath?: string;
40
+ componentCount?: number;
41
+ registryPath?: string;
42
+ contextPath?: string;
43
+ errors: Array<{
44
+ file: string;
45
+ error: string;
46
+ }>;
47
+ }
48
+
49
+ /**
50
+ * Run the build command
51
+ */
52
+ export async function build(options: BuildOptions = {}): Promise<BuildResult> {
53
+ // If building from source, use the scan pipeline
54
+ if (options.fromSource) {
55
+ console.log(pc.cyan(`\n${BRAND.name} Build (from source)\n`));
56
+ console.log(pc.dim('Using zero-config source extraction pipeline\n'));
57
+
58
+ const scanResult = await scan({
59
+ config: options.config,
60
+ output: options.output,
61
+ skipUsage: options.skipUsage,
62
+ skipStorybook: options.skipStorybook,
63
+ verbose: options.verbose,
64
+ });
65
+
66
+ return {
67
+ success: scanResult.success,
68
+ segmentCount: scanResult.componentCount,
69
+ outputPath: scanResult.outputPath,
70
+ errors: scanResult.errors.map((e) => ({ file: e.component, error: e.error })),
71
+ };
72
+ }
73
+
74
+ const { config, configDir } = await loadConfig(options.config);
75
+
76
+ if (options.output) {
77
+ config.outFile = options.output;
78
+ }
79
+
80
+ console.log(pc.cyan(`\n${BRAND.name} Build\n`));
81
+
82
+ const errors: Array<{ file: string; error: string }> = [];
83
+ let segmentCount: number | undefined;
84
+ let outputPath: string | undefined;
85
+ let componentCount: number | undefined;
86
+ let registryPath: string | undefined;
87
+ let contextPath: string | undefined;
88
+
89
+ // Build fragments.json unless --registry-only
90
+ if (!options.registryOnly) {
91
+ console.log(pc.dim('Compiling fragments...\n'));
92
+
93
+ const result = await buildSegments(config, configDir);
94
+
95
+ if (result.errors.length > 0) {
96
+ console.log(pc.yellow('Build completed with errors:\n'));
97
+ for (const error of result.errors) {
98
+ console.log(` ${pc.red('✗')} ${error.file}: ${error.error}`);
99
+ errors.push(error);
100
+ }
101
+ console.log();
102
+ }
103
+
104
+ segmentCount = result.segmentCount;
105
+ outputPath = result.outputPath;
106
+
107
+ console.log(pc.green(`✓ Built ${result.segmentCount} fragment(s)`));
108
+ console.log(pc.dim(` Output: ${result.outputPath}\n`));
109
+ }
110
+
111
+ // Build .fragments/ directory if --registry or --registry-only
112
+ if (options.registry || options.registryOnly) {
113
+ console.log(pc.dim('Generating registry and context...\n'));
114
+
115
+ const fragmentsResult = await buildFragmentsDir(config, configDir);
116
+
117
+ if (fragmentsResult.errors.length > 0) {
118
+ console.log(pc.yellow('Registry build completed with errors:\n'));
119
+ for (const error of fragmentsResult.errors) {
120
+ console.log(` ${pc.red('✗')} ${error.file}: ${error.error}`);
121
+ errors.push(error);
122
+ }
123
+ console.log();
124
+ }
125
+
126
+ componentCount = fragmentsResult.componentCount;
127
+ registryPath = fragmentsResult.registryPath;
128
+ contextPath = fragmentsResult.contextPath;
129
+
130
+ console.log(pc.green(`✓ Generated registry with ${fragmentsResult.componentCount} component(s)`));
131
+ console.log(pc.dim(` Registry: ${fragmentsResult.registryPath}`));
132
+ console.log(pc.dim(` Context: ${fragmentsResult.contextPath}\n`));
133
+ }
134
+
135
+ return {
136
+ success: errors.length === 0,
137
+ segmentCount,
138
+ outputPath,
139
+ componentCount,
140
+ registryPath,
141
+ contextPath,
142
+ errors,
143
+ };
144
+ }
@@ -0,0 +1,337 @@
1
+ /**
2
+ * fragments compare - Compare component renders against Figma designs
3
+ */
4
+
5
+ import pc from 'picocolors';
6
+ import { writeFile, mkdir } from 'node:fs/promises';
7
+ import { resolve, join } from 'node:path';
8
+ import { BRAND } from '../core/index.js';
9
+
10
+ /**
11
+ * Options for compare command
12
+ */
13
+ export interface CompareOptions {
14
+ /** Path to config file */
15
+ config?: string;
16
+ /** Compare specific variant */
17
+ variant?: string;
18
+ /** Figma frame URL (uses segment figma link if not provided) */
19
+ figma?: string;
20
+ /** Diff threshold percentage */
21
+ threshold?: number;
22
+ /** Compare all components with Figma links */
23
+ all?: boolean;
24
+ /** Save diff images to directory */
25
+ output?: string;
26
+ /** Dev server port */
27
+ port?: number | string;
28
+ }
29
+
30
+ /**
31
+ * Compare result from server
32
+ */
33
+ interface CompareResult {
34
+ match?: boolean;
35
+ diffPercentage?: number;
36
+ threshold?: number;
37
+ figmaUrl?: string;
38
+ rendered?: string;
39
+ figma?: string;
40
+ diff?: string;
41
+ changedRegions?: Array<{ x: number; y: number; width: number; height: number }>;
42
+ error?: string;
43
+ suggestion?: string;
44
+ }
45
+
46
+ /**
47
+ * Result of compare command
48
+ */
49
+ export interface CompareCommandResult {
50
+ success: boolean;
51
+ passed: number;
52
+ failed: number;
53
+ skipped: number;
54
+ }
55
+
56
+ /**
57
+ * Run the compare command
58
+ */
59
+ export async function compare(
60
+ component: string | undefined,
61
+ options: CompareOptions = {}
62
+ ): Promise<CompareCommandResult> {
63
+ const {
64
+ variant,
65
+ figma: figmaUrl,
66
+ threshold = 1.0,
67
+ all = false,
68
+ output,
69
+ port = 6006,
70
+ } = options;
71
+
72
+ // Check for Figma access token
73
+ if (!process.env.FIGMA_ACCESS_TOKEN) {
74
+ console.error(pc.red('\nFIGMA_ACCESS_TOKEN environment variable required.'));
75
+ console.log(pc.dim('Generate at: https://www.figma.com/developers/api#access-tokens'));
76
+ console.log(pc.dim(' export FIGMA_ACCESS_TOKEN=figd_xxx'));
77
+ process.exit(1);
78
+ }
79
+
80
+ const baseUrl = `http://localhost:${port}`;
81
+
82
+ console.log(pc.cyan(`\n${BRAND.name} Design Verification\n`));
83
+
84
+ if (all) {
85
+ // Compare all components with Figma links
86
+ return compareAll(baseUrl, threshold, output);
87
+ }
88
+
89
+ // Compare single or selected components
90
+ let componentsToCompare: string[] = [];
91
+
92
+ if (component) {
93
+ componentsToCompare = [component];
94
+ } else {
95
+ // No component specified - show interactive selection
96
+ componentsToCompare = await selectComponents(baseUrl);
97
+ }
98
+
99
+ if (componentsToCompare.length === 0) {
100
+ console.log(pc.dim('\nNo components selected.'));
101
+ return { success: true, passed: 0, failed: 0, skipped: 0 };
102
+ }
103
+
104
+ // Compare selected components
105
+ if (componentsToCompare.length === 1) {
106
+ console.log(pc.dim(`Comparing ${componentsToCompare[0]} to Figma design...\n`));
107
+ } else {
108
+ console.log(pc.dim(`Comparing ${componentsToCompare.length} components to Figma designs...\n`));
109
+ }
110
+
111
+ let passed = 0;
112
+ let failed = 0;
113
+
114
+ for (const comp of componentsToCompare) {
115
+ const response = await fetch(`${baseUrl}/segments/compare`, {
116
+ method: 'POST',
117
+ headers: { 'Content-Type': 'application/json' },
118
+ body: JSON.stringify({
119
+ component: comp,
120
+ variant,
121
+ figmaUrl,
122
+ threshold,
123
+ figmaToken: process.env.FIGMA_ACCESS_TOKEN,
124
+ }),
125
+ });
126
+
127
+ const result = await response.json() as CompareResult;
128
+
129
+ if (result.error) {
130
+ failed++;
131
+ console.log(`${pc.red('✗')} ${pc.bold(comp)} - ${result.error}`);
132
+ continue;
133
+ }
134
+
135
+ if (result.match) {
136
+ passed++;
137
+ console.log(`${pc.green('✓')} ${pc.bold(comp)} ${pc.dim(`${result.diffPercentage}%`)}`);
138
+ } else {
139
+ failed++;
140
+ console.log(`${pc.red('✗')} ${pc.bold(comp)} ${pc.yellow(`${result.diffPercentage}%`)} ${pc.dim(`(threshold: ${threshold}%)`)}`);
141
+ }
142
+
143
+ // Save images if output dir specified
144
+ if (output && result.rendered && result.figma && result.diff) {
145
+ await saveImages(output, comp, result);
146
+ }
147
+ }
148
+
149
+ // Summary for multiple components
150
+ if (componentsToCompare.length > 1) {
151
+ console.log();
152
+ console.log(pc.dim('───────────────────────────────────────'));
153
+ console.log(`\n${pc.green(`${passed} passed`)}, ${pc.red(`${failed} failed`)}\n`);
154
+ } else {
155
+ console.log();
156
+ }
157
+
158
+ if (output && componentsToCompare.length > 0) {
159
+ console.log(pc.dim(`Images saved to: ${output}/\n`));
160
+ }
161
+
162
+ return {
163
+ success: failed === 0,
164
+ passed,
165
+ failed,
166
+ skipped: 0,
167
+ };
168
+ }
169
+
170
+ /**
171
+ * Compare all components with Figma links
172
+ */
173
+ async function compareAll(
174
+ baseUrl: string,
175
+ threshold: number,
176
+ output?: string
177
+ ): Promise<CompareCommandResult> {
178
+ console.log(pc.dim('Comparing all components with Figma links...\n'));
179
+
180
+ // Fetch the context to get all segments
181
+ const contextResp = await fetch(`${baseUrl}/segments/context?format=json`);
182
+ if (!contextResp.ok) {
183
+ throw new Error('Failed to fetch segments. Make sure dev server is running.');
184
+ }
185
+
186
+ const contextText = await contextResp.text();
187
+ let segments: Array<{ name: string; figma?: string }> = [];
188
+ try {
189
+ const contextData = JSON.parse(contextText);
190
+ segments = contextData.components || [];
191
+ } catch {
192
+ segments = [];
193
+ }
194
+
195
+ if (segments.length === 0) {
196
+ console.log(pc.yellow('No components found with Figma links.'));
197
+ console.log(pc.dim('Add figma field to your segment definitions:'));
198
+ console.log(pc.dim(' meta: { figma: "https://figma.com/file/..." }'));
199
+ return { success: true, passed: 0, failed: 0, skipped: 0 };
200
+ }
201
+
202
+ let passed = 0;
203
+ let failed = 0;
204
+ let skipped = 0;
205
+
206
+ for (const seg of segments) {
207
+ // Skip components without figma links
208
+ if (!seg.figma) {
209
+ skipped++;
210
+ console.log(`${pc.dim('⏭️')} ${pc.dim(seg.name)} ${pc.dim('(no figma link)')}`);
211
+ continue;
212
+ }
213
+
214
+ try {
215
+ const response = await fetch(`${baseUrl}/segments/compare`, {
216
+ method: 'POST',
217
+ headers: { 'Content-Type': 'application/json' },
218
+ body: JSON.stringify({
219
+ component: seg.name,
220
+ threshold,
221
+ figmaToken: process.env.FIGMA_ACCESS_TOKEN,
222
+ }),
223
+ });
224
+
225
+ const result = await response.json() as CompareResult;
226
+
227
+ if (result.error) {
228
+ failed++;
229
+ console.log(`${pc.red('✗')} ${pc.bold(seg.name)} - ${result.error}`);
230
+ } else if (result.match) {
231
+ passed++;
232
+ console.log(`${pc.green('✓')} ${pc.bold(seg.name)} ${pc.dim(`${result.diffPercentage}%`)}`);
233
+ } else {
234
+ failed++;
235
+ console.log(`${pc.red('✗')} ${pc.bold(seg.name)} ${pc.yellow(`${result.diffPercentage}%`)} ${pc.dim(`(threshold: ${threshold}%)`)}`);
236
+ }
237
+
238
+ // Save images if output dir specified
239
+ if (output && result.rendered && result.figma && result.diff) {
240
+ await saveImages(output, seg.name, result);
241
+ }
242
+ } catch (error) {
243
+ failed++;
244
+ console.log(`${pc.red('✗')} ${pc.bold(seg.name)} - ${error instanceof Error ? error.message : 'Unknown error'}`);
245
+ }
246
+ }
247
+
248
+ console.log();
249
+ console.log(pc.dim('───────────────────────────────────────'));
250
+ console.log(`\n${pc.green(`${passed} passed`)}, ${pc.red(`${failed} failed`)}, ${pc.dim(`${skipped} skipped`)}\n`);
251
+
252
+ if (output) {
253
+ console.log(pc.dim(`Images saved to: ${output}/\n`));
254
+ }
255
+
256
+ return {
257
+ success: failed === 0,
258
+ passed,
259
+ failed,
260
+ skipped,
261
+ };
262
+ }
263
+
264
+ /**
265
+ * Show interactive selection for components with Figma links
266
+ */
267
+ async function selectComponents(baseUrl: string): Promise<string[]> {
268
+ console.log(pc.dim('Fetching components with Figma links...\n'));
269
+
270
+ const contextResp = await fetch(`${baseUrl}/segments/context?format=json`);
271
+ if (!contextResp.ok) {
272
+ throw new Error('Failed to fetch segments. Make sure dev server is running.');
273
+ }
274
+
275
+ const contextText = await contextResp.text();
276
+ let segments: Array<{ name: string; figma?: string }> = [];
277
+ try {
278
+ const contextData = JSON.parse(contextText);
279
+ segments = (contextData.components || []).filter((s: { figma?: string }) => s.figma);
280
+ } catch {
281
+ segments = [];
282
+ }
283
+
284
+ if (segments.length === 0) {
285
+ console.log(pc.yellow('No components found with Figma links.'));
286
+ console.log(pc.dim('Add figma field to your segment definitions:'));
287
+ console.log(pc.dim(' meta: { figma: "https://figma.com/file/..." }'));
288
+ return [];
289
+ }
290
+
291
+ const { checkbox } = await import('@inquirer/prompts');
292
+
293
+ try {
294
+ const choices = segments.map((seg) => ({
295
+ name: seg.name,
296
+ value: seg.name,
297
+ checked: true,
298
+ }));
299
+
300
+ return await checkbox({
301
+ message: 'Select components to compare:',
302
+ choices,
303
+ pageSize: 15,
304
+ });
305
+ } catch {
306
+ // User cancelled (Ctrl+C)
307
+ console.log(pc.dim('\nNo changes made.'));
308
+ return [];
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Save comparison images to output directory
314
+ */
315
+ async function saveImages(
316
+ outputDir: string,
317
+ component: string,
318
+ result: CompareResult
319
+ ): Promise<void> {
320
+ const dir = resolve(process.cwd(), outputDir);
321
+ await mkdir(dir, { recursive: true });
322
+
323
+ const saveImage = async (data: string, filename: string) => {
324
+ const base64 = data.replace('data:image/png;base64,', '');
325
+ await writeFile(join(dir, filename), Buffer.from(base64, 'base64'));
326
+ };
327
+
328
+ if (result.rendered) {
329
+ await saveImage(result.rendered, `${component}-rendered.png`);
330
+ }
331
+ if (result.figma) {
332
+ await saveImage(result.figma, `${component}-figma.png`);
333
+ }
334
+ if (result.diff) {
335
+ await saveImage(result.diff, `${component}-diff.png`);
336
+ }
337
+ }