@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,401 @@
1
+ import { mkdir, readFile, writeFile, stat, readdir, rm } from 'node:fs/promises';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { existsSync } from 'node:fs';
4
+ import {
5
+ BRAND,
6
+ DEFAULTS,
7
+ type Screenshot,
8
+ type BaselineInfo,
9
+ type Manifest,
10
+ type Viewport,
11
+ type Theme,
12
+ } from '../core/index.js';
13
+ import { ServiceError, buildScreenshotPath, computeHash } from './utils.js';
14
+
15
+ /**
16
+ * Storage manager configuration
17
+ */
18
+ export interface StorageConfig {
19
+ /** Root directory for project (default: process.cwd()) */
20
+ projectRoot?: string;
21
+
22
+ /** Default viewport */
23
+ viewport?: Viewport;
24
+
25
+ /** Default diff threshold */
26
+ threshold?: number;
27
+
28
+ /** Capture delay */
29
+ captureDelay?: number;
30
+ }
31
+
32
+ /**
33
+ * Manages baseline screenshot storage and manifest.
34
+ */
35
+ export class StorageManager {
36
+ private readonly projectRoot: string;
37
+ private readonly dataDir: string;
38
+ private readonly screenshotsDir: string;
39
+ private readonly manifestPath: string;
40
+ private readonly config: Required<Omit<StorageConfig, 'projectRoot'>>;
41
+
42
+ private manifest: Manifest | null = null;
43
+
44
+ constructor(config: StorageConfig = {}) {
45
+ this.projectRoot = config.projectRoot ?? process.cwd();
46
+ this.dataDir = join(this.projectRoot, BRAND.dataDir);
47
+ this.screenshotsDir = join(this.dataDir, BRAND.screenshotsDir);
48
+ this.manifestPath = join(this.dataDir, BRAND.manifestFile);
49
+
50
+ this.config = {
51
+ viewport: config.viewport ?? DEFAULTS.viewport,
52
+ threshold: config.threshold ?? DEFAULTS.diffThreshold,
53
+ captureDelay: config.captureDelay ?? DEFAULTS.captureDelayMs,
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Get the data directory path
59
+ */
60
+ get dataDirPath(): string {
61
+ return this.dataDir;
62
+ }
63
+
64
+ /**
65
+ * Get the screenshots directory path
66
+ */
67
+ get screenshotsDirPath(): string {
68
+ return this.screenshotsDir;
69
+ }
70
+
71
+ /**
72
+ * Initialize the storage directory structure
73
+ */
74
+ async initialize(): Promise<void> {
75
+ // Create directories
76
+ await mkdir(this.screenshotsDir, { recursive: true });
77
+ await mkdir(join(this.dataDir, BRAND.cacheDir), { recursive: true });
78
+ await mkdir(join(this.dataDir, BRAND.diffDir), { recursive: true });
79
+
80
+ // Load or create manifest
81
+ if (existsSync(this.manifestPath)) {
82
+ await this.loadManifest();
83
+ } else {
84
+ this.manifest = this.createEmptyManifest();
85
+ await this.saveManifest();
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Save a screenshot as a baseline
91
+ */
92
+ async saveBaseline(screenshot: Screenshot): Promise<BaselineInfo> {
93
+ await this.ensureInitialized();
94
+
95
+ const { component, variant, theme } = screenshot.metadata;
96
+ const relativePath = buildScreenshotPath(component, variant, theme);
97
+ const absolutePath = join(this.screenshotsDir, relativePath);
98
+
99
+ // Ensure directory exists
100
+ await mkdir(dirname(absolutePath), { recursive: true });
101
+
102
+ // Write the image
103
+ await writeFile(absolutePath, screenshot.data);
104
+
105
+ // Get file size
106
+ const stats = await stat(absolutePath);
107
+
108
+ // Create baseline info
109
+ const baselineInfo: BaselineInfo = {
110
+ component,
111
+ variant,
112
+ theme,
113
+ path: relativePath,
114
+ hash: screenshot.hash,
115
+ viewport: screenshot.viewport,
116
+ capturedAt: screenshot.capturedAt.toISOString(),
117
+ fileSize: stats.size,
118
+ };
119
+
120
+ // Update manifest
121
+ this.updateManifestBaseline(baselineInfo);
122
+ await this.saveManifest();
123
+
124
+ return baselineInfo;
125
+ }
126
+
127
+ /**
128
+ * Load a baseline screenshot
129
+ */
130
+ async loadBaseline(
131
+ component: string,
132
+ variant: string,
133
+ theme: Theme = DEFAULTS.theme
134
+ ): Promise<Screenshot | null> {
135
+ await this.ensureInitialized();
136
+
137
+ const info = this.getBaselineInfo(component, variant, theme);
138
+ if (!info) {
139
+ return null;
140
+ }
141
+
142
+ const absolutePath = join(this.screenshotsDir, info.path);
143
+
144
+ if (!existsSync(absolutePath)) {
145
+ return null;
146
+ }
147
+
148
+ const data = await readFile(absolutePath);
149
+
150
+ return {
151
+ data,
152
+ hash: info.hash,
153
+ viewport: info.viewport,
154
+ capturedAt: new Date(info.capturedAt),
155
+ metadata: {
156
+ component: info.component,
157
+ variant: info.variant,
158
+ theme: info.theme,
159
+ renderTimeMs: 0, // Not available for loaded baselines
160
+ captureTimeMs: 0,
161
+ },
162
+ };
163
+ }
164
+
165
+ /**
166
+ * Get baseline info from manifest
167
+ */
168
+ getBaselineInfo(
169
+ component: string,
170
+ variant: string,
171
+ theme: Theme = DEFAULTS.theme
172
+ ): BaselineInfo | null {
173
+ if (!this.manifest) {
174
+ return null;
175
+ }
176
+
177
+ const key = this.buildBaselineKey(component, variant, theme);
178
+ const componentBaselines = this.manifest.baselines[component];
179
+
180
+ if (!componentBaselines) {
181
+ return null;
182
+ }
183
+
184
+ return componentBaselines[key] ?? null;
185
+ }
186
+
187
+ /**
188
+ * Check if a baseline exists
189
+ */
190
+ hasBaseline(
191
+ component: string,
192
+ variant: string,
193
+ theme: Theme = DEFAULTS.theme
194
+ ): boolean {
195
+ return this.getBaselineInfo(component, variant, theme) !== null;
196
+ }
197
+
198
+ /**
199
+ * List all baselines
200
+ */
201
+ listBaselines(): BaselineInfo[] {
202
+ if (!this.manifest) {
203
+ return [];
204
+ }
205
+
206
+ const baselines: BaselineInfo[] = [];
207
+
208
+ for (const componentBaselines of Object.values(this.manifest.baselines)) {
209
+ for (const baseline of Object.values(componentBaselines)) {
210
+ baselines.push(baseline);
211
+ }
212
+ }
213
+
214
+ return baselines;
215
+ }
216
+
217
+ /**
218
+ * List baselines for a specific component
219
+ */
220
+ listComponentBaselines(component: string): BaselineInfo[] {
221
+ if (!this.manifest) {
222
+ return [];
223
+ }
224
+
225
+ const componentBaselines = this.manifest.baselines[component];
226
+ if (!componentBaselines) {
227
+ return [];
228
+ }
229
+
230
+ return Object.values(componentBaselines);
231
+ }
232
+
233
+ /**
234
+ * Delete a baseline
235
+ */
236
+ async deleteBaseline(
237
+ component: string,
238
+ variant: string,
239
+ theme: Theme = DEFAULTS.theme
240
+ ): Promise<boolean> {
241
+ await this.ensureInitialized();
242
+
243
+ const info = this.getBaselineInfo(component, variant, theme);
244
+ if (!info) {
245
+ return false;
246
+ }
247
+
248
+ // Delete the file
249
+ const absolutePath = join(this.screenshotsDir, info.path);
250
+ if (existsSync(absolutePath)) {
251
+ await rm(absolutePath);
252
+ }
253
+
254
+ // Remove from manifest
255
+ const key = this.buildBaselineKey(component, variant, theme);
256
+ if (this.manifest?.baselines[component]) {
257
+ delete this.manifest.baselines[component][key];
258
+
259
+ // Clean up empty component entry
260
+ if (Object.keys(this.manifest.baselines[component]).length === 0) {
261
+ delete this.manifest.baselines[component];
262
+ }
263
+ }
264
+
265
+ await this.saveManifest();
266
+ return true;
267
+ }
268
+
269
+ /**
270
+ * Save a diff image
271
+ */
272
+ async saveDiff(
273
+ component: string,
274
+ variant: string,
275
+ theme: Theme,
276
+ diffImage: Buffer
277
+ ): Promise<string> {
278
+ const relativePath = buildScreenshotPath(component, variant, theme).replace(
279
+ '.png',
280
+ '-diff.png'
281
+ );
282
+ const absolutePath = join(this.dataDir, BRAND.diffDir, relativePath);
283
+
284
+ await mkdir(dirname(absolutePath), { recursive: true });
285
+ await writeFile(absolutePath, diffImage);
286
+
287
+ return absolutePath;
288
+ }
289
+
290
+ /**
291
+ * Get the manifest
292
+ */
293
+ getManifest(): Manifest | null {
294
+ return this.manifest;
295
+ }
296
+
297
+ /**
298
+ * Reload manifest from disk
299
+ */
300
+ async reloadManifest(): Promise<void> {
301
+ await this.loadManifest();
302
+ }
303
+
304
+ /**
305
+ * Build a unique key for baseline lookup
306
+ */
307
+ private buildBaselineKey(
308
+ component: string,
309
+ variant: string,
310
+ theme: Theme
311
+ ): string {
312
+ return `${variant}:${theme}`;
313
+ }
314
+
315
+ /**
316
+ * Update manifest with a baseline
317
+ */
318
+ private updateManifestBaseline(info: BaselineInfo): void {
319
+ if (!this.manifest) {
320
+ this.manifest = this.createEmptyManifest();
321
+ }
322
+
323
+ if (!this.manifest.baselines[info.component]) {
324
+ this.manifest.baselines[info.component] = {};
325
+ }
326
+
327
+ const key = this.buildBaselineKey(info.component, info.variant, info.theme);
328
+ this.manifest.baselines[info.component][key] = info;
329
+ this.manifest.generatedAt = new Date().toISOString();
330
+ }
331
+
332
+ /**
333
+ * Create an empty manifest
334
+ */
335
+ private createEmptyManifest(): Manifest {
336
+ return {
337
+ version: '1.0.0',
338
+ generatedAt: new Date().toISOString(),
339
+ config: {
340
+ defaultViewport: this.config.viewport,
341
+ defaultThreshold: this.config.threshold,
342
+ captureDelay: this.config.captureDelay,
343
+ },
344
+ baselines: {},
345
+ };
346
+ }
347
+
348
+ /**
349
+ * Load manifest from disk
350
+ */
351
+ private async loadManifest(): Promise<void> {
352
+ try {
353
+ const content = await readFile(this.manifestPath, 'utf-8');
354
+ this.manifest = JSON.parse(content) as Manifest;
355
+ } catch {
356
+ this.manifest = this.createEmptyManifest();
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Save manifest to disk
362
+ */
363
+ private async saveManifest(): Promise<void> {
364
+ if (!this.manifest) {
365
+ return;
366
+ }
367
+
368
+ await mkdir(dirname(this.manifestPath), { recursive: true });
369
+ await writeFile(
370
+ this.manifestPath,
371
+ JSON.stringify(this.manifest, null, 2),
372
+ 'utf-8'
373
+ );
374
+ }
375
+
376
+ /**
377
+ * Ensure storage is initialized
378
+ */
379
+ private async ensureInitialized(): Promise<void> {
380
+ if (!this.manifest) {
381
+ await this.initialize();
382
+ }
383
+ }
384
+ }
385
+
386
+ /**
387
+ * Error class for storage errors
388
+ */
389
+ export class StorageError extends ServiceError {
390
+ constructor(message: string, code: string, suggestion?: string) {
391
+ super(message, code, suggestion);
392
+ this.name = `${BRAND.name}StorageError`;
393
+ }
394
+ }
395
+
396
+ /**
397
+ * Create a storage manager
398
+ */
399
+ export function createStorageManager(config?: StorageConfig): StorageManager {
400
+ return new StorageManager(config);
401
+ }
@@ -0,0 +1,281 @@
1
+ /**
2
+ * Token Fix Suggestions
3
+ *
4
+ * Generates AI-friendly fix suggestions when hardcoded values
5
+ * should be replaced with design tokens.
6
+ */
7
+
8
+ import type {
9
+ DesignToken,
10
+ EnhancedStyleDiffItem,
11
+ TokenFix,
12
+ TokenUsageSummary,
13
+ } from "../core/index.js";
14
+ import { TokenRegistryManager } from "./token-registry.js";
15
+
16
+ /**
17
+ * Context for AI agents to understand and fix token issues
18
+ */
19
+ export interface TokenFixContext {
20
+ /** Component name */
21
+ componentName: string;
22
+
23
+ /** Summary of token usage */
24
+ summary: TokenUsageSummary;
25
+
26
+ /** Detailed fix suggestions for each hardcoded property */
27
+ fixes: TokenFixDetail[];
28
+
29
+ /** Markdown-formatted context for AI agents */
30
+ markdown: string;
31
+
32
+ /** JSON-formatted context for programmatic use */
33
+ json: {
34
+ component: string;
35
+ compliancePercent: number;
36
+ totalProperties: number;
37
+ hardcodedCount: number;
38
+ fixes: Array<{
39
+ property: string;
40
+ currentValue: string;
41
+ suggestedToken: string;
42
+ tokenValue: string;
43
+ codeFix: string;
44
+ reason: string;
45
+ }>;
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Detailed fix for a single property
51
+ */
52
+ export interface TokenFixDetail {
53
+ /** CSS property name (e.g., "backgroundColor") */
54
+ property: string;
55
+
56
+ /** CSS property name in kebab-case (e.g., "background-color") */
57
+ cssProperty: string;
58
+
59
+ /** Current hardcoded value */
60
+ currentValue: string;
61
+
62
+ /** Token that should be used */
63
+ suggestedToken: DesignToken;
64
+
65
+ /** The code fix to apply */
66
+ codeFix: string;
67
+
68
+ /** Confidence score 0-1 */
69
+ confidence: number;
70
+
71
+ /** Human-readable explanation */
72
+ reason: string;
73
+ }
74
+
75
+ /**
76
+ * Generate token fixes for a set of style diffs
77
+ */
78
+ export function generateTokenFixes(
79
+ componentName: string,
80
+ styleDiffs: Array<{
81
+ property: string;
82
+ figma: string;
83
+ rendered: string;
84
+ match: boolean;
85
+ }>,
86
+ registry: TokenRegistryManager,
87
+ theme = "default"
88
+ ): TokenFixContext {
89
+ // Calculate usage summary
90
+ const summary = registry.calculateUsageSummary(styleDiffs, theme);
91
+
92
+ // Generate detailed fixes
93
+ const fixes: TokenFixDetail[] = [];
94
+
95
+ for (const hardcoded of summary.hardcodedProperties) {
96
+ if (!hardcoded.suggestedFix) continue;
97
+
98
+ const token = registry.getToken(hardcoded.suggestedFix.tokenName);
99
+ if (!token) continue;
100
+
101
+ const cssProperty = toCssProperty(hardcoded.property);
102
+
103
+ fixes.push({
104
+ property: hardcoded.property,
105
+ cssProperty,
106
+ currentValue: hardcoded.rendered,
107
+ suggestedToken: token,
108
+ codeFix: hardcoded.suggestedFix.codeFix,
109
+ confidence: hardcoded.suggestedFix.confidence,
110
+ reason: hardcoded.suggestedFix.reason,
111
+ });
112
+ }
113
+
114
+ // Generate markdown context for AI
115
+ const markdown = generateMarkdownContext(componentName, summary, fixes);
116
+
117
+ // Generate JSON context
118
+ const json = {
119
+ component: componentName,
120
+ compliancePercent: summary.compliancePercent,
121
+ totalProperties: summary.totalProperties,
122
+ hardcodedCount: summary.hardcoded,
123
+ fixes: fixes.map((f) => ({
124
+ property: f.property,
125
+ currentValue: f.currentValue,
126
+ suggestedToken: f.suggestedToken.name,
127
+ tokenValue: f.suggestedToken.resolvedValue,
128
+ codeFix: f.codeFix,
129
+ reason: f.reason,
130
+ })),
131
+ };
132
+
133
+ return {
134
+ componentName,
135
+ summary,
136
+ fixes,
137
+ markdown,
138
+ json,
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Generate markdown context for AI agents
144
+ */
145
+ function generateMarkdownContext(
146
+ componentName: string,
147
+ summary: TokenUsageSummary,
148
+ fixes: TokenFixDetail[]
149
+ ): string {
150
+ const lines: string[] = [];
151
+
152
+ // Header
153
+ lines.push(`## Design Token Analysis: ${componentName}`);
154
+ lines.push("");
155
+
156
+ // Summary
157
+ lines.push(`### Compliance: ${summary.compliancePercent}%`);
158
+ lines.push("");
159
+ lines.push(`| Metric | Count |`);
160
+ lines.push(`|--------|-------|`);
161
+ lines.push(`| Total Properties | ${summary.totalProperties} |`);
162
+ lines.push(`| Using Tokens | ${summary.usingTokens} |`);
163
+ lines.push(`| Hardcoded | ${summary.hardcoded} |`);
164
+ lines.push(`| Implicit Matches | ${summary.implicitMatches} |`);
165
+ lines.push("");
166
+
167
+ if (fixes.length === 0) {
168
+ lines.push("✅ **All styles are using design tokens correctly.**");
169
+ return lines.join("\n");
170
+ }
171
+
172
+ // Issues
173
+ lines.push(`### Issues Found: ${fixes.length} hardcoded value(s)`);
174
+ lines.push("");
175
+
176
+ for (let i = 0; i < fixes.length; i++) {
177
+ const fix = fixes[i];
178
+ lines.push(`#### ${i + 1}. \`${fix.cssProperty}\``);
179
+ lines.push("");
180
+ lines.push(`- **Current Value**: \`${fix.currentValue}\``);
181
+ lines.push(`- **Suggested Token**: \`${fix.suggestedToken.name}\` (${fix.suggestedToken.resolvedValue})`);
182
+ lines.push(`- **Confidence**: ${Math.round(fix.confidence * 100)}%`);
183
+ lines.push("");
184
+ lines.push("**Fix:**");
185
+ lines.push("```css");
186
+ lines.push(`/* Before */ ${fix.cssProperty}: ${fix.currentValue};`);
187
+ lines.push(`/* After */ ${fix.codeFix}`);
188
+ lines.push("```");
189
+ lines.push("");
190
+ lines.push(`> ${fix.reason}`);
191
+ lines.push("");
192
+ }
193
+
194
+ // Why use tokens
195
+ lines.push("### Why Use Design Tokens?");
196
+ lines.push("");
197
+ lines.push("1. **Consistency**: Ensures visual consistency across the application");
198
+ lines.push("2. **Maintainability**: Change a token value once, update everywhere");
199
+ lines.push("3. **Theming**: Enables dark mode and other theme variations");
200
+ lines.push("4. **Design System Compliance**: Keeps code aligned with design specifications");
201
+ lines.push("");
202
+
203
+ return lines.join("\n");
204
+ }
205
+
206
+ /**
207
+ * Generate AI context for a single component's token issues
208
+ *
209
+ * This format is optimized for AI agents to understand and fix
210
+ */
211
+ export function generateAIFixContext(
212
+ componentName: string,
213
+ hardcodedItems: EnhancedStyleDiffItem[],
214
+ registry: TokenRegistryManager
215
+ ): string {
216
+ if (hardcodedItems.length === 0) {
217
+ return `Component "${componentName}" is fully compliant with design tokens.`;
218
+ }
219
+
220
+ const lines: string[] = [];
221
+
222
+ lines.push(`# Design Token Fix Required: ${componentName}`);
223
+ lines.push("");
224
+ lines.push(`Found ${hardcodedItems.length} hardcoded CSS value(s) that should use design tokens.`);
225
+ lines.push("");
226
+
227
+ for (const item of hardcodedItems) {
228
+ if (!item.suggestedFix) continue;
229
+
230
+ const token = registry.getToken(item.suggestedFix.tokenName);
231
+ const cssProperty = toCssProperty(item.property);
232
+
233
+ lines.push(`## Issue: Hardcoded \`${cssProperty}\``);
234
+ lines.push("");
235
+ lines.push("| | |");
236
+ lines.push("|---|---|");
237
+ lines.push(`| **Property** | \`${cssProperty}\` |`);
238
+ lines.push(`| **Current Value** | \`${item.rendered}\` |`);
239
+ lines.push(`| **Expected Token** | \`${item.suggestedFix.tokenName}\` |`);
240
+ lines.push(`| **Token Value** | \`${token?.resolvedValue || item.suggestedFix.tokenValue}\` |`);
241
+ lines.push("");
242
+ lines.push("**Suggested Fix:**");
243
+ lines.push("```css");
244
+ lines.push(`/* Replace hardcoded value */`);
245
+ lines.push(`${cssProperty}: var(${item.suggestedFix.tokenName});`);
246
+ lines.push("```");
247
+ lines.push("");
248
+
249
+ if (token) {
250
+ lines.push("**Token Details:**");
251
+ lines.push(`- Category: ${token.category}`);
252
+ lines.push(`- Level: ${token.level === 1 ? "Base" : token.level === 2 ? "Semantic" : "Component"}`);
253
+ lines.push(`- Theme: ${token.theme}`);
254
+ if (token.description) {
255
+ lines.push(`- Description: ${token.description}`);
256
+ }
257
+ lines.push("");
258
+ }
259
+
260
+ lines.push("---");
261
+ lines.push("");
262
+ }
263
+
264
+ lines.push("## Why This Matters");
265
+ lines.push("");
266
+ lines.push("Using design tokens instead of hardcoded values:");
267
+ lines.push("- Ensures the component matches the Figma design");
268
+ lines.push("- Enables automatic theme switching (light/dark mode)");
269
+ lines.push("- Maintains consistency across the design system");
270
+ lines.push("- Makes future design updates easier");
271
+ lines.push("");
272
+
273
+ return lines.join("\n");
274
+ }
275
+
276
+ /**
277
+ * Convert camelCase to kebab-case
278
+ */
279
+ function toCssProperty(prop: string): string {
280
+ return prop.replace(/([A-Z])/g, "-$1").toLowerCase();
281
+ }