@fragments-sdk/cli 0.14.3 → 0.15.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 (181) hide show
  1. package/README.md +0 -3
  2. package/dist/{ai-client-I6MDWNYA.js → ai-client-LSLQGOMM.js} +1 -2
  3. package/dist/bin.js +4745 -3817
  4. package/dist/bin.js.map +1 -1
  5. package/dist/{chunk-TXFCEDOC.js → chunk-2WXKALIG.js} +2 -2
  6. package/dist/{chunk-I34BC3CU.js → chunk-32LIWN2P.js} +1006 -3
  7. package/dist/chunk-32LIWN2P.js.map +1 -0
  8. package/dist/chunk-5JF26E55.js +1255 -0
  9. package/dist/chunk-5JF26E55.js.map +1 -0
  10. package/dist/{chunk-APTQIBS5.js → chunk-6SQPP47U.js} +153 -1342
  11. package/dist/chunk-6SQPP47U.js.map +1 -0
  12. package/dist/chunk-7DZC4YEV.js +294 -0
  13. package/dist/chunk-7DZC4YEV.js.map +1 -0
  14. package/dist/{chunk-PJT5IZ37.js → chunk-BJE3425I.js} +19 -52
  15. package/dist/{chunk-PJT5IZ37.js.map → chunk-BJE3425I.js.map} +1 -1
  16. package/dist/{chunk-55KERLWL.js → chunk-HQ6A6DTV.js} +1587 -1073
  17. package/dist/chunk-HQ6A6DTV.js.map +1 -0
  18. package/dist/chunk-MHIBEEW4.js +511 -0
  19. package/dist/chunk-MHIBEEW4.js.map +1 -0
  20. package/dist/{chunk-5A6X2Y73.js → chunk-ONUP6Z4W.js} +25 -13
  21. package/dist/chunk-ONUP6Z4W.js.map +1 -0
  22. package/dist/chunk-QCN35LJU.js +630 -0
  23. package/dist/chunk-QCN35LJU.js.map +1 -0
  24. package/dist/chunk-T47OLCSF.js +36 -0
  25. package/dist/chunk-T47OLCSF.js.map +1 -0
  26. package/dist/codebase-scanner-MQHUZC2G.js +21 -0
  27. package/dist/converter-7XM3Y6NJ.js +33 -0
  28. package/dist/converter-7XM3Y6NJ.js.map +1 -0
  29. package/dist/core/index.js +43 -2
  30. package/dist/create-IH4R45GE.js +806 -0
  31. package/dist/create-IH4R45GE.js.map +1 -0
  32. package/dist/{generate-RYWIPDN2.js → generate-PVOLUAAC.js} +4 -6
  33. package/dist/{generate-RYWIPDN2.js.map → generate-PVOLUAAC.js.map} +1 -1
  34. package/dist/govern-scan-OYFZYOQW.js +413 -0
  35. package/dist/govern-scan-OYFZYOQW.js.map +1 -0
  36. package/dist/index.d.ts +4 -23
  37. package/dist/index.js +15 -14
  38. package/dist/index.js.map +1 -1
  39. package/dist/{init-WRUSW7R5.js → init-SSGUSP7Z.js} +131 -129
  40. package/dist/init-SSGUSP7Z.js.map +1 -0
  41. package/dist/{init-cloud-REQ3XLHO.js → init-cloud-3DNKPWFB.js} +30 -5
  42. package/dist/{init-cloud-REQ3XLHO.js.map → init-cloud-3DNKPWFB.js.map} +1 -1
  43. package/dist/mcp-bin.js +5 -37
  44. package/dist/mcp-bin.js.map +1 -1
  45. package/dist/node-37AUE74M.js +65 -0
  46. package/dist/push-contracts-WY32TFP6.js +84 -0
  47. package/dist/push-contracts-WY32TFP6.js.map +1 -0
  48. package/dist/scan-PKSYSTRR.js +15 -0
  49. package/dist/{scan-generate-TFZVL3BT.js → scan-generate-VY27PIOX.js} +340 -52
  50. package/dist/scan-generate-VY27PIOX.js.map +1 -0
  51. package/dist/scanner-4KZNOXAK.js +12 -0
  52. package/dist/{service-HKJ6B7P7.js → service-QJGWUIVL.js} +41 -30
  53. package/dist/{snapshot-C5DYIGIV.js → snapshot-WIJMEIFT.js} +2 -3
  54. package/dist/{snapshot-C5DYIGIV.js.map → snapshot-WIJMEIFT.js.map} +1 -1
  55. package/dist/{static-viewer-DUVC4UIM.js → static-viewer-7QIBQZRC.js} +3 -4
  56. package/dist/static-viewer-7QIBQZRC.js.map +1 -0
  57. package/dist/{test-JW7JIDFG.js → test-64Z5BKBA.js} +4 -7
  58. package/dist/{test-JW7JIDFG.js.map → test-64Z5BKBA.js.map} +1 -1
  59. package/dist/token-normalizer-TEPOVBPV.js +312 -0
  60. package/dist/token-normalizer-TEPOVBPV.js.map +1 -0
  61. package/dist/token-parser-32KOIOFN.js +22 -0
  62. package/dist/token-parser-32KOIOFN.js.map +1 -0
  63. package/dist/{tokens-KE73G5JC.js → tokens-NZWFQIAB.js} +10 -9
  64. package/dist/{tokens-KE73G5JC.js.map → tokens-NZWFQIAB.js.map} +1 -1
  65. package/dist/tokens-generate-5JQSJ27E.js +85 -0
  66. package/dist/tokens-generate-5JQSJ27E.js.map +1 -0
  67. package/dist/tokens-push-HY3KO36V.js +148 -0
  68. package/dist/tokens-push-HY3KO36V.js.map +1 -0
  69. package/package.json +8 -6
  70. package/src/bin.ts +300 -48
  71. package/src/commands/__fixtures__/shadcn-label-wrapper/package.json +7 -0
  72. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.contract.json +42 -0
  73. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.tsx +11 -0
  74. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.contract.json +20 -0
  75. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.tsx +14 -0
  76. package/src/commands/__fixtures__/shadcn-label-wrapper/tsconfig.app.json +23 -0
  77. package/src/commands/__tests__/build-freshness.test.ts +231 -0
  78. package/src/commands/__tests__/create.test.ts +71 -0
  79. package/src/commands/__tests__/drift-sync.test.ts +1 -1
  80. package/src/commands/__tests__/govern.test.ts +258 -0
  81. package/src/commands/__tests__/init.test.ts +113 -0
  82. package/src/commands/__tests__/scan-generate.test.ts +189 -70
  83. package/src/commands/__tests__/verify.test.ts +91 -0
  84. package/src/commands/build.ts +54 -1
  85. package/src/commands/context.ts +1 -1
  86. package/src/commands/create.ts +536 -0
  87. package/src/commands/discover.ts +151 -0
  88. package/src/commands/doctor.ts +3 -2
  89. package/src/commands/enhance.ts +3 -1
  90. package/src/commands/govern-scan.ts +565 -0
  91. package/src/commands/govern.ts +67 -4
  92. package/src/commands/init-cloud.ts +32 -4
  93. package/src/commands/init.ts +152 -28
  94. package/src/commands/inspect.ts +290 -0
  95. package/src/commands/migrate-contract.ts +85 -0
  96. package/src/commands/push-contracts.ts +112 -0
  97. package/src/commands/scan-generate.ts +439 -51
  98. package/src/commands/scan.ts +14 -0
  99. package/src/commands/setup.ts +27 -50
  100. package/src/commands/sync.ts +2 -2
  101. package/src/commands/tokens-generate.ts +113 -0
  102. package/src/commands/tokens-push.ts +199 -0
  103. package/src/commands/verify.ts +195 -1
  104. package/src/core/__fixtures__/shadcn-input/input.tsx +7 -0
  105. package/src/core/__fixtures__/shadcn-input/tsconfig.json +14 -0
  106. package/src/core/__fixtures__/shadcn-label/label.tsx +11 -0
  107. package/src/core/__fixtures__/shadcn-label/primitive.tsx +14 -0
  108. package/src/core/__fixtures__/shadcn-label/tsconfig.json +14 -0
  109. package/src/core/__fixtures__/shadcn-radix-label/label.tsx +11 -0
  110. package/src/core/__fixtures__/shadcn-radix-label/node_modules/radix-ui/index.d.ts +12 -0
  111. package/src/core/__fixtures__/shadcn-radix-label/tsconfig.json +14 -0
  112. package/src/core/__tests__/contract-parity.test.ts +316 -0
  113. package/src/core/__tests__/token-resolver.test.ts +1 -1
  114. package/src/core/component-extractor.test.ts +40 -1
  115. package/src/core/config.ts +2 -1
  116. package/src/core/discovery.ts +13 -2
  117. package/src/core/drift-verifier.ts +123 -0
  118. package/src/core/extractor-adapter.ts +80 -0
  119. package/src/index.ts +3 -3
  120. package/src/mcp/__tests__/projectFields.test.ts +1 -1
  121. package/src/mcp/utils.ts +1 -50
  122. package/src/migrate/converter.ts +3 -3
  123. package/src/migrate/fragment-to-contract.ts +253 -0
  124. package/src/migrate/report.ts +1 -1
  125. package/src/scripts/token-benchmark.ts +121 -0
  126. package/src/service/__tests__/props-extractor.test.ts +94 -0
  127. package/src/service/__tests__/token-normalizer.test.ts +690 -0
  128. package/src/service/ast-utils.ts +4 -23
  129. package/src/service/babel-config.ts +23 -0
  130. package/src/service/enhance/converter.ts +61 -0
  131. package/src/service/enhance/props-extractor.ts +25 -8
  132. package/src/service/enhance/scanner.ts +5 -24
  133. package/src/service/index.ts +8 -0
  134. package/src/service/snippet-validation.ts +9 -3
  135. package/src/service/tailwind-v4-parser.ts +314 -0
  136. package/src/service/token-normalizer.ts +510 -0
  137. package/src/service/token-parser.ts +56 -0
  138. package/src/setup.ts +10 -39
  139. package/src/shared/index.ts +1 -0
  140. package/src/shared/project-fields.ts +46 -0
  141. package/src/theme/__tests__/component-contrast.test.ts +2 -2
  142. package/src/theme/__tests__/serializer.test.ts +1 -1
  143. package/src/theme/generator.ts +16 -1
  144. package/src/theme/schema.ts +8 -0
  145. package/src/theme/serializer.ts +13 -9
  146. package/src/theme/types.ts +8 -0
  147. package/src/validators.ts +1 -2
  148. package/src/viewer/__tests__/viewer-integration.test.ts +8 -8
  149. package/src/viewer/style-utils.ts +27 -412
  150. package/src/viewer/vite-plugin.ts +2 -2
  151. package/dist/chunk-55KERLWL.js.map +0 -1
  152. package/dist/chunk-5A6X2Y73.js.map +0 -1
  153. package/dist/chunk-APTQIBS5.js.map +0 -1
  154. package/dist/chunk-EYXVAMEX.js +0 -626
  155. package/dist/chunk-EYXVAMEX.js.map +0 -1
  156. package/dist/chunk-I34BC3CU.js.map +0 -1
  157. package/dist/chunk-LOYS64QS.js +0 -2453
  158. package/dist/chunk-LOYS64QS.js.map +0 -1
  159. package/dist/chunk-Z7EY4VHE.js +0 -50
  160. package/dist/chunk-ZKTFKHWN.js +0 -324
  161. package/dist/chunk-ZKTFKHWN.js.map +0 -1
  162. package/dist/discovery-VDANZAJ2.js +0 -28
  163. package/dist/init-WRUSW7R5.js.map +0 -1
  164. package/dist/sass.node-4XJK6YBF.js +0 -130708
  165. package/dist/sass.node-4XJK6YBF.js.map +0 -1
  166. package/dist/scan-YJHQIRKG.js +0 -14
  167. package/dist/scan-generate-TFZVL3BT.js.map +0 -1
  168. package/dist/viewer-2TZS3NDL.js +0 -2730
  169. package/dist/viewer-2TZS3NDL.js.map +0 -1
  170. package/src/build.ts +0 -612
  171. package/src/commands/dev.ts +0 -107
  172. package/src/core/auto-props.ts +0 -464
  173. package/src/core/component-extractor.ts +0 -1030
  174. package/src/core/token-resolver.ts +0 -155
  175. /package/dist/{ai-client-I6MDWNYA.js.map → ai-client-LSLQGOMM.js.map} +0 -0
  176. /package/dist/{chunk-TXFCEDOC.js.map → chunk-2WXKALIG.js.map} +0 -0
  177. /package/dist/{chunk-Z7EY4VHE.js.map → codebase-scanner-MQHUZC2G.js.map} +0 -0
  178. /package/dist/{discovery-VDANZAJ2.js.map → node-37AUE74M.js.map} +0 -0
  179. /package/dist/{scan-YJHQIRKG.js.map → scan-PKSYSTRR.js.map} +0 -0
  180. /package/dist/{service-HKJ6B7P7.js.map → scanner-4KZNOXAK.js.map} +0 -0
  181. /package/dist/{static-viewer-DUVC4UIM.js.map → service-QJGWUIVL.js.map} +0 -0
@@ -0,0 +1,536 @@
1
+ /**
2
+ * fragments create - Scaffold a new project with Fragments UI and a custom theme
3
+ *
4
+ * Mirrors the shadcn model: configure on the web (usefragments.com/create),
5
+ * then `npx @fragments-sdk/cli create` bootstraps a project with that config.
6
+ */
7
+
8
+ import { execSync } from 'node:child_process';
9
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, unlinkSync } from 'node:fs';
10
+ import { join, resolve } from 'node:path';
11
+ import pc from 'picocolors';
12
+ import { BRAND } from '../core/index.js';
13
+ import { decompressTheme } from '../theme/serializer.js';
14
+ import { generateCssTokens, generateScssTokens } from '../theme/generator.js';
15
+ import type { ThemeConfig } from '../theme/types.js';
16
+
17
+ // ============================================
18
+ // Types
19
+ // ============================================
20
+
21
+ export interface CreateOptions {
22
+ name?: string;
23
+ template?: 'nextjs' | 'vite';
24
+ packageManager?: 'npm' | 'pnpm' | 'yarn' | 'bun';
25
+ theme?: string;
26
+ preset?: string;
27
+ brand?: string;
28
+ scss?: boolean;
29
+ mcp?: boolean;
30
+ yes?: boolean;
31
+ noGit?: boolean;
32
+ }
33
+
34
+ export interface CreateResult {
35
+ success: boolean;
36
+ projectDir?: string;
37
+ error?: string;
38
+ }
39
+
40
+ // ============================================
41
+ // Package Manager Detection
42
+ // ============================================
43
+
44
+ function detectPackageManager(): 'npm' | 'pnpm' | 'yarn' | 'bun' {
45
+ const agent = process.env.npm_config_user_agent || '';
46
+ if (agent.startsWith('pnpm')) return 'pnpm';
47
+ if (agent.startsWith('yarn')) return 'yarn';
48
+ if (agent.startsWith('bun')) return 'bun';
49
+ return 'npm';
50
+ }
51
+
52
+ function getInstallCommand(pm: string): string {
53
+ return pm === 'yarn' ? 'yarn add' : `${pm} add`;
54
+ }
55
+
56
+ function getDevInstallCommand(pm: string): string {
57
+ return pm === 'yarn' ? 'yarn add -D' : `${pm} add -D`;
58
+ }
59
+
60
+ function getRunCommand(pm: string): string {
61
+ return pm === 'npm' ? 'npm run' : pm;
62
+ }
63
+
64
+ // ============================================
65
+ // Validation
66
+ // ============================================
67
+
68
+ function isValidProjectName(name: string): boolean {
69
+ return /^[a-z0-9][a-z0-9._-]*$/.test(name);
70
+ }
71
+
72
+ // ============================================
73
+ // Theme Resolution
74
+ // ============================================
75
+
76
+ const PRESET_API_URL = 'https://canny-otter-874.convex.site/api/theme-presets';
77
+
78
+ async function fetchPreset(presetId: string): Promise<ThemeConfig | null> {
79
+ try {
80
+ const res = await fetch(`${PRESET_API_URL}?id=${encodeURIComponent(presetId)}`);
81
+ if (!res.ok) return null;
82
+ const theme = await res.json();
83
+ if (!theme || !theme.name) return null;
84
+ return theme as ThemeConfig;
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
89
+
90
+ async function resolveTheme(options: CreateOptions): Promise<ThemeConfig | null> {
91
+ if (options.preset) {
92
+ console.log(pc.dim(` Fetching theme preset ${options.preset}...`));
93
+ const theme = await fetchPreset(options.preset);
94
+ if (!theme) {
95
+ console.error(pc.red(`Error: Could not fetch preset "${options.preset}". It may have expired or the ID is invalid.`));
96
+ return null;
97
+ }
98
+ return theme;
99
+ }
100
+
101
+ if (options.theme) {
102
+ const decoded = decompressTheme(options.theme);
103
+ if (!decoded) {
104
+ console.error(pc.red('Error: Could not decode theme string. Make sure you copied it from usefragments.com/create'));
105
+ return null;
106
+ }
107
+ return decoded;
108
+ }
109
+
110
+ if (options.brand) {
111
+ return {
112
+ name: 'custom',
113
+ colors: {
114
+ accent: options.brand,
115
+ },
116
+ };
117
+ }
118
+
119
+ return {
120
+ name: 'default',
121
+ colors: {
122
+ accent: '#6366f1',
123
+ },
124
+ };
125
+ }
126
+
127
+ // ============================================
128
+ // Templates
129
+ // ============================================
130
+
131
+ export function generateNextjsLayout(themePath: string): string {
132
+ return `import type { Metadata } from 'next';
133
+ import '@fragments-sdk/ui/styles';
134
+ import '${themePath}';
135
+ import { Providers } from './providers';
136
+
137
+ export const metadata: Metadata = {
138
+ title: 'My App',
139
+ description: 'Built with Fragments UI',
140
+ };
141
+
142
+ export default function RootLayout({
143
+ children,
144
+ }: {
145
+ children: React.ReactNode;
146
+ }) {
147
+ return (
148
+ <html lang="en" suppressHydrationWarning>
149
+ <body>
150
+ <Providers>{children}</Providers>
151
+ </body>
152
+ </html>
153
+ );
154
+ }
155
+ `;
156
+ }
157
+
158
+ export function generateNextjsProviders(): string {
159
+ return `'use client';
160
+
161
+ import type { ReactNode } from 'react';
162
+ import { ThemeProvider, TooltipProvider, ToastProvider } from '@fragments-sdk/ui';
163
+
164
+ export function Providers({ children }: { children: ReactNode }) {
165
+ return (
166
+ <ThemeProvider defaultMode="system">
167
+ <TooltipProvider>
168
+ <ToastProvider>{children}</ToastProvider>
169
+ </TooltipProvider>
170
+ </ThemeProvider>
171
+ );
172
+ }
173
+ `;
174
+ }
175
+
176
+ export function generateNextjsPage(): string {
177
+ return `'use client';
178
+
179
+ import { Button, Card, Stack, Text, Input } from '@fragments-sdk/ui';
180
+
181
+ export default function Home() {
182
+ return (
183
+ <main style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
184
+ <Card style={{ maxWidth: 480, width: '100%' }}>
185
+ <Card.Header>
186
+ <Card.Title>Welcome to Fragments</Card.Title>
187
+ <Card.Description>Your app is ready. Start building something great.</Card.Description>
188
+ </Card.Header>
189
+ <Card.Body>
190
+ <Stack gap={3}>
191
+ <Input placeholder="Enter something..." />
192
+ <Stack direction="row" gap={2}>
193
+ <Button>Get Started</Button>
194
+ <Button variant="secondary">Learn More</Button>
195
+ </Stack>
196
+ </Stack>
197
+ </Card.Body>
198
+ </Card>
199
+ </main>
200
+ );
201
+ }
202
+ `;
203
+ }
204
+
205
+ function generateViteMain(themePath: string): string {
206
+ return `import { StrictMode } from 'react';
207
+ import { createRoot } from 'react-dom/client';
208
+ import { ThemeProvider, TooltipProvider, ToastProvider } from '@fragments-sdk/ui';
209
+ import '@fragments-sdk/ui/styles';
210
+ import '${themePath}';
211
+ import App from './App';
212
+
213
+ createRoot(document.getElementById('root')!).render(
214
+ <StrictMode>
215
+ <ThemeProvider defaultMode="system">
216
+ <TooltipProvider>
217
+ <ToastProvider>
218
+ <App />
219
+ </ToastProvider>
220
+ </TooltipProvider>
221
+ </ThemeProvider>
222
+ </StrictMode>,
223
+ );
224
+ `;
225
+ }
226
+
227
+ function generateViteApp(): string {
228
+ return `import { Button, Card, Stack, Text, Input } from '@fragments-sdk/ui';
229
+
230
+ function App() {
231
+ return (
232
+ <main style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
233
+ <Card style={{ maxWidth: 480, width: '100%' }}>
234
+ <Card.Header>
235
+ <Card.Title>Welcome to Fragments</Card.Title>
236
+ <Card.Description>Your app is ready. Start building something great.</Card.Description>
237
+ </Card.Header>
238
+ <Card.Body>
239
+ <Stack gap={3}>
240
+ <Input placeholder="Enter something..." />
241
+ <Stack direction="row" gap={2}>
242
+ <Button>Get Started</Button>
243
+ <Button variant="secondary">Learn More</Button>
244
+ </Stack>
245
+ </Stack>
246
+ </Card.Body>
247
+ </Card>
248
+ </main>
249
+ );
250
+ }
251
+
252
+ export default App;
253
+ `;
254
+ }
255
+
256
+ // ============================================
257
+ // Scaffolding
258
+ // ============================================
259
+
260
+ async function promptIfMissing(options: CreateOptions): Promise<CreateOptions> {
261
+ const resolved = { ...options };
262
+
263
+ if (!resolved.name) {
264
+ if (resolved.yes) {
265
+ resolved.name = 'my-app';
266
+ } else {
267
+ try {
268
+ const { input } = await import('@inquirer/prompts');
269
+ resolved.name = await input({
270
+ message: 'Project name:',
271
+ default: 'my-app',
272
+ validate: (val) => isValidProjectName(val) || 'Use lowercase letters, numbers, hyphens, dots, underscores',
273
+ });
274
+ } catch {
275
+ resolved.name = 'my-app';
276
+ }
277
+ }
278
+ }
279
+
280
+ if (!resolved.template && !resolved.yes) {
281
+ try {
282
+ const { select } = await import('@inquirer/prompts');
283
+ resolved.template = await select({
284
+ message: 'Framework:',
285
+ choices: [
286
+ { name: 'Next.js (App Router)', value: 'nextjs' as const },
287
+ { name: 'Vite + React', value: 'vite' as const },
288
+ ],
289
+ default: 'nextjs',
290
+ });
291
+ } catch {
292
+ resolved.template = 'nextjs';
293
+ }
294
+ }
295
+
296
+ resolved.template = resolved.template || 'nextjs';
297
+ resolved.packageManager = resolved.packageManager || detectPackageManager();
298
+
299
+ return resolved;
300
+ }
301
+
302
+ function scaffoldFramework(name: string, template: string, pm: string): void {
303
+ console.log(pc.cyan(`\nScaffolding ${template === 'nextjs' ? 'Next.js' : 'Vite + React'} project...\n`));
304
+
305
+ if (template === 'nextjs') {
306
+ const cmd = `npx create-next-app@latest ${name} --ts --app --src-dir --no-tailwind --no-eslint --import-alias "@/*" --yes`;
307
+ execSync(cmd, { stdio: 'inherit' });
308
+ } else {
309
+ // Vite
310
+ if (pm === 'bun') {
311
+ execSync(`bun create vite ${name} --template react-ts`, { stdio: 'inherit' });
312
+ } else {
313
+ execSync(`npm create vite@latest ${name} -- --template react-ts`, { stdio: 'inherit' });
314
+ }
315
+ }
316
+ }
317
+
318
+ function installDeps(projectDir: string, pm: string, scss: boolean): void {
319
+ console.log(pc.cyan('\nInstalling Fragments UI...\n'));
320
+
321
+ const install = getInstallCommand(pm);
322
+ execSync(`${install} @fragments-sdk/ui`, { cwd: projectDir, stdio: 'inherit' });
323
+
324
+ if (scss) {
325
+ const devInstall = getDevInstallCommand(pm);
326
+ execSync(`${devInstall} sass`, { cwd: projectDir, stdio: 'inherit' });
327
+ }
328
+ }
329
+
330
+ function writeThemeFile(projectDir: string, theme: ThemeConfig, scss: boolean, template: string): string {
331
+ const stylesDir = join(projectDir, 'src', 'styles');
332
+ mkdirSync(stylesDir, { recursive: true });
333
+
334
+ const ext = scss ? 'scss' : 'css';
335
+ const content = scss ? generateScssTokens(theme) : generateCssTokens(theme);
336
+ const filePath = join(stylesDir, `theme.${ext}`);
337
+ writeFileSync(filePath, content, 'utf-8');
338
+
339
+ // Next.js layout is at src/app/layout.tsx → relative path is ../styles/theme.ext
340
+ // Vite main is at src/main.tsx → relative path is ./styles/theme.ext
341
+ if (template === 'nextjs') {
342
+ return `../styles/theme.${ext}`;
343
+ }
344
+ return `./styles/theme.${ext}`;
345
+ }
346
+
347
+ export function addNextTranspilePackages(projectDir: string): void {
348
+ const configCandidates = ['next.config.ts', 'next.config.mjs', 'next.config.js'];
349
+
350
+ for (const configFile of configCandidates) {
351
+ const fullPath = join(projectDir, configFile);
352
+ if (!existsSync(fullPath)) continue;
353
+
354
+ const content = readFileSync(fullPath, 'utf-8');
355
+
356
+ if (content.includes('transpilePackages') && content.includes('@fragments-sdk/ui')) {
357
+ return;
358
+ }
359
+
360
+ if (content.includes('transpilePackages')) {
361
+ return;
362
+ }
363
+
364
+ const patterns = [
365
+ {
366
+ search: /const\s+\w+\s*(?::\s*\w+)?\s*=\s*\{/,
367
+ replacement: (match: string) => `${match}\n transpilePackages: ['@fragments-sdk/ui'],`,
368
+ },
369
+ {
370
+ search: /module\.exports\s*=\s*\{/,
371
+ replacement: (match: string) => `${match}\n transpilePackages: ['@fragments-sdk/ui'],`,
372
+ },
373
+ {
374
+ search: /export\s+default\s*\{/,
375
+ replacement: (match: string) => `${match}\n transpilePackages: ['@fragments-sdk/ui'],`,
376
+ },
377
+ ];
378
+
379
+ for (const pattern of patterns) {
380
+ if (!pattern.search.test(content)) continue;
381
+
382
+ writeFileSync(
383
+ fullPath,
384
+ content.replace(pattern.search, pattern.replacement),
385
+ 'utf-8',
386
+ );
387
+ return;
388
+ }
389
+
390
+ return;
391
+ }
392
+ }
393
+
394
+ function rewriteAppFiles(projectDir: string, template: string, themePath: string): void {
395
+ if (template === 'nextjs') {
396
+ // Rewrite layout.tsx
397
+ const layoutPath = join(projectDir, 'src', 'app', 'layout.tsx');
398
+ writeFileSync(layoutPath, generateNextjsLayout(themePath), 'utf-8');
399
+
400
+ const providersPath = join(projectDir, 'src', 'app', 'providers.tsx');
401
+ writeFileSync(providersPath, generateNextjsProviders(), 'utf-8');
402
+
403
+ // Rewrite page.tsx
404
+ const pagePath = join(projectDir, 'src', 'app', 'page.tsx');
405
+ writeFileSync(pagePath, generateNextjsPage(), 'utf-8');
406
+
407
+ // Remove default page.module.css if it exists
408
+ const moduleCssPath = join(projectDir, 'src', 'app', 'page.module.css');
409
+ try {
410
+ unlinkSync(moduleCssPath);
411
+ } catch { /* doesn't exist, that's fine */ }
412
+
413
+ // Remove globals.css if it exists (we use our own theme)
414
+ const globalsCssPath = join(projectDir, 'src', 'app', 'globals.css');
415
+ try {
416
+ unlinkSync(globalsCssPath);
417
+ } catch { /* doesn't exist */ }
418
+
419
+ addNextTranspilePackages(projectDir);
420
+ } else {
421
+ // Vite: rewrite main.tsx and App.tsx
422
+ const mainPath = join(projectDir, 'src', 'main.tsx');
423
+ writeFileSync(mainPath, generateViteMain(themePath), 'utf-8');
424
+
425
+ const appPath = join(projectDir, 'src', 'App.tsx');
426
+ writeFileSync(appPath, generateViteApp(), 'utf-8');
427
+
428
+ // Clean up default Vite files
429
+ for (const file of ['src/App.css', 'src/index.css']) {
430
+ try {
431
+ unlinkSync(join(projectDir, file));
432
+ } catch { /* doesn't exist */ }
433
+ }
434
+ }
435
+ }
436
+
437
+ function initGit(projectDir: string): void {
438
+ try {
439
+ // create-next-app already inits git, so check first
440
+ if (!existsSync(join(projectDir, '.git'))) {
441
+ execSync('git init', { cwd: projectDir, stdio: 'ignore' });
442
+ execSync('git add -A', { cwd: projectDir, stdio: 'ignore' });
443
+ execSync('git commit -m "Initial commit with Fragments UI"', { cwd: projectDir, stdio: 'ignore' });
444
+ }
445
+ } catch {
446
+ // Git not available — skip silently
447
+ }
448
+ }
449
+
450
+ function configureMcp(projectDir: string): void {
451
+ const mcpConfig = {
452
+ mcpServers: {
453
+ fragments: {
454
+ command: 'npx',
455
+ args: ['-y', '@fragments-sdk/cli', 'mcp'],
456
+ },
457
+ },
458
+ };
459
+
460
+ const mcpDir = join(projectDir, '.mcp');
461
+ mkdirSync(mcpDir, { recursive: true });
462
+ writeFileSync(join(mcpDir, 'config.json'), JSON.stringify(mcpConfig, null, 2), 'utf-8');
463
+ }
464
+
465
+ // ============================================
466
+ // Main
467
+ // ============================================
468
+
469
+ export async function create(options: CreateOptions): Promise<CreateResult> {
470
+ console.log(pc.cyan(`\n${BRAND.name} Create\n`));
471
+
472
+ // 1. Gather inputs
473
+ const resolved = await promptIfMissing(options);
474
+ const name = resolved.name!;
475
+ const template = resolved.template!;
476
+ const pm = resolved.packageManager!;
477
+
478
+ // 2. Validate
479
+ if (!isValidProjectName(name)) {
480
+ return { success: false, error: `Invalid project name: ${name}. Use lowercase letters, numbers, hyphens, dots, underscores.` };
481
+ }
482
+
483
+ const projectDir = resolve(process.cwd(), name);
484
+ if (existsSync(projectDir)) {
485
+ return { success: false, error: `Directory "${name}" already exists.` };
486
+ }
487
+
488
+ // 3. Resolve theme
489
+ const theme = await resolveTheme(resolved);
490
+ if (!theme) {
491
+ return { success: false, error: 'Invalid theme configuration.' };
492
+ }
493
+
494
+ // 4. Scaffold framework
495
+ scaffoldFramework(name, template, pm);
496
+
497
+ if (!existsSync(projectDir)) {
498
+ return { success: false, error: 'Framework scaffolding failed — project directory was not created.' };
499
+ }
500
+
501
+ // 5. Install deps
502
+ installDeps(projectDir, pm, !!resolved.scss);
503
+
504
+ // 6. Write theme tokens
505
+ const themePath = writeThemeFile(projectDir, theme, !!resolved.scss, template);
506
+
507
+ // 7. Rewrite app files with providers + theme import
508
+ rewriteAppFiles(projectDir, template, themePath);
509
+
510
+ // 8. MCP config
511
+ if (resolved.mcp) {
512
+ configureMcp(projectDir);
513
+ console.log(pc.dim(' Configured MCP server for AI tooling'));
514
+ }
515
+
516
+ // 9. Git init
517
+ if (!resolved.noGit) {
518
+ initGit(projectDir);
519
+ }
520
+
521
+ // 10. Success message
522
+ const run = getRunCommand(pm);
523
+ console.log('');
524
+ console.log(pc.green(' Project created successfully!'));
525
+ console.log('');
526
+ console.log(` ${pc.cyan('cd')} ${name}`);
527
+ console.log(` ${pc.cyan(run)} dev`);
528
+ console.log('');
529
+
530
+ if (theme.name !== 'default') {
531
+ console.log(pc.dim(` Theme "${theme.name}" applied with full token set.`));
532
+ console.log(pc.dim(` Edit src/styles/theme.${resolved.scss ? 'scss' : 'css'} to customize.\n`));
533
+ }
534
+
535
+ return { success: true, projectDir };
536
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * `fragments discover` — list and filter components from fragments.json.
3
+ *
4
+ * Loads the compiled output and provides listing, search, category
5
+ * filtering, and status filtering. Outputs a formatted table, compact
6
+ * names list, or JSON depending on flags.
7
+ *
8
+ * This is the LIST mode only — semantic/useCase search requires the
9
+ * Orama sidecar available via the MCP server.
10
+ */
11
+
12
+ import pc from 'picocolors';
13
+ import { readFile } from 'node:fs/promises';
14
+ import { resolve } from 'node:path';
15
+ import type { CompiledFragmentsFile } from '../core/index.js';
16
+ import { BRAND } from '../core/index.js';
17
+ import { loadConfig } from '../core/node.js';
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Types
21
+ // ---------------------------------------------------------------------------
22
+
23
+ export interface DiscoverCommandOptions {
24
+ config?: string;
25
+ search?: string;
26
+ category?: string;
27
+ status?: string;
28
+ compact?: boolean;
29
+ json?: boolean;
30
+ }
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Command implementation
34
+ // ---------------------------------------------------------------------------
35
+
36
+ export async function discover(options: DiscoverCommandOptions): Promise<void> {
37
+ const { config, configDir } = await loadConfig(options.config);
38
+ const outputPath = resolve(configDir, config.outFile ?? BRAND.outFile);
39
+
40
+ let data: CompiledFragmentsFile;
41
+ try {
42
+ const content = await readFile(outputPath, 'utf-8');
43
+ data = JSON.parse(content) as CompiledFragmentsFile;
44
+ } catch {
45
+ console.error(
46
+ pc.red(`Error: Could not load ${BRAND.outFile}. Run \`${BRAND.cliCommand} build\` first.`),
47
+ );
48
+ process.exit(1);
49
+ }
50
+
51
+ let fragments = Object.values(data.fragments);
52
+
53
+ // --- Search filter (name, description, tags) ---
54
+ if (options.search) {
55
+ const term = options.search.toLowerCase();
56
+ fragments = fragments.filter(
57
+ (f) =>
58
+ f.meta.name.toLowerCase().includes(term) ||
59
+ f.meta.description?.toLowerCase().includes(term) ||
60
+ f.meta.tags?.some((t) => t.toLowerCase().includes(term)),
61
+ );
62
+ }
63
+
64
+ // --- Category filter ---
65
+ if (options.category) {
66
+ const cat = options.category.trim().toLowerCase();
67
+ fragments = fragments.filter(
68
+ (f) => f.meta.category?.toLowerCase() === cat,
69
+ );
70
+ }
71
+
72
+ // --- Status filter ---
73
+ if (options.status) {
74
+ const status = options.status.trim().toLowerCase();
75
+ fragments = fragments.filter((f) => (f.meta.status ?? 'stable') === status);
76
+ }
77
+
78
+ // Sort alphabetically
79
+ fragments.sort((a, b) => a.meta.name.localeCompare(b.meta.name));
80
+
81
+ const categories = [...new Set(fragments.map((f) => f.meta.category).filter(Boolean))].sort();
82
+
83
+ // --- JSON output ---
84
+ if (options.json) {
85
+ const output = {
86
+ total: fragments.length,
87
+ fragments: fragments.map((f) => ({
88
+ name: f.meta.name,
89
+ category: f.meta.category,
90
+ status: f.meta.status ?? 'stable',
91
+ description: f.meta.description,
92
+ tags: f.meta.tags,
93
+ variantCount: f.variants.length,
94
+ propCount: Object.keys(f.props ?? {}).length,
95
+ })),
96
+ categories,
97
+ };
98
+ console.log(JSON.stringify(output, null, 2));
99
+ return;
100
+ }
101
+
102
+ // --- Compact output (names only) ---
103
+ if (options.compact) {
104
+ for (const f of fragments) {
105
+ console.log(f.meta.name);
106
+ }
107
+ return;
108
+ }
109
+
110
+ // --- Table output ---
111
+ if (fragments.length === 0) {
112
+ console.log(pc.yellow('\nNo components found matching the given filters.\n'));
113
+ return;
114
+ }
115
+
116
+ console.log(pc.bold(`\n${BRAND.name} Components (${fragments.length})\n`));
117
+
118
+ console.log(
119
+ pc.dim(
120
+ ` ${'Name'.padEnd(28)} ${'Category'.padEnd(16)} ${'Status'.padEnd(14)} ${'Variants'.padEnd(10)} ${'Props'}`,
121
+ ),
122
+ );
123
+ console.log(pc.dim(` ${'─'.repeat(80)}`));
124
+
125
+ for (const f of fragments) {
126
+ const name = f.meta.name.length > 26 ? f.meta.name.slice(0, 23) + '...' : f.meta.name;
127
+ const category = (f.meta.category ?? '').padEnd(16);
128
+ const status = f.meta.status ?? 'stable';
129
+ const statusColored =
130
+ status === 'stable' ? pc.green(status.padEnd(14)) :
131
+ status === 'deprecated' ? pc.red(status.padEnd(14)) :
132
+ status === 'beta' ? pc.yellow(status.padEnd(14)) :
133
+ pc.cyan(status.padEnd(14));
134
+ const variantCount = String(f.variants.length).padEnd(10);
135
+ const propCount = String(Object.keys(f.props ?? {}).length);
136
+
137
+ console.log(
138
+ ` ${pc.cyan(name.padEnd(28))} ${pc.dim(category)} ${statusColored} ${variantCount} ${propCount}`,
139
+ );
140
+ }
141
+
142
+ // Summary
143
+ console.log(pc.dim(`\n ${'─'.repeat(80)}`));
144
+ console.log(
145
+ pc.dim(` ${fragments.length} component(s) across ${categories.length} categories`),
146
+ );
147
+ if (categories.length > 0) {
148
+ console.log(pc.dim(` Categories: ${categories.join(', ')}`));
149
+ }
150
+ console.log();
151
+ }
@@ -13,6 +13,7 @@ import { readFile, access } from 'node:fs/promises';
13
13
  import { join, resolve } from 'node:path';
14
14
  import pc from 'picocolors';
15
15
  import { BRAND } from '../core/index.js';
16
+ import { NEUTRAL_PALETTES } from '@fragments-sdk/viewer/docs-data';
16
17
 
17
18
  // ============================================
18
19
  // Types
@@ -43,10 +44,10 @@ export interface DoctorResult {
43
44
  }
44
45
 
45
46
  // ============================================
46
- // Valid seed values (match _seeds.scss)
47
+ // Valid seed values derived from canonical sources
47
48
  // ============================================
48
49
 
49
- const VALID_NEUTRALS = ['stone', 'ice', 'earth', 'sand', 'fire'];
50
+ const VALID_NEUTRALS = NEUTRAL_PALETTES.map((p) => p.name);
50
51
  const VALID_DENSITIES = ['compact', 'default', 'relaxed'];
51
52
  const VALID_RADII = ['sharp', 'subtle', 'default', 'rounded', 'pill'];
52
53