@fragments-sdk/cli 0.11.1 → 0.13.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 (89) hide show
  1. package/dist/ai-client-I6MDWNYA.js +21 -0
  2. package/dist/bin.js +419 -410
  3. package/dist/bin.js.map +1 -1
  4. package/dist/{chunk-HRFUSSZI.js → chunk-3SOAPJDX.js} +2 -2
  5. package/dist/{chunk-D5PYOXEI.js → chunk-4K7EAQ5L.js} +148 -13
  6. package/dist/{chunk-D5PYOXEI.js.map → chunk-4K7EAQ5L.js.map} +1 -1
  7. package/dist/chunk-DXX6HADE.js +443 -0
  8. package/dist/chunk-DXX6HADE.js.map +1 -0
  9. package/dist/chunk-EYXVAMEX.js +626 -0
  10. package/dist/chunk-EYXVAMEX.js.map +1 -0
  11. package/dist/{chunk-ZM4ZQZWZ.js → chunk-FO6EBJWP.js} +39 -37
  12. package/dist/chunk-FO6EBJWP.js.map +1 -0
  13. package/dist/{chunk-OQO55NKV.js → chunk-QM7SVOGF.js} +120 -12
  14. package/dist/chunk-QM7SVOGF.js.map +1 -0
  15. package/dist/{chunk-5G3VZH43.js → chunk-RF3C6LGA.js} +281 -351
  16. package/dist/chunk-RF3C6LGA.js.map +1 -0
  17. package/dist/{chunk-WXSR2II7.js → chunk-SM674YAS.js} +58 -6
  18. package/dist/chunk-SM674YAS.js.map +1 -0
  19. package/dist/chunk-SXTKFDCR.js +104 -0
  20. package/dist/chunk-SXTKFDCR.js.map +1 -0
  21. package/dist/{chunk-PW7QTQA6.js → chunk-UV5JQV3R.js} +2 -2
  22. package/dist/core/index.js +13 -1
  23. package/dist/{discovery-NEOY4MPN.js → discovery-VSGC76JN.js} +3 -3
  24. package/dist/{generate-FBHSXR3D.js → generate-QZXOXYFW.js} +4 -4
  25. package/dist/index.js +7 -6
  26. package/dist/index.js.map +1 -1
  27. package/dist/init-XK6PRUE5.js +636 -0
  28. package/dist/init-XK6PRUE5.js.map +1 -0
  29. package/dist/mcp-bin.js +2 -2
  30. package/dist/{scan-CJF2DOQW.js → scan-CHQHXWVD.js} +6 -6
  31. package/dist/scan-generate-U3RFVDTX.js +1115 -0
  32. package/dist/scan-generate-U3RFVDTX.js.map +1 -0
  33. package/dist/{service-TQYWY65E.js → service-MMEKG4MZ.js} +3 -3
  34. package/dist/{snapshot-SV2JOFZH.js → snapshot-53TUR3HW.js} +2 -2
  35. package/dist/{static-viewer-NUBFPKWH.js → static-viewer-KKCR4KXR.js} +3 -3
  36. package/dist/static-viewer-KKCR4KXR.js.map +1 -0
  37. package/dist/{test-Z5LVO724.js → test-5UCKXYSC.js} +4 -4
  38. package/dist/{tokens-CE46OTMD.js → tokens-L46MK5AW.js} +5 -5
  39. package/dist/{viewer-DLLJIMCK.js → viewer-M2EQQSGE.js} +14 -14
  40. package/dist/viewer-M2EQQSGE.js.map +1 -0
  41. package/package.json +11 -9
  42. package/src/ai-client.ts +156 -0
  43. package/src/bin.ts +99 -2
  44. package/src/build.ts +95 -33
  45. package/src/commands/__tests__/drift-sync.test.ts +252 -0
  46. package/src/commands/__tests__/scan-generate.test.ts +497 -45
  47. package/src/commands/enhance.ts +11 -35
  48. package/src/commands/govern.ts +122 -0
  49. package/src/commands/init.ts +288 -260
  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/sync.ts +357 -0
  54. package/src/commands/validate.ts +43 -1
  55. package/src/core/component-extractor.test.ts +282 -0
  56. package/src/core/component-extractor.ts +1030 -0
  57. package/src/core/discovery.ts +93 -7
  58. package/src/service/enhance/props-extractor.ts +235 -13
  59. package/src/validators.ts +236 -0
  60. package/src/viewer/vite-plugin.ts +1 -1
  61. package/dist/chunk-5G3VZH43.js.map +0 -1
  62. package/dist/chunk-OQO55NKV.js.map +0 -1
  63. package/dist/chunk-WXSR2II7.js.map +0 -1
  64. package/dist/chunk-ZM4ZQZWZ.js.map +0 -1
  65. package/dist/init-UFGK5TCN.js +0 -867
  66. package/dist/init-UFGK5TCN.js.map +0 -1
  67. package/dist/scan-generate-SJAN5MVI.js +0 -691
  68. package/dist/scan-generate-SJAN5MVI.js.map +0 -1
  69. package/dist/viewer-DLLJIMCK.js.map +0 -1
  70. package/src/ai.ts +0 -266
  71. package/src/commands/init-framework.ts +0 -414
  72. package/src/mcp/bin.ts +0 -36
  73. package/src/migrate/bin.ts +0 -114
  74. package/src/theme/index.ts +0 -77
  75. package/src/viewer/bin.ts +0 -86
  76. package/src/viewer/cli/health.ts +0 -256
  77. package/src/viewer/cli/index.ts +0 -33
  78. package/src/viewer/cli/scan.ts +0 -124
  79. package/src/viewer/cli/utils.ts +0 -174
  80. /package/dist/{discovery-NEOY4MPN.js.map → ai-client-I6MDWNYA.js.map} +0 -0
  81. /package/dist/{chunk-HRFUSSZI.js.map → chunk-3SOAPJDX.js.map} +0 -0
  82. /package/dist/{chunk-PW7QTQA6.js.map → chunk-UV5JQV3R.js.map} +0 -0
  83. /package/dist/{scan-CJF2DOQW.js.map → discovery-VSGC76JN.js.map} +0 -0
  84. /package/dist/{generate-FBHSXR3D.js.map → generate-QZXOXYFW.js.map} +0 -0
  85. /package/dist/{service-TQYWY65E.js.map → scan-CHQHXWVD.js.map} +0 -0
  86. /package/dist/{static-viewer-NUBFPKWH.js.map → service-MMEKG4MZ.js.map} +0 -0
  87. /package/dist/{snapshot-SV2JOFZH.js.map → snapshot-53TUR3HW.js.map} +0 -0
  88. /package/dist/{test-Z5LVO724.js.map → test-5UCKXYSC.js.map} +0 -0
  89. /package/dist/{tokens-CE46OTMD.js.map → tokens-L46MK5AW.js.map} +0 -0
@@ -56,6 +56,8 @@ export interface ScanOptions {
56
56
  skipStorybook?: boolean;
57
57
  /** Verbose output */
58
58
  verbose?: boolean;
59
+ /** Suppress all console output (for use as a sub-step) */
60
+ quiet?: boolean;
59
61
  }
60
62
 
61
63
  export interface ScanResult {
@@ -78,6 +80,9 @@ export async function scan(options: ScanOptions = {}): Promise<ScanResult> {
78
80
  const errors: Array<{ component: string; error: string }> = [];
79
81
  const warnings: Array<{ component: string; warning: string }> = [];
80
82
 
83
+ // In quiet mode, suppress all console output
84
+ const log = options.quiet ? (() => {}) : console.log.bind(console);
85
+
81
86
  // Load config or use defaults
82
87
  let configDir: string;
83
88
  let outputFile: string;
@@ -95,11 +100,11 @@ export async function scan(options: ScanOptions = {}): Promise<ScanResult> {
95
100
  componentPatterns = options.componentPatterns;
96
101
  }
97
102
 
98
- console.log(pc.cyan(`\n${BRAND.name} Scan\n`));
99
- console.log(pc.dim("Zero-config fragments.json generation from source code\n"));
103
+ log(pc.cyan(`\n${BRAND.name} Scan\n`));
104
+ log(pc.dim("Zero-config fragments.json generation from source code\n"));
100
105
 
101
106
  // Phase 1: Discover components
102
- console.log(pc.dim("Phase 1: Discovering components..."));
107
+ log(pc.dim("Phase 1: Discovering components..."));
103
108
  const components = await discoverAllComponents(configDir, {
104
109
  patterns: componentPatterns,
105
110
  exclude: ["**/*.test.*", "**/*.spec.*", "**/__tests__/**"],
@@ -107,7 +112,7 @@ export async function scan(options: ScanOptions = {}): Promise<ScanResult> {
107
112
  });
108
113
 
109
114
  if (components.length === 0) {
110
- console.log(pc.yellow("No components found. Check your patterns or config."));
115
+ log(pc.yellow("No components found. Check your patterns or config."));
111
116
  return {
112
117
  success: false,
113
118
  outputPath: resolve(configDir, outputFile),
@@ -121,18 +126,18 @@ export async function scan(options: ScanOptions = {}): Promise<ScanResult> {
121
126
  };
122
127
  }
123
128
 
124
- console.log(pc.green(` Found ${components.length} components`));
129
+ log(pc.green(` Found ${components.length} components`));
125
130
  if (options.verbose) {
126
131
  for (const comp of components.slice(0, 10)) {
127
- console.log(pc.dim(` - ${comp.name}: ${comp.relativePath}`));
132
+ log(pc.dim(` - ${comp.name}: ${comp.relativePath}`));
128
133
  }
129
134
  if (components.length > 10) {
130
- console.log(pc.dim(` ... and ${components.length - 10} more`));
135
+ log(pc.dim(` ... and ${components.length - 10} more`));
131
136
  }
132
137
  }
133
138
 
134
139
  // Phase 2: Extract props from TypeScript
135
- console.log(pc.dim("\nPhase 2: Extracting props from TypeScript..."));
140
+ log(pc.dim("\nPhase 2: Extracting props from TypeScript..."));
136
141
  const propsMap = new Map<string, ReturnType<typeof convertToFragmentProps>>();
137
142
  const propsResults = new Map<string, PropsExtractionResult>();
138
143
  let propsExtracted = 0;
@@ -159,7 +164,7 @@ export async function scan(options: ScanOptions = {}): Promise<ScanResult> {
159
164
  }
160
165
  }
161
166
 
162
- console.log(pc.green(` Extracted props for ${propsExtracted} components`));
167
+ log(pc.green(` Extracted props for ${propsExtracted} components`));
163
168
 
164
169
  // Phase 3: Scan for usage patterns
165
170
  let usageAnalysis: UsageAnalysis | undefined;
@@ -167,7 +172,7 @@ export async function scan(options: ScanOptions = {}): Promise<ScanResult> {
167
172
  let allRelations = new Map<string, ComponentRelation[]>();
168
173
 
169
174
  if (!options.skipUsage) {
170
- console.log(pc.dim("\nPhase 3: Scanning for usage patterns..."));
175
+ log(pc.dim("\nPhase 3: Scanning for usage patterns..."));
171
176
  const usageDir = options.usageDir || configDir;
172
177
 
173
178
  try {
@@ -196,17 +201,17 @@ export async function scan(options: ScanOptions = {}): Promise<ScanResult> {
196
201
 
197
202
  // Infer relations
198
203
  allRelations = inferAllRelations(usageAnalysis);
199
- console.log(pc.green(` Found ${usagesFound} usages across ${usageAnalysis.totalFiles} files`));
200
- console.log(pc.green(` Inferred relations for ${allRelations.size} components`));
204
+ log(pc.green(` Found ${usagesFound} usages across ${usageAnalysis.totalFiles} files`));
205
+ log(pc.green(` Inferred relations for ${allRelations.size} components`));
201
206
  } catch (e) {
202
207
  warnings.push({
203
208
  component: "*",
204
209
  warning: `Usage scanning failed: ${e instanceof Error ? e.message : String(e)}`,
205
210
  });
206
- console.log(pc.yellow(` Usage scanning failed: ${e instanceof Error ? e.message : "unknown error"}`));
211
+ log(pc.yellow(` Usage scanning failed: ${e instanceof Error ? e.message : "unknown error"}`));
207
212
  }
208
213
  } else {
209
- console.log(pc.dim("\nPhase 3: Skipping usage analysis"));
214
+ log(pc.dim("\nPhase 3: Skipping usage analysis"));
210
215
  }
211
216
 
212
217
  // Phase 4: Parse Storybook stories
@@ -214,7 +219,7 @@ export async function scan(options: ScanOptions = {}): Promise<ScanResult> {
214
219
  let storiesParsed = 0;
215
220
 
216
221
  if (!options.skipStorybook) {
217
- console.log(pc.dim("\nPhase 4: Parsing Storybook stories..."));
222
+ log(pc.dim("\nPhase 4: Parsing Storybook stories..."));
218
223
 
219
224
  try {
220
225
  const allStories = await parseAllStories(configDir);
@@ -226,20 +231,20 @@ export async function scan(options: ScanOptions = {}): Promise<ScanResult> {
226
231
  }
227
232
  }
228
233
 
229
- console.log(pc.green(` Parsed stories for ${storiesParsed} components`));
234
+ log(pc.green(` Parsed stories for ${storiesParsed} components`));
230
235
  } catch (e) {
231
236
  warnings.push({
232
237
  component: "*",
233
238
  warning: `Storybook parsing failed: ${e instanceof Error ? e.message : String(e)}`,
234
239
  });
235
- console.log(pc.yellow(` Storybook parsing failed: ${e instanceof Error ? e.message : "unknown error"}`));
240
+ log(pc.yellow(` Storybook parsing failed: ${e instanceof Error ? e.message : "unknown error"}`));
236
241
  }
237
242
  } else {
238
- console.log(pc.dim("\nPhase 4: Skipping Storybook parsing"));
243
+ log(pc.dim("\nPhase 4: Skipping Storybook parsing"));
239
244
  }
240
245
 
241
246
  // Phase 5: Generate fragments
242
- console.log(pc.dim("\nPhase 5: Generating fragments..."));
247
+ log(pc.dim("\nPhase 5: Generating fragments..."));
243
248
  const fragments: Record<string, CompiledFragment> = {};
244
249
 
245
250
  for (const comp of components) {
@@ -277,32 +282,32 @@ export async function scan(options: ScanOptions = {}): Promise<ScanResult> {
277
282
  const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
278
283
 
279
284
  // Summary
280
- console.log(pc.dim("\n────────────────────────────────────────"));
281
- console.log(pc.green(`\n✓ Generated fragments.json in ${elapsed}s`));
282
- console.log(pc.dim(` Output: ${relative(process.cwd(), outputPath)}`));
283
- console.log(pc.dim(` Components: ${Object.keys(fragments).length}`));
284
- console.log(pc.dim(` Props extracted: ${propsExtracted}`));
285
- console.log(pc.dim(` Usages found: ${usagesFound}`));
286
- console.log(pc.dim(` Relations inferred: ${allRelations.size}`));
287
- console.log(pc.dim(` Stories parsed: ${storiesParsed}`));
285
+ log(pc.dim("\n────────────────────────────────────────"));
286
+ log(pc.green(`\n✓ Generated fragments.json in ${elapsed}s`));
287
+ log(pc.dim(` Output: ${relative(process.cwd(), outputPath)}`));
288
+ log(pc.dim(` Components: ${Object.keys(fragments).length}`));
289
+ log(pc.dim(` Props extracted: ${propsExtracted}`));
290
+ log(pc.dim(` Usages found: ${usagesFound}`));
291
+ log(pc.dim(` Relations inferred: ${allRelations.size}`));
292
+ log(pc.dim(` Stories parsed: ${storiesParsed}`));
288
293
 
289
294
  if (warnings.length > 0) {
290
- console.log(pc.yellow(`\n ${warnings.length} warning(s)`));
295
+ log(pc.yellow(`\n ${warnings.length} warning(s)`));
291
296
  if (options.verbose) {
292
297
  for (const w of warnings) {
293
- console.log(pc.dim(` ${w.component}: ${w.warning}`));
298
+ log(pc.dim(` ${w.component}: ${w.warning}`));
294
299
  }
295
300
  }
296
301
  }
297
302
 
298
303
  if (errors.length > 0) {
299
- console.log(pc.red(`\n ${errors.length} error(s)`));
304
+ log(pc.red(`\n ${errors.length} error(s)`));
300
305
  for (const e of errors) {
301
- console.log(pc.dim(` ${e.component}: ${e.error}`));
306
+ log(pc.dim(` ${e.component}: ${e.error}`));
302
307
  }
303
308
  }
304
309
 
305
- console.log();
310
+ log();
306
311
 
307
312
  return {
308
313
  success: errors.length === 0,
@@ -36,13 +36,13 @@ export interface SetupResult {
36
36
  errors: string[];
37
37
  }
38
38
 
39
- type Framework = 'nextjs-app' | 'nextjs-pages' | 'vite' | 'unknown';
39
+ export type Framework = 'nextjs-app' | 'nextjs-pages' | 'vite' | 'remix' | 'astro' | 'unknown';
40
40
 
41
41
  // ============================================
42
42
  // Detection
43
43
  // ============================================
44
44
 
45
- async function fileExists(path: string): Promise<boolean> {
45
+ export async function fileExists(path: string): Promise<boolean> {
46
46
  try {
47
47
  await access(path);
48
48
  return true;
@@ -51,7 +51,7 @@ async function fileExists(path: string): Promise<boolean> {
51
51
  }
52
52
  }
53
53
 
54
- async function detectFramework(root: string): Promise<Framework> {
54
+ export async function detectSetupFramework(root: string): Promise<Framework> {
55
55
  // Next.js App Router
56
56
  if (
57
57
  await fileExists(join(root, 'app/layout.tsx')) ||
@@ -77,7 +77,23 @@ async function detectFramework(root: string): Promise<Framework> {
77
77
  return 'nextjs-app';
78
78
  }
79
79
 
80
- // Vite
80
+ // Remix
81
+ if (
82
+ await fileExists(join(root, 'app/root.tsx')) ||
83
+ await fileExists(join(root, 'app/root.ts'))
84
+ ) {
85
+ return 'remix';
86
+ }
87
+
88
+ // Astro
89
+ if (
90
+ await fileExists(join(root, 'astro.config.mjs')) ||
91
+ await fileExists(join(root, 'astro.config.ts'))
92
+ ) {
93
+ return 'astro';
94
+ }
95
+
96
+ // Vite (check after Remix/Astro since they also use Vite under the hood)
81
97
  if (
82
98
  await fileExists(join(root, 'vite.config.ts')) ||
83
99
  await fileExists(join(root, 'vite.config.js'))
@@ -85,10 +101,25 @@ async function detectFramework(root: string): Promise<Framework> {
85
101
  return 'vite';
86
102
  }
87
103
 
104
+ // Fallback: check package.json for framework deps
105
+ try {
106
+ const pkgPath = join(root, 'package.json');
107
+ const pkgContent = await readFile(pkgPath, 'utf-8');
108
+ const pkg = JSON.parse(pkgContent);
109
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
110
+
111
+ if (allDeps['next']) return 'nextjs-app';
112
+ if (allDeps['@remix-run/react']) return 'remix';
113
+ if (allDeps['astro']) return 'astro';
114
+ if (allDeps['vite']) return 'vite';
115
+ } catch {
116
+ // No package.json or parse error
117
+ }
118
+
88
119
  return 'unknown';
89
120
  }
90
121
 
91
- async function findEntryFile(root: string, framework: Framework): Promise<string | null> {
122
+ export async function findEntryFile(root: string, framework: Framework): Promise<string | null> {
92
123
  const candidates: string[] = [];
93
124
 
94
125
  switch (framework) {
@@ -101,6 +132,16 @@ async function findEntryFile(root: string, framework: Framework): Promise<string
101
132
  case 'nextjs-pages':
102
133
  candidates.push('pages/_app.tsx', 'pages/_app.ts');
103
134
  break;
135
+ case 'remix':
136
+ candidates.push('app/root.tsx', 'app/root.ts');
137
+ break;
138
+ case 'astro':
139
+ candidates.push(
140
+ 'src/layouts/Layout.astro',
141
+ 'src/layouts/BaseLayout.astro',
142
+ 'src/pages/index.astro'
143
+ );
144
+ break;
104
145
  case 'vite':
105
146
  candidates.push(
106
147
  'src/main.tsx', 'src/main.ts',
@@ -138,7 +179,7 @@ async function findNextConfig(root: string): Promise<string | null> {
138
179
  // Actions
139
180
  // ============================================
140
181
 
141
- async function addStylesImport(root: string, entryFile: string): Promise<{ modified: boolean; message: string }> {
182
+ export async function addStylesImport(root: string, entryFile: string): Promise<{ modified: boolean; message: string }> {
142
183
  const fullPath = join(root, entryFile);
143
184
  const content = await readFile(fullPath, 'utf-8');
144
185
 
@@ -146,22 +187,37 @@ async function addStylesImport(root: string, entryFile: string): Promise<{ modif
146
187
  return { modified: false, message: `Styles already imported in ${entryFile}` };
147
188
  }
148
189
 
149
- // Add import at the top of the file, after any 'use client' directive
150
190
  const stylesImport = "import '@fragments-sdk/ui/styles';";
151
191
  let newContent: string;
152
192
 
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);
193
+ // Astro files: insert inside frontmatter block (between --- fences)
194
+ if (entryFile.endsWith('.astro')) {
195
+ const fenceStart = content.indexOf('---');
196
+ if (fenceStart !== -1) {
197
+ const insertPos = fenceStart + 4; // after "---\n"
198
+ newContent = content.slice(0, insertPos) + stylesImport + '\n' + content.slice(insertPos);
199
+ } else {
200
+ // No frontmatter — add one
201
+ newContent = `---\n${stylesImport}\n---\n${content}`;
202
+ }
156
203
  } else {
157
- newContent = stylesImport + '\n' + content;
204
+ const useClientMatch = content.match(/^(?:\uFEFF)?[ \t]*['"]use client['"]\s*;?[ \t]*$/m);
205
+
206
+ if (useClientMatch && useClientMatch.index != null) {
207
+ const directiveLineEnd = content.indexOf('\n', useClientMatch.index);
208
+ const directiveEnd = directiveLineEnd === -1 ? content.length : directiveLineEnd + 1;
209
+ const separator = directiveLineEnd === -1 ? '\n' : '';
210
+ newContent = content.slice(0, directiveEnd) + separator + stylesImport + '\n' + content.slice(directiveEnd);
211
+ } else {
212
+ newContent = stylesImport + '\n' + content;
213
+ }
158
214
  }
159
215
 
160
216
  await writeFile(fullPath, newContent, 'utf-8');
161
217
  return { modified: true, message: `Added styles import to ${entryFile}` };
162
218
  }
163
219
 
164
- async function addThemeProvider(root: string, entryFile: string, framework: Framework): Promise<{ modified: boolean; message: string }> {
220
+ export async function addThemeProvider(root: string, entryFile: string, framework: Framework): Promise<{ modified: boolean; message: string }> {
165
221
  const fullPath = join(root, entryFile);
166
222
  const content = await readFile(fullPath, 'utf-8');
167
223
 
@@ -169,49 +225,81 @@ async function addThemeProvider(root: string, entryFile: string, framework: Fram
169
225
  return { modified: false, message: `ThemeProvider already present in ${entryFile}` };
170
226
  }
171
227
 
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';";
228
+ // Astro uses .astro files can't inject React imports
229
+ if (framework === 'astro') {
230
+ return { modified: false, message: 'Add ThemeProvider in your React island — see https://usefragments.com/getting-started#astro' };
231
+ }
176
232
 
177
- let newContent = content;
233
+ // Add provider import after the last import line
234
+ const providerImport = "import { ThemeProvider, TooltipProvider, ToastProvider } from '@fragments-sdk/ui';";
178
235
 
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
- }
236
+ let newContent = content;
237
+ const lines = content.split('\n');
187
238
 
188
- if (lastImportIdx >= 0) {
189
- importLines.splice(lastImportIdx + 1, 0, providerImport);
190
- newContent = importLines.join('\n');
191
- } else {
192
- newContent = providerImport + '\n' + content;
239
+ // Prefer placing right after the @fragments-sdk/ui/styles import if it exists
240
+ let insertIdx = -1;
241
+ for (let i = 0; i < lines.length; i++) {
242
+ if (lines[i].includes('@fragments-sdk/ui/styles')) {
243
+ insertIdx = i;
244
+ break;
193
245
  }
246
+ }
194
247
 
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
- };
248
+ // Otherwise, place after the last import line
249
+ if (insertIdx === -1) {
250
+ for (let i = 0; i < lines.length; i++) {
251
+ if (lines[i].startsWith('import ') || lines[i].startsWith("import '") || lines[i].startsWith('import "')) {
252
+ insertIdx = i;
253
+ }
205
254
  }
255
+ }
206
256
 
207
- await writeFile(fullPath, newContent, 'utf-8');
208
- return { modified: true, message: `Added provider imports to ${entryFile}. Wrap {children} with ThemeProvider.` };
257
+ if (insertIdx >= 0) {
258
+ lines.splice(insertIdx + 1, 0, providerImport);
259
+ newContent = lines.join('\n');
260
+ } else {
261
+ newContent = providerImport + '\n' + content;
209
262
  }
210
263
 
211
- return { modified: false, message: 'Manual ThemeProvider setup needed — see https://usefragments.com/getting-started#provider-setup' };
264
+ await writeFile(fullPath, newContent, 'utf-8');
265
+
266
+ // Framework-specific wrap instructions
267
+ if (framework === 'nextjs-app') {
268
+ const hint = !content.includes('suppressHydrationWarning')
269
+ ? `\n Add suppressHydrationWarning to your <html> tag`
270
+ : '';
271
+ return {
272
+ modified: true,
273
+ message: `Added provider imports to ${entryFile}. Wrap {children} with:\n` +
274
+ ` <ThemeProvider defaultMode="system"><TooltipProvider><ToastProvider>{children}</ToastProvider></TooltipProvider></ThemeProvider>${hint}`,
275
+ };
276
+ }
277
+
278
+ if (framework === 'nextjs-pages') {
279
+ return {
280
+ modified: true,
281
+ message: `Added provider imports to ${entryFile}. Wrap <Component {...pageProps} /> with:\n` +
282
+ ` <ThemeProvider defaultMode="system"><TooltipProvider><ToastProvider>...</ToastProvider></TooltipProvider></ThemeProvider>`,
283
+ };
284
+ }
285
+
286
+ if (framework === 'remix') {
287
+ return {
288
+ modified: true,
289
+ message: `Added provider imports to ${entryFile}. Wrap <Outlet /> with:\n` +
290
+ ` <ThemeProvider defaultMode="system"><TooltipProvider><ToastProvider>...</ToastProvider></TooltipProvider></ThemeProvider>`,
291
+ };
292
+ }
293
+
294
+ // Vite and unknown — generic instruction
295
+ return {
296
+ modified: true,
297
+ message: `Added provider imports to ${entryFile}. Wrap your app root with:\n` +
298
+ ` <ThemeProvider defaultMode="system"><TooltipProvider><ToastProvider>...</ToastProvider></TooltipProvider></ThemeProvider>`,
299
+ };
212
300
  }
213
301
 
214
- async function addTranspilePackages(root: string): Promise<{ modified: boolean; message: string }> {
302
+ export async function addTranspilePackages(root: string): Promise<{ modified: boolean; message: string }> {
215
303
  const configFile = await findNextConfig(root);
216
304
  if (!configFile) {
217
305
  return { modified: false, message: 'No next.config found' };
@@ -235,8 +323,8 @@ async function addTranspilePackages(root: string): Promise<{ modified: boolean;
235
323
  // Add transpilePackages to the config
236
324
  // Try to find the config object and add the property
237
325
  const patterns = [
238
- // const nextConfig = { ... }
239
- { search: /const\s+\w+\s*=\s*\{/, replacement: (match: string) => `${match}\n transpilePackages: ['@fragments-sdk/ui'],` },
326
+ // const nextConfig: NextConfig = { ... } (with optional type annotation)
327
+ { search: /const\s+\w+\s*(?::\s*\w+)?\s*=\s*\{/, replacement: (match: string) => `${match}\n transpilePackages: ['@fragments-sdk/ui'],` },
240
328
  // module.exports = { ... }
241
329
  { search: /module\.exports\s*=\s*\{/, replacement: (match: string) => `${match}\n transpilePackages: ['@fragments-sdk/ui'],` },
242
330
  // export default { ... }
@@ -380,12 +468,15 @@ export async function setup(options: SetupOptions = {}): Promise<SetupResult> {
380
468
  console.log(pc.cyan(`\n${BRAND.name} Setup\n`));
381
469
 
382
470
  // 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';
471
+ const framework = await detectSetupFramework(root);
472
+ const frameworkLabels: Record<string, string> = {
473
+ 'nextjs-app': 'Next.js (App Router)',
474
+ 'nextjs-pages': 'Next.js (Pages Router)',
475
+ 'vite': 'Vite',
476
+ 'remix': 'Remix',
477
+ 'astro': 'Astro',
478
+ };
479
+ const frameworkLabel = frameworkLabels[framework] || 'Unknown';
389
480
 
390
481
  console.log(` ${pc.dim('Framework:')} ${frameworkLabel}`);
391
482