@fragments-sdk/cli 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/dist/bin.d.ts +1 -0
  2. package/dist/bin.js +502 -84
  3. package/dist/bin.js.map +1 -1
  4. package/dist/{chunk-CJEGT3WD.js → chunk-566BNPQZ.js} +21 -6
  5. package/dist/chunk-566BNPQZ.js.map +1 -0
  6. package/dist/{chunk-WI6SLMSO.js → chunk-CAMXG5HJ.js} +5 -5
  7. package/dist/chunk-D2CDBRNU.js +2 -0
  8. package/dist/{chunk-YMPGYEWK.js → chunk-D5PYOXEI.js} +2 -2
  9. package/dist/{chunk-NGIMCIK2.js → chunk-OQO55NKV.js} +405 -34
  10. package/dist/chunk-OQO55NKV.js.map +1 -0
  11. package/dist/{chunk-TOIE7VXF.js → chunk-PW7QTQA6.js} +2 -2
  12. package/dist/{chunk-AWYCDRPG.js → chunk-WXSR2II7.js} +2 -2
  13. package/dist/chunk-WXSR2II7.js.map +1 -0
  14. package/dist/{chunk-2JIKCJX3.js → chunk-ZDA3PLQ6.js} +17 -14
  15. package/dist/chunk-ZDA3PLQ6.js.map +1 -0
  16. package/dist/core/index.d.ts +1 -2092
  17. package/dist/core/index.js +26 -21
  18. package/dist/{discovery-Z4RDDFVR.js → discovery-NEOY4MPN.js} +3 -3
  19. package/dist/generate-BGKTKO6E.js +459 -0
  20. package/dist/generate-BGKTKO6E.js.map +1 -0
  21. package/dist/index.d.ts +3 -5
  22. package/dist/index.js +7 -8
  23. package/dist/index.js.map +1 -1
  24. package/dist/{init-KSAAS7X3.js → init-Q53R5Q2T.js} +66 -76
  25. package/dist/init-Q53R5Q2T.js.map +1 -0
  26. package/dist/mcp-bin.js +5 -7
  27. package/dist/mcp-bin.js.map +1 -1
  28. package/dist/scan-OQU7M4GH.js +14 -0
  29. package/dist/scan-generate-T5QNUG7N.js +691 -0
  30. package/dist/scan-generate-T5QNUG7N.js.map +1 -0
  31. package/dist/{service-A5GIGGGK.js → service-TQYWY65E.js} +4 -5
  32. package/dist/{static-viewer-NSODM5VX.js → static-viewer-NUBFPKWH.js} +4 -5
  33. package/dist/static-viewer-NUBFPKWH.js.map +1 -0
  34. package/dist/{test-RPWZAYSJ.js → test-2CSOSS3B.js} +4 -5
  35. package/dist/{test-RPWZAYSJ.js.map → test-2CSOSS3B.js.map} +1 -1
  36. package/dist/{tokens-NIXSZRX7.js → tokens-DXEGYTOJ.js} +6 -7
  37. package/dist/{tokens-NIXSZRX7.js.map → tokens-DXEGYTOJ.js.map} +1 -1
  38. package/dist/{viewer-SBTJDMP7.js → viewer-DBEPYM3G.js} +245 -23
  39. package/dist/viewer-DBEPYM3G.js.map +1 -0
  40. package/package.json +2 -1
  41. package/src/bin.ts +33 -1
  42. package/src/build.ts +13 -3
  43. package/src/commands/__tests__/scan-generate.test.ts +308 -0
  44. package/src/commands/build.ts +16 -2
  45. package/src/commands/generate.ts +383 -68
  46. package/src/commands/init.ts +81 -56
  47. package/src/commands/perf.ts +1 -1
  48. package/src/commands/scan-generate.ts +1013 -0
  49. package/src/commands/setup.ts +499 -0
  50. package/src/core/auto-props.ts +1 -1
  51. package/src/core/bundle-measurer.ts +2 -2
  52. package/src/core/config.ts +16 -4
  53. package/src/core/discovery.ts +2 -2
  54. package/src/core/generators/context.ts +1 -1
  55. package/src/core/generators/registry.ts +3 -3
  56. package/src/core/generators/typescript-extractor.ts +11 -1
  57. package/src/core/graph-extractor.ts +1 -1
  58. package/src/core/index.ts +3 -190
  59. package/src/core/loader.ts +2 -2
  60. package/src/core/parser.ts +1 -1
  61. package/src/core/previewLoader.ts +1 -1
  62. package/src/index.ts +2 -2
  63. package/src/migrate/converter.ts +9 -1
  64. package/src/migrate/parser.ts +2 -0
  65. package/src/migrate/types.ts +2 -0
  66. package/src/service/snippet-validation.test.ts +1 -1
  67. package/src/service/snippet-validation.ts +2 -2
  68. package/src/setup.ts +69 -24
  69. package/src/viewer/__tests__/viewer-integration.test.ts +4 -10
  70. package/src/viewer/components/AccessibilityPanel.tsx +305 -312
  71. package/src/viewer/components/ActionsPanel.tsx +31 -29
  72. package/src/viewer/components/AllVariantsPreview.tsx +78 -0
  73. package/src/viewer/components/App.tsx +187 -740
  74. package/src/viewer/components/BottomPanel.tsx +228 -132
  75. package/src/viewer/components/CodePanel.tsx +1 -1
  76. package/src/viewer/components/CommandPalette.tsx +7 -10
  77. package/src/viewer/components/ComponentDocView.tsx +164 -0
  78. package/src/viewer/components/ComponentGraph.tsx +111 -142
  79. package/src/viewer/components/ContractPanel.tsx +6 -6
  80. package/src/viewer/components/EmptyVariantMessage.tsx +54 -0
  81. package/src/viewer/components/FigmaEmbed.tsx +20 -18
  82. package/src/viewer/components/FragmentEditor.tsx +92 -115
  83. package/src/viewer/components/HeaderSearch.tsx +24 -0
  84. package/src/viewer/components/HealthDashboard.tsx +16 -2
  85. package/src/viewer/components/Icons.tsx +9 -0
  86. package/src/viewer/components/InteractionsPanel.tsx +101 -117
  87. package/src/viewer/components/IsolatedPreviewFrame.tsx +1 -0
  88. package/src/viewer/components/LandingPage.tsx +3 -3
  89. package/src/viewer/components/LeftSidebar.tsx +141 -63
  90. package/src/viewer/components/LoadErrorMessage.tsx +102 -0
  91. package/src/viewer/components/MultiViewportPreview.tsx +61 -142
  92. package/src/viewer/components/NoVariantsMessage.tsx +59 -0
  93. package/src/viewer/components/PanelShell.tsx +161 -0
  94. package/src/viewer/components/PerformancePanel.tsx +31 -28
  95. package/src/viewer/components/PreviewArea.tsx +1 -1
  96. package/src/viewer/components/PreviewAside.tsx +168 -0
  97. package/src/viewer/components/PreviewFrameHost.tsx +3 -3
  98. package/src/viewer/components/PropsEditor.tsx +70 -156
  99. package/src/viewer/components/ResizablePanel.tsx +103 -263
  100. package/src/viewer/components/RightSidebar.tsx +3 -9
  101. package/src/viewer/components/SkeletonLoader.tsx +13 -13
  102. package/src/viewer/components/TokenStylePanel.tsx +182 -209
  103. package/src/viewer/components/TopToolbar.tsx +159 -0
  104. package/src/viewer/components/VariantMatrix.tsx +42 -86
  105. package/src/viewer/components/VariantTabs.tsx +3 -3
  106. package/src/viewer/components/ViewerHeader.tsx +69 -0
  107. package/src/viewer/components/WebMCPDevTools.tsx +17 -23
  108. package/src/viewer/components/viewer-utils.ts +16 -0
  109. package/src/viewer/entry.tsx +5 -0
  110. package/src/viewer/hooks/useAppState.ts +27 -4
  111. package/src/viewer/hooks/usePreviewBridge.ts +2 -2
  112. package/src/viewer/preview-frame.html +6 -12
  113. package/src/viewer/server.ts +169 -2
  114. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss +10 -0
  115. package/src/viewer/vendor/shared/src/ComponentDocContent.module.scss.d.ts +2 -0
  116. package/src/viewer/vendor/shared/src/ComponentDocContent.tsx +274 -0
  117. package/src/viewer/vendor/shared/src/DocsHeaderBar.tsx +6 -18
  118. package/src/viewer/vendor/shared/src/DocsPageShell.tsx +5 -0
  119. package/src/viewer/vendor/shared/src/DocsSidebarNav.tsx +5 -16
  120. package/src/viewer/vendor/shared/src/PropsTable.module.scss +68 -0
  121. package/src/viewer/vendor/shared/src/PropsTable.module.scss.d.ts +2 -0
  122. package/src/viewer/vendor/shared/src/PropsTable.tsx +76 -0
  123. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss +114 -0
  124. package/src/viewer/vendor/shared/src/VariantPreviewCard.module.scss.d.ts +2 -0
  125. package/src/viewer/vendor/shared/src/VariantPreviewCard.tsx +134 -0
  126. package/src/viewer/vendor/shared/src/index.ts +8 -0
  127. package/src/viewer/vendor/shared/src/types.ts +12 -0
  128. package/src/viewer/vite-plugin.ts +109 -4
  129. package/dist/chunk-2JIKCJX3.js.map +0 -1
  130. package/dist/chunk-AWYCDRPG.js.map +0 -1
  131. package/dist/chunk-CJEGT3WD.js.map +0 -1
  132. package/dist/chunk-EKLMXTWU.js +0 -80
  133. package/dist/chunk-EKLMXTWU.js.map +0 -1
  134. package/dist/chunk-GOVI6COW.js +0 -195
  135. package/dist/chunk-GOVI6COW.js.map +0 -1
  136. package/dist/chunk-NGIMCIK2.js.map +0 -1
  137. package/dist/defineFragment-D0UTve-I.d.ts +0 -665
  138. package/dist/generate-35OIMW4Y.js +0 -252
  139. package/dist/generate-35OIMW4Y.js.map +0 -1
  140. package/dist/init-KSAAS7X3.js.map +0 -1
  141. package/dist/scan-65RH3QMM.js +0 -15
  142. package/dist/viewer-SBTJDMP7.js.map +0 -1
  143. package/src/core/__tests__/preview-runtime.test.tsx +0 -111
  144. package/src/core/composition.test.ts +0 -262
  145. package/src/core/composition.ts +0 -318
  146. package/src/core/constants.ts +0 -114
  147. package/src/core/context.ts +0 -2
  148. package/src/core/defineFragment.ts +0 -141
  149. package/src/core/figma.ts +0 -263
  150. package/src/core/fragment-types.ts +0 -214
  151. package/src/core/performance-presets.ts +0 -142
  152. package/src/core/preview-runtime.tsx +0 -144
  153. package/src/core/schema.ts +0 -221
  154. package/src/core/storyAdapter.test.ts +0 -571
  155. package/src/core/storyAdapter.ts +0 -761
  156. package/src/core/storybook-csf.ts +0 -11
  157. package/src/core/token-parser.ts +0 -321
  158. package/src/core/token-types.ts +0 -287
  159. package/src/core/types.ts +0 -762
  160. /package/dist/{chunk-WI6SLMSO.js.map → chunk-CAMXG5HJ.js.map} +0 -0
  161. /package/dist/{discovery-Z4RDDFVR.js.map → chunk-D2CDBRNU.js.map} +0 -0
  162. /package/dist/{chunk-YMPGYEWK.js.map → chunk-D5PYOXEI.js.map} +0 -0
  163. /package/dist/{chunk-TOIE7VXF.js.map → chunk-PW7QTQA6.js.map} +0 -0
  164. /package/dist/{scan-65RH3QMM.js.map → discovery-NEOY4MPN.js.map} +0 -0
  165. /package/dist/{service-A5GIGGGK.js.map → scan-OQU7M4GH.js.map} +0 -0
  166. /package/dist/{static-viewer-NSODM5VX.js.map → service-TQYWY65E.js.map} +0 -0
@@ -0,0 +1,499 @@
1
+ /**
2
+ * fragments setup - Configure @fragments-sdk/ui in a consumer project
3
+ *
4
+ * Auto-detects framework (Next.js, Vite, etc.) and scaffolds:
5
+ * - Styles import in entry file
6
+ * - ThemeProvider wrapper in layout/app
7
+ * - transpilePackages for Next.js
8
+ * - SCSS seeds file (optional)
9
+ */
10
+
11
+ import { readFile, writeFile, access, mkdir } from 'node:fs/promises';
12
+ import { join, resolve, dirname } from 'node:path';
13
+ import pc from 'picocolors';
14
+ import { BRAND } from '../core/index.js';
15
+
16
+ // ============================================
17
+ // Types
18
+ // ============================================
19
+
20
+ export interface SetupOptions {
21
+ /** Project root directory (defaults to cwd) */
22
+ root?: string;
23
+ /** Skip interactive prompts */
24
+ yes?: boolean;
25
+ /** Brand color hex (e.g., #6366f1) */
26
+ brand?: string;
27
+ /** Include SCSS seed file */
28
+ scss?: boolean;
29
+ /** Configure MCP server for AI tooling */
30
+ mcp?: boolean;
31
+ }
32
+
33
+ export interface SetupResult {
34
+ success: boolean;
35
+ actions: string[];
36
+ errors: string[];
37
+ }
38
+
39
+ type Framework = 'nextjs-app' | 'nextjs-pages' | 'vite' | 'unknown';
40
+
41
+ // ============================================
42
+ // Detection
43
+ // ============================================
44
+
45
+ async function fileExists(path: string): Promise<boolean> {
46
+ try {
47
+ await access(path);
48
+ return true;
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+
54
+ async function detectFramework(root: string): Promise<Framework> {
55
+ // Next.js App Router
56
+ if (
57
+ await fileExists(join(root, 'app/layout.tsx')) ||
58
+ await fileExists(join(root, 'src/app/layout.tsx'))
59
+ ) {
60
+ return 'nextjs-app';
61
+ }
62
+
63
+ // Next.js Pages Router
64
+ if (
65
+ await fileExists(join(root, 'pages/_app.tsx')) ||
66
+ await fileExists(join(root, 'pages/_app.ts'))
67
+ ) {
68
+ return 'nextjs-pages';
69
+ }
70
+
71
+ // Next.js (config exists but no router detected yet)
72
+ if (
73
+ await fileExists(join(root, 'next.config.ts')) ||
74
+ await fileExists(join(root, 'next.config.js')) ||
75
+ await fileExists(join(root, 'next.config.mjs'))
76
+ ) {
77
+ return 'nextjs-app';
78
+ }
79
+
80
+ // Vite
81
+ if (
82
+ await fileExists(join(root, 'vite.config.ts')) ||
83
+ await fileExists(join(root, 'vite.config.js'))
84
+ ) {
85
+ return 'vite';
86
+ }
87
+
88
+ return 'unknown';
89
+ }
90
+
91
+ async function findEntryFile(root: string, framework: Framework): Promise<string | null> {
92
+ const candidates: string[] = [];
93
+
94
+ switch (framework) {
95
+ case 'nextjs-app':
96
+ candidates.push(
97
+ 'src/app/layout.tsx', 'app/layout.tsx',
98
+ 'src/app/layout.ts', 'app/layout.ts'
99
+ );
100
+ break;
101
+ case 'nextjs-pages':
102
+ candidates.push('pages/_app.tsx', 'pages/_app.ts');
103
+ break;
104
+ case 'vite':
105
+ candidates.push(
106
+ 'src/main.tsx', 'src/main.ts',
107
+ 'src/index.tsx', 'src/index.ts'
108
+ );
109
+ break;
110
+ default:
111
+ candidates.push(
112
+ 'src/main.tsx', 'src/main.ts',
113
+ 'src/index.tsx', 'src/index.ts',
114
+ 'src/App.tsx', 'src/App.ts'
115
+ );
116
+ }
117
+
118
+ for (const candidate of candidates) {
119
+ if (await fileExists(join(root, candidate))) {
120
+ return candidate;
121
+ }
122
+ }
123
+
124
+ return null;
125
+ }
126
+
127
+ async function findNextConfig(root: string): Promise<string | null> {
128
+ const candidates = ['next.config.ts', 'next.config.mjs', 'next.config.js'];
129
+ for (const candidate of candidates) {
130
+ if (await fileExists(join(root, candidate))) {
131
+ return candidate;
132
+ }
133
+ }
134
+ return null;
135
+ }
136
+
137
+ // ============================================
138
+ // Actions
139
+ // ============================================
140
+
141
+ async function addStylesImport(root: string, entryFile: string): Promise<{ modified: boolean; message: string }> {
142
+ const fullPath = join(root, entryFile);
143
+ const content = await readFile(fullPath, 'utf-8');
144
+
145
+ if (content.includes('@fragments-sdk/ui/styles')) {
146
+ return { modified: false, message: `Styles already imported in ${entryFile}` };
147
+ }
148
+
149
+ // Add import at the top of the file, after any 'use client' directive
150
+ const stylesImport = "import '@fragments-sdk/ui/styles';";
151
+ let newContent: string;
152
+
153
+ if (content.startsWith("'use client'") || content.startsWith('"use client"')) {
154
+ const directiveEnd = content.indexOf('\n') + 1;
155
+ newContent = content.slice(0, directiveEnd) + stylesImport + '\n' + content.slice(directiveEnd);
156
+ } else {
157
+ newContent = stylesImport + '\n' + content;
158
+ }
159
+
160
+ await writeFile(fullPath, newContent, 'utf-8');
161
+ return { modified: true, message: `Added styles import to ${entryFile}` };
162
+ }
163
+
164
+ async function addThemeProvider(root: string, entryFile: string, framework: Framework): Promise<{ modified: boolean; message: string }> {
165
+ const fullPath = join(root, entryFile);
166
+ const content = await readFile(fullPath, 'utf-8');
167
+
168
+ if (content.includes('ThemeProvider')) {
169
+ return { modified: false, message: `ThemeProvider already present in ${entryFile}` };
170
+ }
171
+
172
+ // For Next.js App Router, add import and wrap children
173
+ if (framework === 'nextjs-app') {
174
+ // Add import
175
+ const providerImport = "import { ThemeProvider, TooltipProvider, ToastProvider } from '@fragments-sdk/ui';";
176
+
177
+ let newContent = content;
178
+
179
+ // Find the last import line to add our import after it
180
+ const importLines = content.split('\n');
181
+ let lastImportIdx = -1;
182
+ for (let i = 0; i < importLines.length; i++) {
183
+ if (importLines[i].startsWith('import ') || importLines[i].startsWith("import '") || importLines[i].startsWith('import "')) {
184
+ lastImportIdx = i;
185
+ }
186
+ }
187
+
188
+ if (lastImportIdx >= 0) {
189
+ importLines.splice(lastImportIdx + 1, 0, providerImport);
190
+ newContent = importLines.join('\n');
191
+ } else {
192
+ newContent = providerImport + '\n' + content;
193
+ }
194
+
195
+ // Add suppressHydrationWarning hint and provider wrapping in a comment
196
+ // We can't safely auto-wrap JSX, so we add a guide comment instead
197
+ if (!content.includes('suppressHydrationWarning')) {
198
+ await writeFile(fullPath, newContent, 'utf-8');
199
+ return {
200
+ modified: true,
201
+ message: `Added provider imports to ${entryFile}. Wrap your {children} with:\n` +
202
+ ` <ThemeProvider defaultMode="system"><TooltipProvider><ToastProvider>{children}</ToastProvider></TooltipProvider></ThemeProvider>\n` +
203
+ ` Add suppressHydrationWarning to your <html> tag`,
204
+ };
205
+ }
206
+
207
+ await writeFile(fullPath, newContent, 'utf-8');
208
+ return { modified: true, message: `Added provider imports to ${entryFile}. Wrap {children} with ThemeProvider.` };
209
+ }
210
+
211
+ return { modified: false, message: 'Manual ThemeProvider setup needed — see https://usefragments.com/getting-started#provider-setup' };
212
+ }
213
+
214
+ async function addTranspilePackages(root: string): Promise<{ modified: boolean; message: string }> {
215
+ const configFile = await findNextConfig(root);
216
+ if (!configFile) {
217
+ return { modified: false, message: 'No next.config found' };
218
+ }
219
+
220
+ const fullPath = join(root, configFile);
221
+ const content = await readFile(fullPath, 'utf-8');
222
+
223
+ if (content.includes('transpilePackages') && content.includes('@fragments-sdk/ui')) {
224
+ return { modified: false, message: `transpilePackages already configured in ${configFile}` };
225
+ }
226
+
227
+ if (content.includes('transpilePackages')) {
228
+ // transpilePackages exists but without @fragments-sdk/ui — need manual addition
229
+ return {
230
+ modified: false,
231
+ message: `transpilePackages found in ${configFile} but missing '@fragments-sdk/ui'. Please add it manually.`,
232
+ };
233
+ }
234
+
235
+ // Add transpilePackages to the config
236
+ // Try to find the config object and add the property
237
+ const patterns = [
238
+ // const nextConfig = { ... }
239
+ { search: /const\s+\w+\s*=\s*\{/, replacement: (match: string) => `${match}\n transpilePackages: ['@fragments-sdk/ui'],` },
240
+ // module.exports = { ... }
241
+ { search: /module\.exports\s*=\s*\{/, replacement: (match: string) => `${match}\n transpilePackages: ['@fragments-sdk/ui'],` },
242
+ // export default { ... }
243
+ { search: /export\s+default\s*\{/, replacement: (match: string) => `${match}\n transpilePackages: ['@fragments-sdk/ui'],` },
244
+ ];
245
+
246
+ for (const pattern of patterns) {
247
+ if (pattern.search.test(content)) {
248
+ const newContent = content.replace(pattern.search, pattern.replacement);
249
+ await writeFile(fullPath, newContent, 'utf-8');
250
+ return { modified: true, message: `Added transpilePackages to ${configFile}` };
251
+ }
252
+ }
253
+
254
+ return {
255
+ modified: false,
256
+ message: `Could not auto-modify ${configFile}. Add transpilePackages: ['@fragments-sdk/ui'] manually.`,
257
+ };
258
+ }
259
+
260
+ async function createScssSeeds(root: string, brand?: string): Promise<{ modified: boolean; message: string }> {
261
+ // Try common SCSS locations
262
+ const scssLocations = [
263
+ 'src/app/globals.scss',
264
+ 'app/globals.scss',
265
+ 'src/styles/globals.scss',
266
+ 'src/globals.scss',
267
+ 'styles/globals.scss',
268
+ ];
269
+
270
+ // Check if any exists already
271
+ for (const loc of scssLocations) {
272
+ const fullPath = join(root, loc);
273
+ if (await fileExists(fullPath)) {
274
+ const content = await readFile(fullPath, 'utf-8');
275
+ if (content.includes('@fragments-sdk/ui/styles')) {
276
+ return { modified: false, message: `SCSS seeds already configured in ${loc}` };
277
+ }
278
+
279
+ // Prepend @use to existing file
280
+ const seedContent = generateScssSeedImport(brand);
281
+ const newContent = seedContent + '\n' + content;
282
+ await writeFile(fullPath, newContent, 'utf-8');
283
+ return { modified: true, message: `Added SCSS seed import to ${loc}` };
284
+ }
285
+ }
286
+
287
+ // Create new globals.scss in the most common location
288
+ const targetDir = await fileExists(join(root, 'src/app'))
289
+ ? 'src/app'
290
+ : await fileExists(join(root, 'src'))
291
+ ? 'src/styles'
292
+ : 'styles';
293
+
294
+ const targetPath = join(targetDir, 'globals.scss');
295
+ const fullPath = join(root, targetPath);
296
+
297
+ await mkdir(dirname(fullPath), { recursive: true });
298
+ await writeFile(fullPath, generateScssSeedImport(brand), 'utf-8');
299
+ return { modified: true, message: `Created ${targetPath} with SCSS seed configuration` };
300
+ }
301
+
302
+ async function setupMcpConfig(root: string): Promise<{ modified: boolean; message: string }> {
303
+ // Check common MCP config locations
304
+ const mcpConfigPaths = [
305
+ '.cursor/mcp.json',
306
+ '.vscode/mcp.json',
307
+ ];
308
+
309
+ for (const configPath of mcpConfigPaths) {
310
+ const fullPath = join(root, configPath);
311
+ if (await fileExists(fullPath)) {
312
+ try {
313
+ const content = await readFile(fullPath, 'utf-8');
314
+ const config = JSON.parse(content);
315
+ const servers = config.mcpServers || {};
316
+
317
+ // Check if fragments is already configured
318
+ const hasFragments = Object.values(servers).some((server: unknown) => {
319
+ const s = server as { command?: string; args?: string[] };
320
+ return s.args?.some((arg: string) => arg.includes('@fragments-sdk/mcp'));
321
+ });
322
+
323
+ if (hasFragments) {
324
+ return { modified: false, message: `MCP server already configured in ${configPath}` };
325
+ }
326
+
327
+ // Add fragments MCP server
328
+ servers.fragments = {
329
+ command: 'npx',
330
+ args: ['@fragments-sdk/mcp'],
331
+ };
332
+ config.mcpServers = servers;
333
+
334
+ await writeFile(fullPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
335
+ return { modified: true, message: `Added Fragments MCP server to ${configPath}` };
336
+ } catch {
337
+ return { modified: false, message: `Could not parse ${configPath}` };
338
+ }
339
+ }
340
+ }
341
+
342
+ // No existing MCP config — create one for Cursor (most common)
343
+ const cursorDir = join(root, '.cursor');
344
+ const cursorMcpPath = join(cursorDir, 'mcp.json');
345
+
346
+ await mkdir(cursorDir, { recursive: true });
347
+ const mcpConfig = {
348
+ mcpServers: {
349
+ fragments: {
350
+ command: 'npx',
351
+ args: ['@fragments-sdk/mcp'],
352
+ },
353
+ },
354
+ };
355
+
356
+ await writeFile(cursorMcpPath, JSON.stringify(mcpConfig, null, 2) + '\n', 'utf-8');
357
+ return { modified: true, message: 'Created .cursor/mcp.json with Fragments MCP server' };
358
+ }
359
+
360
+ function generateScssSeedImport(brand?: string): string {
361
+ const brandColor = brand || '#0066ff';
362
+ return `@use '@fragments-sdk/ui/styles' with (
363
+ $fui-brand: ${brandColor},
364
+ $fui-neutral: "stone",
365
+ $fui-density: "default",
366
+ $fui-radius-style: "rounded"
367
+ );
368
+ `;
369
+ }
370
+
371
+ // ============================================
372
+ // Main Setup Function
373
+ // ============================================
374
+
375
+ export async function setup(options: SetupOptions = {}): Promise<SetupResult> {
376
+ const root = resolve(options.root ?? process.cwd());
377
+ const actions: string[] = [];
378
+ const errors: string[] = [];
379
+
380
+ console.log(pc.cyan(`\n${BRAND.name} Setup\n`));
381
+
382
+ // 1. Detect framework
383
+ const framework = await detectFramework(root);
384
+ const frameworkLabel =
385
+ framework === 'nextjs-app' ? 'Next.js (App Router)' :
386
+ framework === 'nextjs-pages' ? 'Next.js (Pages Router)' :
387
+ framework === 'vite' ? 'Vite' :
388
+ 'Unknown';
389
+
390
+ console.log(` ${pc.dim('Framework:')} ${frameworkLabel}`);
391
+
392
+ // 2. Find entry file
393
+ const entryFile = await findEntryFile(root, framework);
394
+ if (entryFile) {
395
+ console.log(` ${pc.dim('Entry:')} ${entryFile}`);
396
+ } else {
397
+ console.log(` ${pc.yellow('!')} Could not detect entry file`);
398
+ }
399
+ console.log();
400
+
401
+ // 3. Add styles import
402
+ if (entryFile) {
403
+ try {
404
+ const result = await addStylesImport(root, entryFile);
405
+ const icon = result.modified ? pc.green('+') : pc.dim('·');
406
+ console.log(` ${icon} ${result.message}`);
407
+ if (result.modified) actions.push(result.message);
408
+ } catch (error) {
409
+ const msg = `Failed to add styles import: ${error instanceof Error ? error.message : error}`;
410
+ console.log(` ${pc.red('✗')} ${msg}`);
411
+ errors.push(msg);
412
+ }
413
+ }
414
+
415
+ // 4. Add ThemeProvider imports
416
+ if (entryFile) {
417
+ try {
418
+ const result = await addThemeProvider(root, entryFile, framework);
419
+ const icon = result.modified ? pc.green('+') : pc.dim('·');
420
+ console.log(` ${icon} ${result.message}`);
421
+ if (result.modified) actions.push(result.message);
422
+ } catch (error) {
423
+ const msg = `Failed to add ThemeProvider: ${error instanceof Error ? error.message : error}`;
424
+ console.log(` ${pc.red('✗')} ${msg}`);
425
+ errors.push(msg);
426
+ }
427
+ }
428
+
429
+ // 5. Next.js: add transpilePackages
430
+ if (framework === 'nextjs-app' || framework === 'nextjs-pages') {
431
+ try {
432
+ const result = await addTranspilePackages(root);
433
+ const icon = result.modified ? pc.green('+') : pc.dim('·');
434
+ console.log(` ${icon} ${result.message}`);
435
+ if (result.modified) actions.push(result.message);
436
+ } catch (error) {
437
+ const msg = `Failed to update next.config: ${error instanceof Error ? error.message : error}`;
438
+ console.log(` ${pc.red('✗')} ${msg}`);
439
+ errors.push(msg);
440
+ }
441
+ }
442
+
443
+ // 6. Create SCSS seeds file (if --scss flag or brand color specified)
444
+ if (options.scss || options.brand) {
445
+ try {
446
+ const result = await createScssSeeds(root, options.brand);
447
+ const icon = result.modified ? pc.green('+') : pc.dim('·');
448
+ console.log(` ${icon} ${result.message}`);
449
+ if (result.modified) actions.push(result.message);
450
+ } catch (error) {
451
+ const msg = `Failed to create SCSS seeds: ${error instanceof Error ? error.message : error}`;
452
+ console.log(` ${pc.red('✗')} ${msg}`);
453
+ errors.push(msg);
454
+ }
455
+ }
456
+
457
+ // 7. Configure MCP server (if --mcp flag)
458
+ if (options.mcp) {
459
+ try {
460
+ const result = await setupMcpConfig(root);
461
+ const icon = result.modified ? pc.green('+') : pc.dim('·');
462
+ console.log(` ${icon} ${result.message}`);
463
+ if (result.modified) actions.push(result.message);
464
+ } catch (error) {
465
+ const msg = `Failed to configure MCP: ${error instanceof Error ? error.message : error}`;
466
+ console.log(` ${pc.red('✗')} ${msg}`);
467
+ errors.push(msg);
468
+ }
469
+ }
470
+
471
+ // Summary
472
+ console.log();
473
+ if (errors.length > 0) {
474
+ console.log(pc.red(` ${errors.length} error(s) occurred during setup`));
475
+ } else if (actions.length > 0) {
476
+ console.log(pc.green(` ✓ Setup complete — ${actions.length} file(s) modified`));
477
+ } else {
478
+ console.log(pc.green(' ✓ Already configured — no changes needed'));
479
+ }
480
+
481
+ // Next steps
482
+ console.log();
483
+ console.log(pc.dim(' Next steps:'));
484
+ if (!options.scss && !options.brand) {
485
+ console.log(pc.dim(' • Run with --scss to add build-time theme seeds'));
486
+ }
487
+ if (!options.mcp) {
488
+ console.log(pc.dim(' • Run with --mcp to configure AI tooling (MCP server)'));
489
+ }
490
+ console.log(pc.dim(' • Run `fragments doctor` to verify your setup'));
491
+ console.log(pc.dim(' • Visit https://usefragments.com/getting-started'));
492
+ console.log();
493
+
494
+ return {
495
+ success: errors.length === 0,
496
+ actions,
497
+ errors,
498
+ };
499
+ }
@@ -1,7 +1,7 @@
1
1
  import { existsSync, statSync } from "node:fs";
2
2
  import { dirname, extname, join, resolve } from "node:path";
3
3
  import ts from "typescript";
4
- import type { PropDefinition } from "./types.js";
4
+ import type { PropDefinition } from '@fragments-sdk/core';
5
5
 
6
6
  export interface AutoDetectedPropDefinition {
7
7
  type: PropDefinition["type"];
@@ -15,8 +15,8 @@ import {
15
15
  resolvePerformanceConfig,
16
16
  classifyComplexity,
17
17
  type PerformanceConfig,
18
- type PerformanceData,
19
- } from './performance-presets.js';
18
+ type PerfData as PerformanceData,
19
+ } from '@fragments-sdk/core';
20
20
 
21
21
  // ---------------------------------------------------------------------------
22
22
  // Types
@@ -1,9 +1,15 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { resolve, dirname } from 'node:path';
3
3
  import { createJiti } from 'jiti';
4
- import { BRAND } from './constants.js';
5
- import type { FragmentsConfig } from './types.js';
6
- import { fragmentsConfigSchema } from './schema.js';
4
+ import { BRAND, fragmentsConfigSchema } from '@fragments-sdk/core';
5
+ import type { FragmentsConfig, StorybookFilterConfig } from '@fragments-sdk/core';
6
+
7
+ const STORYBOOK_FILTER_DEFAULTS: StorybookFilterConfig = {
8
+ excludeDeprecated: true,
9
+ excludeTests: true,
10
+ excludeSvgIcons: true,
11
+ excludeSubComponents: true,
12
+ };
7
13
 
8
14
  const DEFAULT_CONFIG: FragmentsConfig = {
9
15
  include: [
@@ -13,6 +19,7 @@ const DEFAULT_CONFIG: FragmentsConfig = {
13
19
  exclude: ['**/node_modules/**'],
14
20
  components: ['src/**/index.tsx', 'src/**/*.tsx'],
15
21
  framework: 'react',
22
+ storybook: STORYBOOK_FILTER_DEFAULTS,
16
23
  snippets: {
17
24
  mode: 'warn',
18
25
  scope: 'snippet+render',
@@ -77,8 +84,13 @@ export async function loadConfig(configPath?: string): Promise<{
77
84
  throw new Error(`Invalid config in ${resolvedPath}:\n${errors}`);
78
85
  }
79
86
 
87
+ const merged: FragmentsConfig = { ...DEFAULT_CONFIG, ...result.data };
88
+
89
+ // Deep-merge storybook filter defaults so consumers only override what they need
90
+ merged.storybook = { ...STORYBOOK_FILTER_DEFAULTS, ...result.data?.storybook };
91
+
80
92
  return {
81
- config: { ...DEFAULT_CONFIG, ...result.data },
93
+ config: merged,
82
94
  configDir: dirname(resolvedPath),
83
95
  };
84
96
  } catch (error) {
@@ -2,8 +2,8 @@ import { resolve, dirname, basename } from 'node:path';
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { existsSync } from 'node:fs';
4
4
  import fg from 'fast-glob';
5
- import type { FragmentsConfig } from './types.js';
6
- import { BRAND } from './constants.js';
5
+ import { BRAND } from '@fragments-sdk/core';
6
+ import type { FragmentsConfig } from '@fragments-sdk/core';
7
7
 
8
8
  export interface DiscoveredFile {
9
9
  /** Absolute path to the file */
@@ -15,7 +15,7 @@ import type {
15
15
  RegistryPropEntry,
16
16
  FragmentDoNotItem,
17
17
  FragmentContextOptions,
18
- } from "../fragment-types.js";
18
+ } from '@fragments-sdk/core';
19
19
 
20
20
  /**
21
21
  * Result of context generation
@@ -11,15 +11,15 @@
11
11
  import { readFileSync, existsSync } from "node:fs";
12
12
  import { relative, dirname, basename, join } from "node:path";
13
13
  import fg from "fast-glob";
14
+ import { BRAND } from '@fragments-sdk/core';
14
15
  import type {
15
16
  Fragment,
16
17
  FragmentRegistry,
17
18
  FragmentIndex,
18
19
  RegistryComponentEntry,
19
- } from "../fragment-types.js";
20
+ RegistryOptions,
21
+ } from '@fragments-sdk/core';
20
22
  import { extractPropsFromFile } from "./typescript-extractor.js";
21
- import { BRAND } from "../constants.js";
22
- import type { RegistryOptions } from "../types.js";
23
23
 
24
24
  /**
25
25
  * Options for registry generation
@@ -5,7 +5,7 @@
5
5
 
6
6
  import ts from "typescript";
7
7
  import { readFileSync } from "node:fs";
8
- import type { RegistryPropEntry } from "../fragment-types.js";
8
+ import type { RegistryPropEntry } from '@fragments-sdk/core';
9
9
 
10
10
  /**
11
11
  * Result of extracting props from a component file
@@ -13,6 +13,8 @@ import type { RegistryPropEntry } from "../fragment-types.js";
13
13
  export interface ExtractedProps {
14
14
  /** Component name */
15
15
  componentName: string;
16
+ /** Whether the component uses export default */
17
+ isDefaultExport?: boolean;
16
18
  /** Props interface name (e.g., "ButtonProps") */
17
19
  propsInterfaceName?: string;
18
20
  /** Extracted props */
@@ -56,6 +58,7 @@ export function extractPropsFromSource(
56
58
  // Find all exports and props interfaces
57
59
  const propsInterfaces = new Map<string, ts.InterfaceDeclaration | ts.TypeAliasDeclaration>();
58
60
  const componentExports: string[] = [];
61
+ const defaultExports = new Set<string>();
59
62
  const importedModules: string[] = [];
60
63
 
61
64
  ts.forEachChild(sourceFile, (node) => {
@@ -88,8 +91,14 @@ export function extractPropsFromSource(
88
91
  const hasExport = node.modifiers?.some(
89
92
  (m) => m.kind === ts.SyntaxKind.ExportKeyword
90
93
  );
94
+ const hasDefault = node.modifiers?.some(
95
+ (m) => m.kind === ts.SyntaxKind.DefaultKeyword
96
+ );
91
97
  if (hasExport) {
92
98
  componentExports.push(node.name.text);
99
+ if (hasDefault) {
100
+ defaultExports.add(node.name.text);
101
+ }
93
102
  }
94
103
  }
95
104
 
@@ -130,6 +139,7 @@ export function extractPropsFromSource(
130
139
  }
131
140
 
132
141
  result.componentName = mainComponent;
142
+ result.isDefaultExport = defaultExports.has(mainComponent);
133
143
 
134
144
  // Find matching props interface
135
145
  const propsInterfaceName = `${mainComponent}Props`;
@@ -24,7 +24,7 @@ import type {
24
24
  GraphHealth,
25
25
  } from '@fragments-sdk/context/graph';
26
26
  import { EDGE_TYPE_WEIGHTS, computeHealthFromData } from '@fragments-sdk/context/graph';
27
- import type { CompiledFragment, CompiledBlock } from './types.js';
27
+ import type { CompiledFragment, CompiledBlock } from '@fragments-sdk/core';
28
28
 
29
29
  // ---------------------------------------------------------------------------
30
30
  // Public API