@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,156 @@
1
+ /**
2
+ * Shared AI client infrastructure for LLM-powered CLI commands.
3
+ *
4
+ * Used by:
5
+ * - `fragments enhance` (usage analysis + documentation generation)
6
+ * - `fragments init --scan --enrich` (knowledge field enrichment)
7
+ *
8
+ * Supports Anthropic (Claude) and OpenAI APIs via dynamic import.
9
+ * No new dependencies — uses existing @anthropic-ai/sdk and openai.
10
+ */
11
+
12
+ export type AIProvider = 'anthropic' | 'openai' | 'none';
13
+
14
+ /**
15
+ * Default lightweight models for enrichment (cheap, fast).
16
+ * Enhance uses its own heavier models locally.
17
+ */
18
+ export const ENRICHMENT_MODELS: Record<AIProvider, string> = {
19
+ anthropic: 'claude-haiku-4-5-20251001',
20
+ openai: 'gpt-4o-mini',
21
+ none: '',
22
+ };
23
+
24
+ /**
25
+ * Detect which AI provider to use based on available API keys and options.
26
+ */
27
+ export function detectProvider(opts?: {
28
+ provider?: AIProvider;
29
+ apiKey?: string;
30
+ }): AIProvider {
31
+ if (opts?.provider) return opts.provider;
32
+ if (opts?.apiKey) {
33
+ if (opts.apiKey.startsWith('sk-ant-')) return 'anthropic';
34
+ if (opts.apiKey.startsWith('sk-')) return 'openai';
35
+ }
36
+ if (process.env.ANTHROPIC_API_KEY) return 'anthropic';
37
+ if (process.env.OPENAI_API_KEY) return 'openai';
38
+ return 'none';
39
+ }
40
+
41
+ /**
42
+ * Resolve the API key for a given provider.
43
+ */
44
+ export function getApiKey(provider: AIProvider, explicitKey?: string): string | undefined {
45
+ if (explicitKey) return explicitKey;
46
+ if (provider === 'anthropic') return process.env.ANTHROPIC_API_KEY;
47
+ if (provider === 'openai') return process.env.OPENAI_API_KEY;
48
+ return undefined;
49
+ }
50
+
51
+ /**
52
+ * Create an AI client for the given provider via dynamic import.
53
+ */
54
+ export async function createAIClient(provider: AIProvider, apiKey: string): Promise<unknown> {
55
+ if (provider === 'anthropic') {
56
+ const Anthropic = (await import('@anthropic-ai/sdk')).default;
57
+ return new Anthropic({ apiKey });
58
+ }
59
+ if (provider === 'openai') {
60
+ const OpenAI = (await import('openai')).default;
61
+ return new OpenAI({ apiKey });
62
+ }
63
+ throw new Error(`Unknown provider: ${provider}`);
64
+ }
65
+
66
+ export interface CompletionResult {
67
+ text: string;
68
+ inputTokens: number;
69
+ outputTokens: number;
70
+ }
71
+
72
+ /**
73
+ * Generate a completion using the appropriate provider API.
74
+ */
75
+ export async function generateCompletion(
76
+ client: unknown,
77
+ provider: AIProvider,
78
+ model: string,
79
+ system: string,
80
+ user: string,
81
+ maxTokens: number = 1024
82
+ ): Promise<CompletionResult> {
83
+ if (provider === 'anthropic') {
84
+ const anthropic = client as import('@anthropic-ai/sdk').default;
85
+ const response = await anthropic.messages.create({
86
+ model,
87
+ max_tokens: maxTokens,
88
+ system,
89
+ messages: [{ role: 'user', content: user }],
90
+ });
91
+
92
+ const content = response.content[0];
93
+ if (content.type !== 'text') {
94
+ throw new Error('Unexpected response type');
95
+ }
96
+
97
+ return {
98
+ text: content.text,
99
+ inputTokens: response.usage?.input_tokens || 0,
100
+ outputTokens: response.usage?.output_tokens || 0,
101
+ };
102
+ }
103
+
104
+ if (provider === 'openai') {
105
+ const openai = client as import('openai').default;
106
+ const response = await openai.chat.completions.create({
107
+ model,
108
+ max_tokens: maxTokens,
109
+ messages: [
110
+ { role: 'system', content: system },
111
+ { role: 'user', content: user },
112
+ ],
113
+ });
114
+
115
+ const content = response.choices[0]?.message?.content;
116
+ if (!content) {
117
+ throw new Error('No response from OpenAI');
118
+ }
119
+
120
+ return {
121
+ text: content,
122
+ inputTokens: response.usage?.prompt_tokens || 0,
123
+ outputTokens: response.usage?.completion_tokens || 0,
124
+ };
125
+ }
126
+
127
+ throw new Error(`Unknown provider: ${provider}`);
128
+ }
129
+
130
+ /**
131
+ * Parse a JSON response from an LLM, handling ```json fences and raw JSON.
132
+ */
133
+ export function parseJSONResponse<T = unknown>(text: string): T {
134
+ const jsonMatch = text.match(/```json\n?([\s\S]*?)\n?```/) || text.match(/\{[\s\S]*\}/);
135
+ const jsonStr = jsonMatch ? (jsonMatch[1] || jsonMatch[0]) : text;
136
+ return JSON.parse(jsonStr);
137
+ }
138
+
139
+ /**
140
+ * Calculate estimated cost based on model and token usage.
141
+ */
142
+ export function calculateCost(model: string, inputTokens: number, outputTokens: number): number {
143
+ // Approximate costs per 1M tokens (input/output)
144
+ const pricing: Record<string, { input: number; output: number }> = {
145
+ // Anthropic
146
+ 'claude-haiku-4-5-20251001': { input: 0.80, output: 4.00 },
147
+ 'claude-sonnet-4-20250514': { input: 3.00, output: 15.00 },
148
+ // OpenAI
149
+ 'gpt-4o-mini': { input: 0.15, output: 0.60 },
150
+ 'gpt-4o': { input: 2.50, output: 10.00 },
151
+ };
152
+
153
+ const modelPricing = pricing[model] || { input: 3.00, output: 15.00 };
154
+ return (inputTokens / 1_000_000) * modelPricing.input +
155
+ (outputTokens / 1_000_000) * modelPricing.output;
156
+ }
package/src/bin.ts CHANGED
@@ -39,6 +39,7 @@ import { graph } from './commands/graph.js';
39
39
  import { perf } from './commands/perf.js';
40
40
  import { doctor } from './commands/doctor.js';
41
41
  import { setup } from './commands/setup.js';
42
+ import { sync } from './commands/sync.js';
42
43
 
43
44
  // Import existing commands that were already extracted
44
45
  import { runScreenshotCommand } from './screenshot.js';
@@ -62,6 +63,8 @@ program
62
63
  .option('--schema', 'Validate fragment schema only')
63
64
  .option('--coverage', 'Validate coverage only')
64
65
  .option('--snippets', 'Validate snippet/render policy only')
66
+ .option('--drift', 'Detect metadata drift between source and fragments')
67
+ .option('--tsconfig <path>', 'Path to tsconfig.json (for drift detection)')
65
68
  .option('--snippet-mode <mode>', 'Override snippet policy mode (warn|error)')
66
69
  .option('--component-start <name>', 'Start component name for alphabetical snippet batch validation')
67
70
  .option('--component-limit <n>', 'Component count for alphabetical snippet batch validation', (value) => Number.parseInt(value, 10))
@@ -77,6 +80,33 @@ program
77
80
  }
78
81
  });
79
82
 
83
+ // ============================================================================
84
+ // SYNC COMMAND
85
+ // ============================================================================
86
+ program
87
+ .command('sync')
88
+ .description('Auto-update fragment files from component source')
89
+ .option('-c, --config <path>', 'Path to config file')
90
+ .option('--tsconfig <path>', 'Path to tsconfig.json')
91
+ .option('--dry-run', 'Preview changes without writing')
92
+ .option('--component <name>', 'Sync specific component only')
93
+ .action(async (options) => {
94
+ try {
95
+ const result = await sync({
96
+ config: options.config,
97
+ tsconfig: options.tsconfig,
98
+ dryRun: options.dryRun,
99
+ component: options.component,
100
+ });
101
+ if (!result.success) {
102
+ process.exit(1);
103
+ }
104
+ } catch (error) {
105
+ console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
106
+ process.exit(1);
107
+ }
108
+ });
109
+
80
110
  // ============================================================================
81
111
  // BUILD COMMAND
82
112
  // ============================================================================
@@ -794,10 +824,16 @@ program
794
824
  // ============================================================================
795
825
  program
796
826
  .command('init')
797
- .description('Initialize fragments in a project (interactive by default)')
827
+ .description('Initialize fragments in a project (zero-config by default)')
798
828
  .option('--force', 'Overwrite existing config')
799
- .option('-y, --yes', 'Non-interactive mode - auto-detect and use defaults')
829
+ .option('-y, --yes', 'Non-interactive mode (now the default)')
830
+ .option('--configure', 'Interactive mode for theme seeds, snapshots, etc.')
800
831
  .option('--scan <path>', 'Scan a TypeScript component directory and generate fragment files')
832
+ .option('--enrich', 'Use AI to fill knowledge fields during --scan (requires API key)')
833
+ .option('--dry-run', 'Show what --enrich would generate without calling API')
834
+ .option('--provider <provider>', 'AI provider for enrichment: anthropic or openai')
835
+ .option('--api-key <key>', 'API key for AI enrichment')
836
+ .option('--model <model>', 'Override AI model for enrichment')
801
837
  .action(async (options) => {
802
838
  try {
803
839
  const { init } = await import('./commands/init.js');
@@ -806,6 +842,12 @@ program
806
842
  force: options.force,
807
843
  yes: options.scan ? true : options.yes,
808
844
  scan: options.scan,
845
+ configure: options.configure,
846
+ enrich: options.enrich,
847
+ dryRun: options.dryRun,
848
+ provider: options.provider,
849
+ apiKey: options.apiKey,
850
+ model: options.model,
809
851
  });
810
852
 
811
853
  if (!result.success) {
@@ -821,6 +863,36 @@ program
821
863
  }
822
864
  });
823
865
 
866
+ // ============================================================================
867
+ // SNAPSHOT COMMAND
868
+ // ============================================================================
869
+ program
870
+ .command('snapshot')
871
+ .description('Run visual snapshot tests per component variant')
872
+ .option('-p, --port <port>', 'Port of running dev server (skips starting one)')
873
+ .option('--update', 'Update existing snapshots instead of comparing')
874
+ .option('--component <name>', 'Filter to a specific component')
875
+ .option('--spec <path>', 'Path to snapshot spec file')
876
+ .option('--ci', 'CI mode - exit 1 on mismatch')
877
+ .action(async (options) => {
878
+ try {
879
+ const { snapshot } = await import('./commands/snapshot.js');
880
+ const result = await snapshot({
881
+ port: options.port,
882
+ update: options.update,
883
+ component: options.component,
884
+ spec: options.spec,
885
+ ci: options.ci,
886
+ });
887
+ if (!result.success) {
888
+ process.exit(1);
889
+ }
890
+ } catch (error) {
891
+ console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
892
+ process.exit(1);
893
+ }
894
+ });
895
+
824
896
  // ============================================================================
825
897
  // TOKENS COMMAND
826
898
  // ============================================================================
package/src/build.ts CHANGED
@@ -21,10 +21,13 @@ import {
21
21
  generateContextMd,
22
22
  } from "./core/node.js";
23
23
  import {
24
- extractCustomPropsFromComponentFile,
25
24
  resolveComponentSourcePath,
26
- type AutoDetectedPropDefinition,
27
25
  } from "./core/auto-props.js";
26
+ import {
27
+ createComponentExtractor,
28
+ type PropMeta,
29
+ type ComponentMeta,
30
+ } from "./core/component-extractor.js";
28
31
  import { buildComponentGraph } from "./core/graph-extractor.js";
29
32
  import { serializeGraph } from "@fragments-sdk/context/graph";
30
33
  import { resolvePerformanceConfig } from "./core/index.js";
@@ -53,17 +56,21 @@ function normalizeParsedProps(
53
56
 
54
57
  function mergeDocumentedAndAutoProps(
55
58
  documentedProps: Record<string, CompiledProp>,
56
- autoProps: Record<string, AutoDetectedPropDefinition>
59
+ autoProps: Record<string, PropMeta>
57
60
  ): Record<string, CompiledProp> {
58
61
  return Object.fromEntries(
59
- Object.keys(autoProps).map((name) => {
62
+ Object.keys(autoProps)
63
+ // Strip inherited HTML/React props — they're identical across all components
64
+ // and bloat fragments.json. MCP consumers know these exist implicitly.
65
+ .filter((name) => autoProps[name].source === 'local' || name in documentedProps)
66
+ .map((name) => {
60
67
  const documented = documentedProps[name];
61
68
  const auto = autoProps[name];
62
69
 
63
70
  return [
64
71
  name,
65
72
  {
66
- type: auto.type,
73
+ type: auto.typeKind,
67
74
  description: documented?.description ?? auto.description ?? "",
68
75
  default: auto.default !== undefined ? auto.default : documented?.default,
69
76
  required: auto.required,
@@ -75,6 +82,30 @@ function mergeDocumentedAndAutoProps(
75
82
  );
76
83
  }
77
84
 
85
+ /**
86
+ * Auto-compile a propsSummary for the contract from extracted props.
87
+ * Format: "variant: primary|secondary|ghost (required)"
88
+ */
89
+ function compilePropsSummary(props: Record<string, PropMeta>): string[] {
90
+ return Object.entries(props)
91
+ .filter(([_, p]) => p.source === 'local')
92
+ .map(([name, prop]) => {
93
+ let summary = name + ': ';
94
+ if (prop.values && prop.values.length > 0) {
95
+ summary += prop.values.join('|');
96
+ } else {
97
+ summary += prop.typeKind;
98
+ }
99
+ if (prop.default !== undefined) {
100
+ summary += ` (default: ${prop.default})`;
101
+ }
102
+ if (prop.required) {
103
+ summary += ' (required)';
104
+ }
105
+ return summary;
106
+ });
107
+ }
108
+
78
109
  export interface BuildResult {
79
110
  success: boolean;
80
111
  outputPath: string;
@@ -98,6 +129,15 @@ export async function buildFragments(
98
129
  const warnings: Array<{ file: string; warning: string }> = [];
99
130
  const fragments: CompiledFragmentsFile["fragments"] = {};
100
131
 
132
+ // Create a persistent extractor — shared LanguageService across all fragments
133
+ // Try to find a tsconfig.json in the config directory
134
+ const tsconfigCandidates = [
135
+ resolve(configDir, 'tsconfig.json'),
136
+ resolve(configDir, '..', 'tsconfig.json'),
137
+ ];
138
+ const tsconfigPath = tsconfigCandidates.find((p) => existsSync(p));
139
+ const extractor = createComponentExtractor(tsconfigPath);
140
+
101
141
  for (const file of files) {
102
142
  try {
103
143
  // Read file content as text
@@ -139,38 +179,38 @@ export async function buildFragments(
139
179
  parsed.componentImport
140
180
  );
141
181
 
182
+ // Extract full component metadata using persistent LanguageService
183
+ let extractedMeta: ComponentMeta | null = null;
142
184
  if (componentExportName && componentSourcePath) {
143
- const autoPropsResult = extractCustomPropsFromComponentFile(
144
- componentSourcePath,
145
- componentExportName
146
- );
147
-
148
- for (const warning of autoPropsResult.warnings) {
149
- warnings.push({ file: file.relativePath, warning });
185
+ try {
186
+ extractedMeta = extractor.extract(componentSourcePath, componentExportName);
187
+ } catch {
188
+ // Extraction failure is non-fatal — fall back to documented props
150
189
  }
151
190
 
152
- const hasAutoProps = Object.keys(autoPropsResult.props).length > 0;
153
- if (autoPropsResult.resolved && hasAutoProps) {
154
- const removedDocumentedProps = Object.keys(documentedProps).filter(
155
- (propName) => !(propName in autoPropsResult.props)
156
- );
191
+ if (extractedMeta) {
192
+ const autoProps = extractedMeta.props;
193
+ const hasAutoProps = Object.keys(autoProps).length > 0;
157
194
 
158
- if (removedDocumentedProps.length > 0) {
195
+ if (hasAutoProps) {
196
+ const removedDocumentedProps = Object.keys(documentedProps).filter(
197
+ (propName) => !(propName in autoProps)
198
+ );
199
+
200
+ if (removedDocumentedProps.length > 0) {
201
+ warnings.push({
202
+ file: file.relativePath,
203
+ warning: `Removed ${removedDocumentedProps.length} documented props not present in source API: ${removedDocumentedProps.join(", ")}`,
204
+ });
205
+ }
206
+
207
+ mergedProps = mergeDocumentedAndAutoProps(documentedProps, autoProps);
208
+ } else if (Object.keys(documentedProps).length > 0) {
159
209
  warnings.push({
160
210
  file: file.relativePath,
161
- warning: `Removed ${removedDocumentedProps.length} documented props not present in source API: ${removedDocumentedProps.join(", ")}`,
211
+ warning: "Auto-props extraction returned no props; falling back to documented props",
162
212
  });
163
213
  }
164
-
165
- mergedProps = mergeDocumentedAndAutoProps(
166
- documentedProps,
167
- autoPropsResult.props
168
- );
169
- } else if (autoPropsResult.resolved && !hasAutoProps && Object.keys(documentedProps).length > 0) {
170
- warnings.push({
171
- file: file.relativePath,
172
- warning: "Auto-props extraction returned no custom props; falling back to documented props",
173
- });
174
214
  }
175
215
  } else if (!componentExportName) {
176
216
  warnings.push({
@@ -184,6 +224,26 @@ export async function buildFragments(
184
224
  });
185
225
  }
186
226
 
227
+ // Auto-compile contract if not manually authored
228
+ let contract = parsed.contract;
229
+ if (!contract?.propsSummary && extractedMeta) {
230
+ const summary = compilePropsSummary(extractedMeta.props);
231
+ if (summary.length > 0) {
232
+ contract = { ...contract, propsSummary: summary };
233
+ }
234
+ }
235
+
236
+ // Auto-enrich AI metadata from extractor's composition data
237
+ let ai = parsed.ai;
238
+ if (extractedMeta?.composition) {
239
+ const comp = extractedMeta.composition;
240
+ ai = {
241
+ compositionPattern: comp.pattern,
242
+ subComponents: comp.parts.map((p) => p.name),
243
+ ...ai, // Manually authored ai fields take precedence
244
+ };
245
+ }
246
+
187
247
  // Build compiled fragment from parsed metadata
188
248
  const compiled: CompiledFragment = {
189
249
  filePath: file.relativePath,
@@ -221,10 +281,10 @@ export async function buildFragments(
221
281
  ...(v.figma && { figma: v.figma }),
222
282
  ...(v.args && { args: v.args }),
223
283
  })),
224
- // Include AI metadata if present
225
- ...(parsed.ai && { ai: parsed.ai }),
226
- // Include contract metadata if present
227
- ...(parsed.contract && { contract: parsed.contract }),
284
+ // Include AI metadata (auto-enriched or manual)
285
+ ...(ai && { ai }),
286
+ // Include contract metadata (auto-compiled or manual)
287
+ ...(contract && { contract }),
228
288
  };
229
289
 
230
290
  fragments[parsed.meta.name] = compiled;
@@ -236,6 +296,8 @@ export async function buildFragments(
236
296
  }
237
297
  }
238
298
 
299
+ extractor.dispose();
300
+
239
301
  // Discover and compile block files
240
302
  const blocks: Record<string, CompiledBlock> = {};
241
303
  try {