@fragments-sdk/cli 0.10.1 → 0.12.1

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 (223) hide show
  1. package/dist/ai-client-I6MDWNYA.js +21 -0
  2. package/dist/bin.js +292 -367
  3. package/dist/bin.js.map +1 -1
  4. package/dist/{chunk-PW7QTQA6.js → chunk-4OC7FTJB.js} +2 -2
  5. package/dist/{chunk-HRFUSSZI.js → chunk-AM4MRTMN.js} +2 -2
  6. package/dist/{chunk-5G3VZH43.js → chunk-GVDSFQ4E.js} +281 -351
  7. package/dist/chunk-GVDSFQ4E.js.map +1 -0
  8. package/dist/chunk-JJ2VRTBU.js +626 -0
  9. package/dist/chunk-JJ2VRTBU.js.map +1 -0
  10. package/dist/{chunk-D5PYOXEI.js → chunk-LVWFOLUZ.js} +148 -13
  11. package/dist/{chunk-D5PYOXEI.js.map → chunk-LVWFOLUZ.js.map} +1 -1
  12. package/dist/{chunk-WXSR2II7.js → chunk-OQKMEFOS.js} +58 -6
  13. package/dist/chunk-OQKMEFOS.js.map +1 -0
  14. package/dist/chunk-SXTKFDCR.js +104 -0
  15. package/dist/chunk-SXTKFDCR.js.map +1 -0
  16. package/dist/chunk-T5OMVL7E.js +443 -0
  17. package/dist/chunk-T5OMVL7E.js.map +1 -0
  18. package/dist/{chunk-ZM4ZQZWZ.js → chunk-TPWGL2XS.js} +39 -37
  19. package/dist/chunk-TPWGL2XS.js.map +1 -0
  20. package/dist/{chunk-OQO55NKV.js → chunk-WFS63PCW.js} +85 -11
  21. package/dist/chunk-WFS63PCW.js.map +1 -0
  22. package/dist/core/index.js +9 -1
  23. package/dist/{discovery-NEOY4MPN.js → discovery-ZJQSXF56.js} +3 -3
  24. package/dist/{generate-FBHSXR3D.js → generate-RJFS2JWA.js} +4 -4
  25. package/dist/index.js +7 -6
  26. package/dist/index.js.map +1 -1
  27. package/dist/init-ZSX3NRCZ.js +636 -0
  28. package/dist/init-ZSX3NRCZ.js.map +1 -0
  29. package/dist/mcp-bin.js +2 -2
  30. package/dist/{scan-CJF2DOQW.js → scan-3PMCJ4RB.js} +6 -6
  31. package/dist/scan-generate-SYU4PYZD.js +1115 -0
  32. package/dist/scan-generate-SYU4PYZD.js.map +1 -0
  33. package/dist/{service-TQYWY65E.js → service-VMGNJZ42.js} +3 -3
  34. package/dist/snapshot-XOISO2IS.js +139 -0
  35. package/dist/snapshot-XOISO2IS.js.map +1 -0
  36. package/dist/{static-viewer-NUBFPKWH.js → static-viewer-5GXH2MGE.js} +3 -3
  37. package/dist/static-viewer-5GXH2MGE.js.map +1 -0
  38. package/dist/{test-Z5LVO724.js → test-SI4NSHQX.js} +4 -4
  39. package/dist/{tokens-CE46OTMD.js → tokens-T6SIVUT5.js} +5 -5
  40. package/dist/{viewer-DNMNC5VS.js → viewer-7ZEAFBVN.js} +80 -58
  41. package/dist/viewer-7ZEAFBVN.js.map +1 -0
  42. package/package.json +6 -14
  43. package/src/ai-client.ts +156 -0
  44. package/src/bin.ts +74 -2
  45. package/src/build.ts +95 -33
  46. package/src/commands/__tests__/drift-sync.test.ts +252 -0
  47. package/src/commands/__tests__/scan-generate.test.ts +497 -45
  48. package/src/commands/enhance.ts +11 -35
  49. package/src/commands/init.ts +296 -193
  50. package/src/commands/scan-generate.ts +740 -139
  51. package/src/commands/scan.ts +37 -32
  52. package/src/commands/setup.ts +143 -52
  53. package/src/commands/snapshot.ts +197 -0
  54. package/src/commands/sync.ts +357 -0
  55. package/src/commands/validate.ts +43 -1
  56. package/src/core/component-extractor.test.ts +282 -0
  57. package/src/core/component-extractor.ts +1030 -0
  58. package/src/core/discovery.ts +93 -7
  59. package/src/service/enhance/props-extractor.ts +235 -13
  60. package/src/validators.ts +236 -0
  61. package/src/viewer/__tests__/viewer-integration.test.ts +85 -74
  62. package/src/viewer/server.ts +37 -22
  63. package/src/viewer/vite-plugin.ts +25 -9
  64. package/dist/chunk-5G3VZH43.js.map +0 -1
  65. package/dist/chunk-OQO55NKV.js.map +0 -1
  66. package/dist/chunk-WXSR2II7.js.map +0 -1
  67. package/dist/chunk-ZM4ZQZWZ.js.map +0 -1
  68. package/dist/init-NDQXUWDU.js +0 -796
  69. package/dist/init-NDQXUWDU.js.map +0 -1
  70. package/dist/scan-generate-SJAN5MVI.js +0 -691
  71. package/dist/scan-generate-SJAN5MVI.js.map +0 -1
  72. package/dist/viewer-DNMNC5VS.js.map +0 -1
  73. package/src/ai.ts +0 -266
  74. package/src/commands/init-framework.ts +0 -414
  75. package/src/mcp/bin.ts +0 -36
  76. package/src/migrate/bin.ts +0 -114
  77. package/src/theme/index.ts +0 -77
  78. package/src/viewer/__tests__/a11y-fixes.test.ts +0 -358
  79. package/src/viewer/__tests__/jsx-parser.test.ts +0 -502
  80. package/src/viewer/__tests__/render-utils.test.ts +0 -232
  81. package/src/viewer/__tests__/style-utils.test.ts +0 -404
  82. package/src/viewer/assets/fragments-logo.ts +0 -4
  83. package/src/viewer/assets/fragments_logo.png +0 -0
  84. package/src/viewer/bin.ts +0 -86
  85. package/src/viewer/cli/health.ts +0 -256
  86. package/src/viewer/cli/index.ts +0 -33
  87. package/src/viewer/cli/scan.ts +0 -124
  88. package/src/viewer/cli/utils.ts +0 -174
  89. package/src/viewer/components/AccessibilityPanel.tsx +0 -1457
  90. package/src/viewer/components/ActionCapture.tsx +0 -172
  91. package/src/viewer/components/ActionsPanel.tsx +0 -332
  92. package/src/viewer/components/AllVariantsPreview.tsx +0 -78
  93. package/src/viewer/components/App.tsx +0 -582
  94. package/src/viewer/components/BottomPanel.tsx +0 -288
  95. package/src/viewer/components/CodePanel.naming.test.tsx +0 -59
  96. package/src/viewer/components/CodePanel.tsx +0 -118
  97. package/src/viewer/components/CommandPalette.tsx +0 -392
  98. package/src/viewer/components/ComponentDocView.tsx +0 -164
  99. package/src/viewer/components/ComponentGraph.tsx +0 -380
  100. package/src/viewer/components/ComponentHeader.tsx +0 -88
  101. package/src/viewer/components/ContractPanel.tsx +0 -241
  102. package/src/viewer/components/EmptyVariantMessage.tsx +0 -54
  103. package/src/viewer/components/ErrorBoundary.tsx +0 -97
  104. package/src/viewer/components/FigmaEmbed.tsx +0 -238
  105. package/src/viewer/components/FragmentEditor.tsx +0 -525
  106. package/src/viewer/components/FragmentRenderer.tsx +0 -61
  107. package/src/viewer/components/HeaderSearch.tsx +0 -24
  108. package/src/viewer/components/HealthDashboard.tsx +0 -441
  109. package/src/viewer/components/HmrStatusIndicator.tsx +0 -61
  110. package/src/viewer/components/Icons.tsx +0 -479
  111. package/src/viewer/components/InteractionsPanel.tsx +0 -757
  112. package/src/viewer/components/IsolatedPreviewFrame.tsx +0 -346
  113. package/src/viewer/components/IsolatedRender.tsx +0 -113
  114. package/src/viewer/components/KeyboardShortcutsHelp.tsx +0 -53
  115. package/src/viewer/components/LandingPage.tsx +0 -421
  116. package/src/viewer/components/Layout.tsx +0 -27
  117. package/src/viewer/components/LeftSidebar.tsx +0 -472
  118. package/src/viewer/components/LoadErrorMessage.tsx +0 -102
  119. package/src/viewer/components/MultiViewportPreview.tsx +0 -522
  120. package/src/viewer/components/NoVariantsMessage.tsx +0 -59
  121. package/src/viewer/components/PanelShell.tsx +0 -161
  122. package/src/viewer/components/PerformancePanel.tsx +0 -304
  123. package/src/viewer/components/PreviewArea.tsx +0 -472
  124. package/src/viewer/components/PreviewAside.tsx +0 -168
  125. package/src/viewer/components/PreviewFrameHost.tsx +0 -303
  126. package/src/viewer/components/PreviewPane.tsx +0 -149
  127. package/src/viewer/components/PreviewToolbar.tsx +0 -80
  128. package/src/viewer/components/PropsEditor.tsx +0 -506
  129. package/src/viewer/components/PropsTable.tsx +0 -111
  130. package/src/viewer/components/RelationsSection.tsx +0 -88
  131. package/src/viewer/components/ResizablePanel.tsx +0 -271
  132. package/src/viewer/components/RightSidebar.tsx +0 -102
  133. package/src/viewer/components/RuntimeToolsRegistrar.tsx +0 -17
  134. package/src/viewer/components/ScreenshotButton.tsx +0 -90
  135. package/src/viewer/components/Sidebar.tsx +0 -169
  136. package/src/viewer/components/SkeletonLoader.tsx +0 -161
  137. package/src/viewer/components/ThemeProvider.tsx +0 -42
  138. package/src/viewer/components/Toast.tsx +0 -3
  139. package/src/viewer/components/TokenStylePanel.tsx +0 -699
  140. package/src/viewer/components/TopToolbar.tsx +0 -159
  141. package/src/viewer/components/UsageSection.tsx +0 -95
  142. package/src/viewer/components/VariantMatrix.tsx +0 -388
  143. package/src/viewer/components/VariantRenderer.tsx +0 -131
  144. package/src/viewer/components/VariantTabs.tsx +0 -40
  145. package/src/viewer/components/ViewerHeader.tsx +0 -69
  146. package/src/viewer/components/ViewerStateSync.tsx +0 -52
  147. package/src/viewer/components/ViewportSelector.tsx +0 -172
  148. package/src/viewer/components/WebMCPDevTools.tsx +0 -503
  149. package/src/viewer/components/WebMCPIntegration.tsx +0 -47
  150. package/src/viewer/components/WebMCPStatusIndicator.tsx +0 -60
  151. package/src/viewer/components/_future/CreatePage.tsx +0 -836
  152. package/src/viewer/components/viewer-utils.ts +0 -16
  153. package/src/viewer/composition-renderer.ts +0 -381
  154. package/src/viewer/constants/index.ts +0 -1
  155. package/src/viewer/constants/ui.ts +0 -166
  156. package/src/viewer/entry.tsx +0 -335
  157. package/src/viewer/hooks/index.ts +0 -2
  158. package/src/viewer/hooks/useA11yCache.ts +0 -383
  159. package/src/viewer/hooks/useA11yService.ts +0 -364
  160. package/src/viewer/hooks/useActions.ts +0 -138
  161. package/src/viewer/hooks/useAppState.ts +0 -147
  162. package/src/viewer/hooks/useCompiledFragments.ts +0 -42
  163. package/src/viewer/hooks/useFigmaIntegration.ts +0 -132
  164. package/src/viewer/hooks/useHmrStatus.ts +0 -109
  165. package/src/viewer/hooks/useKeyboardShortcuts.ts +0 -270
  166. package/src/viewer/hooks/usePreviewBridge.ts +0 -347
  167. package/src/viewer/hooks/useScrollSpy.ts +0 -78
  168. package/src/viewer/hooks/useUrlState.ts +0 -318
  169. package/src/viewer/hooks/useViewSettings.ts +0 -111
  170. package/src/viewer/index.html +0 -28
  171. package/src/viewer/intelligence/healthReport.ts +0 -505
  172. package/src/viewer/intelligence/styleDrift.ts +0 -340
  173. package/src/viewer/intelligence/usageScanner.ts +0 -309
  174. package/src/viewer/jsx-parser.ts +0 -486
  175. package/src/viewer/preview-frame-entry.tsx +0 -25
  176. package/src/viewer/preview-frame.html +0 -125
  177. package/src/viewer/public/favicon.ico +0 -0
  178. package/src/viewer/render-template.html +0 -68
  179. package/src/viewer/styles/globals.css +0 -278
  180. package/src/viewer/types/a11y.ts +0 -197
  181. package/src/viewer/utils/a11y-fixes.ts +0 -509
  182. package/src/viewer/utils/actionExport.ts +0 -372
  183. package/src/viewer/utils/colorSchemes.ts +0 -201
  184. package/src/viewer/utils/detectRelationships.ts +0 -256
  185. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss +0 -10
  186. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss.d.ts +0 -2
  187. package/src/viewer/vendor/shared/src/ComponentDocContent.tsx +0 -274
  188. package/src/viewer/vendor/shared/src/DocsHeaderBar.tsx +0 -129
  189. package/src/viewer/vendor/shared/src/DocsPageAsideHost.tsx +0 -89
  190. package/src/viewer/vendor/shared/src/DocsPageShell.tsx +0 -124
  191. package/src/viewer/vendor/shared/src/DocsSearchCommand.tsx +0 -99
  192. package/src/viewer/vendor/shared/src/DocsSidebarNav.tsx +0 -66
  193. package/src/viewer/vendor/shared/src/PropsTable.module.scss +0 -68
  194. package/src/viewer/vendor/shared/src/PropsTable.module.scss.d.ts +0 -2
  195. package/src/viewer/vendor/shared/src/PropsTable.tsx +0 -76
  196. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss +0 -114
  197. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss.d.ts +0 -2
  198. package/src/viewer/vendor/shared/src/VariantPreviewCard.tsx +0 -137
  199. package/src/viewer/vendor/shared/src/docs-data/index.ts +0 -32
  200. package/src/viewer/vendor/shared/src/docs-data/mcp-configs.ts +0 -72
  201. package/src/viewer/vendor/shared/src/docs-data/palettes.ts +0 -75
  202. package/src/viewer/vendor/shared/src/docs-data/setup-examples.ts +0 -55
  203. package/src/viewer/vendor/shared/src/docs-layout.scss +0 -28
  204. package/src/viewer/vendor/shared/src/docs-layout.scss.d.ts +0 -2
  205. package/src/viewer/vendor/shared/src/index.ts +0 -34
  206. package/src/viewer/vendor/shared/src/types.ts +0 -53
  207. package/src/viewer/webmcp/__tests__/analytics.test.ts +0 -108
  208. package/src/viewer/webmcp/analytics.ts +0 -165
  209. package/src/viewer/webmcp/index.ts +0 -3
  210. package/src/viewer/webmcp/posthog-bridge.ts +0 -39
  211. package/src/viewer/webmcp/runtime-tools.ts +0 -152
  212. package/src/viewer/webmcp/scan-utils.ts +0 -135
  213. package/src/viewer/webmcp/use-tool-analytics.ts +0 -69
  214. package/src/viewer/webmcp/viewer-state.ts +0 -45
  215. /package/dist/{discovery-NEOY4MPN.js.map → ai-client-I6MDWNYA.js.map} +0 -0
  216. /package/dist/{chunk-PW7QTQA6.js.map → chunk-4OC7FTJB.js.map} +0 -0
  217. /package/dist/{chunk-HRFUSSZI.js.map → chunk-AM4MRTMN.js.map} +0 -0
  218. /package/dist/{scan-CJF2DOQW.js.map → discovery-ZJQSXF56.js.map} +0 -0
  219. /package/dist/{generate-FBHSXR3D.js.map → generate-RJFS2JWA.js.map} +0 -0
  220. /package/dist/{service-TQYWY65E.js.map → scan-3PMCJ4RB.js.map} +0 -0
  221. /package/dist/{static-viewer-NUBFPKWH.js.map → service-VMGNJZ42.js.map} +0 -0
  222. /package/dist/{test-Z5LVO724.js.map → test-SI4NSHQX.js.map} +0 -0
  223. /package/dist/{tokens-CE46OTMD.js.map → tokens-T6SIVUT5.js.map} +0 -0
@@ -0,0 +1,357 @@
1
+ /**
2
+ * fragments sync — Auto-update fragment files from component source.
3
+ *
4
+ * Detects drift between component source and fragment documentation,
5
+ * then updates fragment files to match. Preserves human-authored fields
6
+ * (usage, description, variants) while updating machine-derivable fields
7
+ * (props, composition, contract).
8
+ */
9
+
10
+ import pc from 'picocolors';
11
+ import { readFile, writeFile } from 'node:fs/promises';
12
+ import { BRAND } from '../core/index.js';
13
+ import { loadConfig } from '../core/node.js';
14
+ import { discoverFragmentFiles, loadFragmentFile } from '../core/node.js';
15
+ import { parseFragmentFile } from '../core/parser.js';
16
+ import { resolveComponentSourcePath } from '../core/auto-props.js';
17
+ import { createComponentExtractor, type PropMeta, type CompositionMeta } from '../core/component-extractor.js';
18
+ import type { FragmentsConfig } from '@fragments-sdk/core';
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Public types
22
+ // ---------------------------------------------------------------------------
23
+
24
+ export interface SyncOptions {
25
+ /** Path to config file */
26
+ config?: string;
27
+ /** Path to tsconfig.json */
28
+ tsconfig?: string;
29
+ /** Preview changes without writing */
30
+ dryRun?: boolean;
31
+ /** Sync specific component only */
32
+ component?: string;
33
+ }
34
+
35
+ export interface SyncedComponent {
36
+ name: string;
37
+ file: string;
38
+ changes: string[];
39
+ }
40
+
41
+ export interface SyncResult {
42
+ success: boolean;
43
+ updated: SyncedComponent[];
44
+ skipped: Array<{ name: string; reason: string }>;
45
+ errors: Array<{ file: string; message: string }>;
46
+ }
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Command
50
+ // ---------------------------------------------------------------------------
51
+
52
+ export async function sync(options: SyncOptions = {}): Promise<SyncResult> {
53
+ const { config, configDir } = await loadConfig(options.config);
54
+
55
+ console.log(pc.cyan(`\n${BRAND.name} Sync\n`));
56
+
57
+ if (options.dryRun) {
58
+ console.log(pc.dim('Dry run — no files will be modified.\n'));
59
+ }
60
+
61
+ const result = await runSync(config, configDir, options);
62
+
63
+ // Print updated
64
+ if (result.updated.length > 0) {
65
+ const verb = options.dryRun ? 'Would update' : 'Updated';
66
+ console.log(pc.bold(`${verb} ${result.updated.length} fragment(s):\n`));
67
+ for (const comp of result.updated) {
68
+ console.log(` ${pc.green('✓')} ${pc.bold(comp.name)} ${pc.dim(`(${comp.file})`)}`);
69
+ for (const change of comp.changes) {
70
+ console.log(` ${pc.dim('•')} ${change}`);
71
+ }
72
+ }
73
+ console.log();
74
+ }
75
+
76
+ // Print skipped
77
+ if (result.skipped.length > 0) {
78
+ console.log(pc.dim(`Skipped ${result.skipped.length}: ${result.skipped.map(s => s.name).join(', ')}\n`));
79
+ }
80
+
81
+ // Print errors
82
+ if (result.errors.length > 0) {
83
+ console.log(pc.red(pc.bold('Errors:')));
84
+ for (const err of result.errors) {
85
+ console.log(` ${pc.red('✗')} ${pc.bold(err.file)}: ${err.message}`);
86
+ }
87
+ console.log();
88
+ }
89
+
90
+ // Summary
91
+ if (result.updated.length === 0 && result.errors.length === 0) {
92
+ console.log(pc.green('All fragments are in sync — nothing to update.\n'));
93
+ }
94
+
95
+ return result;
96
+ }
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // Core sync logic
100
+ // ---------------------------------------------------------------------------
101
+
102
+ async function runSync(
103
+ config: FragmentsConfig,
104
+ configDir: string,
105
+ options: SyncOptions
106
+ ): Promise<SyncResult> {
107
+ const fragmentFiles = await discoverFragmentFiles(config, configDir);
108
+ const updated: SyncedComponent[] = [];
109
+ const skipped: Array<{ name: string; reason: string }> = [];
110
+ const errors: Array<{ file: string; message: string }> = [];
111
+
112
+ if (fragmentFiles.length === 0) {
113
+ return { success: true, updated, skipped, errors };
114
+ }
115
+
116
+ const extractor = createComponentExtractor(options.tsconfig);
117
+
118
+ try {
119
+ for (const file of fragmentFiles) {
120
+ try {
121
+ const fragment = await loadFragmentFile(file.absolutePath);
122
+ if (!fragment?.meta?.name) continue;
123
+
124
+ // Filter to specific component if requested
125
+ if (options.component && fragment.meta.name !== options.component) continue;
126
+
127
+ // Parse fragment source to find component import
128
+ const fileContent = await readFile(file.absolutePath, 'utf-8');
129
+ const parsed = parseFragmentFile(fileContent, file.absolutePath);
130
+ if (!parsed.componentImport) {
131
+ skipped.push({ name: fragment.meta.name, reason: 'No component import found' });
132
+ continue;
133
+ }
134
+
135
+ // Resolve source path
136
+ const sourcePath = resolveComponentSourcePath(file.absolutePath, parsed.componentImport);
137
+ if (!sourcePath) {
138
+ skipped.push({ name: fragment.meta.name, reason: 'Cannot resolve component source' });
139
+ continue;
140
+ }
141
+
142
+ // Extract current state from source
143
+ const meta = extractor.extract(sourcePath, fragment.meta.name);
144
+ if (!meta) {
145
+ skipped.push({ name: fragment.meta.name, reason: 'Extraction returned null' });
146
+ continue;
147
+ }
148
+
149
+ // Compute the patch
150
+ const patch = computePatch(fileContent, fragment, meta);
151
+ if (patch.changes.length === 0) {
152
+ skipped.push({ name: fragment.meta.name, reason: 'Already in sync' });
153
+ continue;
154
+ }
155
+
156
+ if (!options.dryRun) {
157
+ await writeFile(file.absolutePath, patch.updatedContent, 'utf-8');
158
+ }
159
+
160
+ updated.push({
161
+ name: fragment.meta.name,
162
+ file: file.relativePath,
163
+ changes: patch.changes,
164
+ });
165
+ } catch (err) {
166
+ errors.push({
167
+ file: file.relativePath,
168
+ message: err instanceof Error ? err.message : String(err),
169
+ });
170
+ }
171
+ }
172
+ } finally {
173
+ extractor.dispose();
174
+ }
175
+
176
+ return {
177
+ success: errors.length === 0,
178
+ updated,
179
+ skipped,
180
+ errors,
181
+ };
182
+ }
183
+
184
+ // ---------------------------------------------------------------------------
185
+ // Patch computation — text-level updates to fragment file
186
+ // ---------------------------------------------------------------------------
187
+
188
+ interface PatchResult {
189
+ updatedContent: string;
190
+ changes: string[];
191
+ }
192
+
193
+ /**
194
+ * Compute text-level patches for a fragment file based on extracted metadata.
195
+ *
196
+ * Strategy: find the `props: { ... }` block in the source and replace it with
197
+ * an updated version that includes new props. Preserves human-authored prop
198
+ * entries (descriptions, constraints) while adding missing props and removing
199
+ * stale ones.
200
+ */
201
+ function computePatch(
202
+ fileContent: string,
203
+ fragment: { meta: { name: string }; props: Record<string, unknown>; ai?: { compositionPattern?: string; subComponents?: string[] } },
204
+ meta: { props: Record<string, PropMeta>; composition: CompositionMeta | null }
205
+ ): PatchResult {
206
+ const changes: string[] = [];
207
+ let content = fileContent;
208
+
209
+ // Filter to local props only
210
+ const localSourceProps = Object.fromEntries(
211
+ Object.entries(meta.props).filter(([_, p]) => p.source === 'local')
212
+ );
213
+
214
+ // Detect added props
215
+ const addedProps: string[] = [];
216
+ for (const name of Object.keys(localSourceProps)) {
217
+ if (!(name in fragment.props)) {
218
+ addedProps.push(name);
219
+ }
220
+ }
221
+
222
+ // Detect removed props
223
+ const removedProps: string[] = [];
224
+ for (const name of Object.keys(fragment.props)) {
225
+ if (!(name in localSourceProps)) {
226
+ removedProps.push(name);
227
+ }
228
+ }
229
+
230
+ // Apply prop additions — insert before the closing brace of the props block
231
+ if (addedProps.length > 0) {
232
+ const propsBlockEnd = findPropsBlockEnd(content);
233
+ if (propsBlockEnd !== -1) {
234
+ const newEntries = addedProps.map(name => {
235
+ const prop = localSourceProps[name];
236
+ return formatPropEntry(name, prop);
237
+ }).join('\n');
238
+
239
+ content = content.slice(0, propsBlockEnd) + newEntries + '\n ' + content.slice(propsBlockEnd);
240
+ changes.push(`Added props: ${addedProps.join(', ')}`);
241
+ }
242
+ }
243
+
244
+ // Apply prop removals — comment out removed props
245
+ if (removedProps.length > 0) {
246
+ for (const name of removedProps) {
247
+ // Match the prop entry line(s) — find `propName: {` and its block
248
+ const propRegex = new RegExp(`([ \\t]*)${escapeRegex(name)}:\\s*\\{`, 'g');
249
+ const match = propRegex.exec(content);
250
+ if (match) {
251
+ const indent = match[1];
252
+ const startIdx = match.index;
253
+ // Find the matching closing brace
254
+ const endIdx = findMatchingBrace(content, match.index + match[0].length - 1);
255
+ if (endIdx !== -1) {
256
+ // Find the end of the line after the closing brace (include trailing comma)
257
+ let lineEnd = endIdx + 1;
258
+ if (content[lineEnd] === ',') lineEnd++;
259
+ if (content[lineEnd] === '\n') lineEnd++;
260
+
261
+ const removedBlock = content.slice(startIdx, lineEnd);
262
+ const commented = removedBlock
263
+ .split('\n')
264
+ .map(line => line ? `${indent}// [drift:removed] ${line.trimStart()}` : '')
265
+ .join('\n');
266
+ content = content.slice(0, startIdx) + commented + content.slice(lineEnd);
267
+ }
268
+ }
269
+ }
270
+ changes.push(`Removed props: ${removedProps.join(', ')}`);
271
+ }
272
+
273
+ // Sync composition in ai block
274
+ if (meta.composition && !fragment.ai?.compositionPattern) {
275
+ changes.push(`Composition: "${meta.composition.pattern}" pattern detected`);
276
+ }
277
+
278
+ return { updatedContent: content, changes };
279
+ }
280
+
281
+ // ---------------------------------------------------------------------------
282
+ // Helpers
283
+ // ---------------------------------------------------------------------------
284
+
285
+ /** Find the closing `}` of the top-level `props: { ... }` block */
286
+ function findPropsBlockEnd(content: string): number {
287
+ const propsStart = content.search(/\bprops:\s*\{/);
288
+ if (propsStart === -1) return -1;
289
+ const braceStart = content.indexOf('{', propsStart);
290
+ return findMatchingBrace(content, braceStart);
291
+ }
292
+
293
+ /** Find the matching closing brace for an opening brace at `start` */
294
+ function findMatchingBrace(content: string, start: number): number {
295
+ let depth = 0;
296
+ let inString: string | null = null;
297
+ let escaped = false;
298
+
299
+ for (let i = start; i < content.length; i++) {
300
+ const ch = content[i];
301
+
302
+ if (escaped) {
303
+ escaped = false;
304
+ continue;
305
+ }
306
+
307
+ if (ch === '\\') {
308
+ escaped = true;
309
+ continue;
310
+ }
311
+
312
+ if (inString) {
313
+ if (ch === inString) inString = null;
314
+ continue;
315
+ }
316
+
317
+ if (ch === "'" || ch === '"' || ch === '`') {
318
+ inString = ch;
319
+ continue;
320
+ }
321
+
322
+ if (ch === '{') depth++;
323
+ else if (ch === '}') {
324
+ depth--;
325
+ if (depth === 0) return i;
326
+ }
327
+ }
328
+
329
+ return -1;
330
+ }
331
+
332
+ /** Format a PropMeta into a fragment prop entry string */
333
+ function formatPropEntry(name: string, prop: PropMeta): string {
334
+ const lines: string[] = [];
335
+ lines.push(` ${name}: {`);
336
+ lines.push(` type: '${prop.typeKind}',`);
337
+ if (prop.description) {
338
+ lines.push(` description: ${JSON.stringify(prop.description)},`);
339
+ } else {
340
+ lines.push(` description: '', // TODO: add description`);
341
+ }
342
+ if (prop.required) {
343
+ lines.push(` required: true,`);
344
+ }
345
+ if (prop.values && prop.values.length > 0) {
346
+ lines.push(` values: [${prop.values.map(v => `'${v}'`).join(', ')}],`);
347
+ }
348
+ if (prop.default !== undefined) {
349
+ lines.push(` default: '${prop.default}',`);
350
+ }
351
+ lines.push(` },`);
352
+ return lines.join('\n');
353
+ }
354
+
355
+ function escapeRegex(str: string): string {
356
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
357
+ }
@@ -5,7 +5,8 @@
5
5
  import pc from 'picocolors';
6
6
  import { BRAND } from '../core/index.js';
7
7
  import { loadConfig } from '../core/node.js';
8
- import { validateSchema, validateCoverage, validateAll, validateSnippets } from '../validators.js';
8
+ import { validateSchema, validateCoverage, validateAll, validateSnippets, validateDrift } from '../validators.js';
9
+ import type { DriftValidationResult } from '../validators.js';
9
10
 
10
11
  /**
11
12
  * Options for validate command
@@ -19,6 +20,10 @@ export interface ValidateOptions {
19
20
  coverage?: boolean;
20
21
  /** Validate snippet/render policy only */
21
22
  snippets?: boolean;
23
+ /** Detect metadata drift between source and fragments */
24
+ drift?: boolean;
25
+ /** Path to tsconfig.json for drift detection */
26
+ tsconfig?: string;
22
27
  /** Override snippet policy mode for this run */
23
28
  snippetMode?: 'warn' | 'error';
24
29
  /** Start component name for alphabetical snippet batch validation */
@@ -68,6 +73,11 @@ export async function validate(options: ValidateOptions = {}): Promise<ValidateR
68
73
  componentStart: options.componentStart,
69
74
  componentLimit,
70
75
  });
76
+ } else if (options.drift) {
77
+ console.log(pc.dim('Running drift detection...\n'));
78
+ const driftResult = await validateDrift(config, configDir, { tsconfig: options.tsconfig });
79
+ result = driftResult;
80
+ printDriftReport(driftResult);
71
81
  } else {
72
82
  console.log(pc.dim('Running all validations...\n'));
73
83
  result = await validateAll(config, configDir, {
@@ -113,3 +123,35 @@ export async function validate(options: ValidateOptions = {}): Promise<ValidateR
113
123
 
114
124
  return result;
115
125
  }
126
+
127
+ /**
128
+ * Print a structured drift report grouped by component
129
+ */
130
+ function printDriftReport(result: DriftValidationResult): void {
131
+ if (result.reports.length === 0) {
132
+ console.log(pc.green('No drift detected — fragments are in sync with source.\n'));
133
+ return;
134
+ }
135
+
136
+ console.log(pc.bold(`Drift detected in ${result.reports.length} component(s):\n`));
137
+
138
+ for (const report of result.reports) {
139
+ console.log(` ${pc.bold(report.component)} ${pc.dim(`(${report.file})`)}`);
140
+
141
+ for (const drift of report.drifts) {
142
+ const icon = drift.kind === 'removed' ? pc.red('−') :
143
+ drift.kind === 'added' ? pc.green('+') : pc.yellow('~');
144
+ const label = drift.kind.replace('_', ' ');
145
+ console.log(` ${icon} ${drift.prop}: ${label}`);
146
+ if (drift.kind !== 'added' && drift.kind !== 'removed') {
147
+ console.log(pc.dim(` fragment: ${drift.fragment}`));
148
+ console.log(pc.dim(` source: ${drift.source}`));
149
+ }
150
+ }
151
+
152
+ if (report.compositionDrift) {
153
+ console.log(` ${pc.yellow('~')} composition: ${report.compositionDrift}`);
154
+ }
155
+ console.log();
156
+ }
157
+ }