@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
@@ -327,7 +327,34 @@ export async function initCloud(options: InitCloudOptions = {}): Promise<void> {
327
327
  console.log(pc.green(` ✓ Created ${BRAND.configFile}`));
328
328
  }
329
329
 
330
- // ── 6. Run first check ────────────────────────────────────────────
330
+ // ── 6. Push contracts if available ──────────────────────────────────
331
+ const fragmentsJsonPath = resolve('fragments.json');
332
+ if (existsSync(fragmentsJsonPath)) {
333
+ console.log(pc.dim('\n Found fragments.json — pushing contracts to Cloud...'));
334
+ try {
335
+ const { pushContracts } = await import('@fragments-sdk/govern');
336
+ const raw = readFileSync(fragmentsJsonPath, 'utf-8');
337
+ const parsed = JSON.parse(raw);
338
+ if (parsed.fragments && Array.isArray(parsed.fragments)) {
339
+ const result = await pushContracts({
340
+ contractRegistry: JSON.stringify({ fragments: parsed.fragments }),
341
+ apiKey: auth.apiKey,
342
+ url: cloudUrl,
343
+ });
344
+ if (result.ok) {
345
+ console.log(pc.green(` ✓ Pushed ${parsed.fragments.length} component contracts`));
346
+ } else {
347
+ console.log(pc.yellow(` ⚠ Contract push failed: ${result.error}`));
348
+ }
349
+ }
350
+ } catch {
351
+ console.log(pc.yellow(' ⚠ Could not push contracts'));
352
+ }
353
+ } else {
354
+ console.log(pc.dim('\n No fragments.json found — run `fragments scan` or `fragments build` to generate contracts'));
355
+ }
356
+
357
+ // ── 7. Run first check ────────────────────────────────────────────
331
358
  if (!options.skipCheck) {
332
359
  console.log(pc.dim('\n Running first governance check...\n'));
333
360
  try {
@@ -346,9 +373,10 @@ export async function initCloud(options: InitCloudOptions = {}): Promise<void> {
346
373
  }
347
374
  }
348
375
 
349
- // ── 7. Done ───────────────────────────────────────────────────────
376
+ // ── 8. Done ───────────────────────────────────────────────────────
350
377
  console.log(pc.green('\n ✓ All set!') + ' Your project is connected to Fragments Cloud.\n');
351
378
  console.log(pc.dim(` Dashboard: ${cloudUrl}`));
352
- console.log(pc.dim(' Run checks: fragments govern check --cloud'));
353
- console.log(pc.dim(' View config: fragments.config.ts\n'));
379
+ console.log(pc.dim(' Run checks: fragments govern scan'));
380
+ console.log(pc.dim(' Push contracts: fragments govern push-contracts'));
381
+ console.log(pc.dim(' View config: fragments.config.ts\n'));
354
382
  }
@@ -20,6 +20,49 @@ import {
20
20
  addTranspilePackages,
21
21
  } from "./setup.js";
22
22
 
23
+ /**
24
+ * Detect existing UI component libraries by reading package.json.
25
+ * Returns the library display name or null if none found.
26
+ */
27
+ async function detectExistingUILibrary(projectRoot: string): Promise<string | null> {
28
+ let pkg: Record<string, unknown>;
29
+ try {
30
+ const raw = await readFile(join(projectRoot, "package.json"), "utf-8");
31
+ pkg = JSON.parse(raw);
32
+ } catch {
33
+ return null;
34
+ }
35
+
36
+ const allDeps: Record<string, string> = {
37
+ ...(pkg.dependencies as Record<string, string> || {}),
38
+ ...(pkg.devDependencies as Record<string, string> || {}),
39
+ };
40
+
41
+ // shadcn/ui: tailwindcss present + components/ui/ directory
42
+ if (allDeps["tailwindcss"]) {
43
+ const shadcnFiles = await fg(["**/components/ui/*.tsx", "**/components/ui/*.ts"], {
44
+ cwd: projectRoot,
45
+ ignore: ["**/node_modules/**"],
46
+ });
47
+ if (shadcnFiles.length > 0) {
48
+ return "shadcn/ui";
49
+ }
50
+ }
51
+
52
+ if (allDeps["@mui/material"]) return "Material UI";
53
+ if (allDeps["@chakra-ui/react"]) return "Chakra UI";
54
+ if (allDeps["@mantine/core"]) return "Mantine";
55
+ if (allDeps["antd"]) return "Ant Design";
56
+
57
+ // Radix UI: has @radix-ui/react-* packages but NOT @fragments-sdk/ui
58
+ const hasRadix = Object.keys(allDeps).some((dep) => dep.startsWith("@radix-ui/react-"));
59
+ if (hasRadix && !allDeps["@fragments-sdk/ui"]) {
60
+ return "Radix UI";
61
+ }
62
+
63
+ return null;
64
+ }
65
+
23
66
  export interface InitOptions {
24
67
  /** Project root directory */
25
68
  projectRoot?: string;
@@ -43,6 +86,10 @@ export interface InitOptions {
43
86
  apiKey?: string;
44
87
  /** Override AI model for enrichment */
45
88
  model?: string;
89
+ /** Generate metadata/governance files without injecting runtime UI */
90
+ metadataOnly?: boolean;
91
+ /** Alias for metadataOnly */
92
+ govern?: boolean;
46
93
  }
47
94
 
48
95
  export interface InitResult {
@@ -342,6 +389,49 @@ export default defineFragment({
342
389
  `;
343
390
  }
344
391
 
392
+ function generateExampleContract(): string {
393
+ return JSON.stringify({
394
+ $schema: 'https://usefragments.com/schemas/contract.v1.json',
395
+ name: 'Button',
396
+ description: 'Interactive button for triggering actions',
397
+ category: 'Actions',
398
+ status: 'stable',
399
+ sourcePath: 'src/components/Button.tsx',
400
+ exportName: 'Button',
401
+ propsSummary: [
402
+ 'variant: primary|secondary|ghost (default: primary)',
403
+ 'size: sm|md|lg (default: md)',
404
+ 'children: node (required)',
405
+ ],
406
+ props: {
407
+ children: { type: 'node', required: true, description: 'Button label content' },
408
+ variant: {
409
+ type: 'enum',
410
+ values: ['primary', 'secondary', 'ghost'],
411
+ default: 'primary',
412
+ description: 'Visual style variant',
413
+ },
414
+ size: {
415
+ type: 'enum',
416
+ values: ['sm', 'md', 'lg'],
417
+ default: 'md',
418
+ description: 'Button size',
419
+ },
420
+ },
421
+ usage: {
422
+ when: ['Triggering an action (save, submit, delete)', 'Form submission', 'Opening dialogs or menus'],
423
+ whenNot: ['Simple navigation (use Link)', 'Toggling state (use Switch)'],
424
+ guidelines: ['Use Primary for the main action in a context', 'Only one Primary button per section'],
425
+ },
426
+ examples: [
427
+ { name: 'Primary', description: 'Default action button', code: '<Button variant="primary">Save Changes</Button>' },
428
+ { name: 'Secondary', description: 'Less prominent action', code: '<Button variant="secondary">Cancel</Button>' },
429
+ { name: 'Ghost', description: 'Minimal visual weight', code: '<Button variant="ghost">Learn More</Button>' },
430
+ ],
431
+ provenance: { source: 'manual', verified: false },
432
+ }, null, 2) + '\n';
433
+ }
434
+
345
435
  /**
346
436
  * Start the dev server
347
437
  */
@@ -539,7 +629,7 @@ export async function init(options: InitOptions = {}): Promise<InitResult> {
539
629
  const relScanPath = relative(projectRoot, scanPath);
540
630
  const configPath = join(projectRoot, BRAND.configFile);
541
631
  const configContent = generateConfig({
542
- includePaths: [`${relScanPath}/**/*.fragment.tsx`],
632
+ includePaths: [`${relScanPath}/**/*.contract.json`],
543
633
  componentPaths: [`${relScanPath}/**/*.tsx`],
544
634
  framework: "react",
545
635
  });
@@ -642,7 +732,7 @@ export async function init(options: InitOptions = {}): Promise<InitResult> {
642
732
 
643
733
  // Step 5: Create configuration file
644
734
  const includePaths: string[] = [
645
- `${componentPath}/**/*.fragment.tsx`,
735
+ `${componentPath}/**/*.contract.json`,
646
736
  ];
647
737
 
648
738
  if (scenario === 'stories') {
@@ -667,33 +757,62 @@ export async function init(options: InitOptions = {}): Promise<InitResult> {
667
757
  }
668
758
 
669
759
  // Step 6: Auto-inject styles + framework config
670
- const entryFile = await findEntryFile(projectRoot, framework);
760
+ // Detect existing UI libraries — skip @fragments-sdk/ui runtime if one is found
761
+ const detectedUILib = await detectExistingUILibrary(projectRoot);
671
762
 
672
- if (entryFile) {
673
- try {
674
- const stylesResult = await addStylesImport(projectRoot, entryFile);
675
- if (stylesResult.modified) {
676
- console.log(pc.green(` ✓ Added styles import to ${entryFile}`));
677
- } else {
678
- console.log(pc.dim(` · ${stylesResult.message}`));
679
- }
680
- } catch (e) {
681
- errors.push(`Failed to add styles import: ${e instanceof Error ? e.message : e}`);
763
+ // Check if @fragments-sdk/ui is already a dependency
764
+ let hasFragmentsUI = false;
765
+ try {
766
+ const pkgRaw = await readFile(join(projectRoot, "package.json"), "utf-8");
767
+ const pkgJson = JSON.parse(pkgRaw);
768
+ const allDeps = {
769
+ ...(pkgJson.dependencies || {}),
770
+ ...(pkgJson.devDependencies || {}),
771
+ };
772
+ hasFragmentsUI = !!allDeps["@fragments-sdk/ui"];
773
+ } catch {
774
+ // no package.json — can't determine, default to injecting
775
+ }
776
+
777
+ const explicitMetadataOnly = !!options.metadataOnly || !!options.govern;
778
+ const skipUIRuntime = explicitMetadataOnly || (detectedUILib !== null && !hasFragmentsUI);
779
+
780
+ if (skipUIRuntime) {
781
+ if (explicitMetadataOnly) {
782
+ console.log(pc.dim(` · Metadata-only mode — skipping @fragments-sdk/ui runtime setup`));
783
+ } else {
784
+ console.log(pc.dim(` · Detected ${detectedUILib} — skipping @fragments-sdk/ui runtime setup`));
682
785
  }
786
+ console.log(pc.dim(` · Run '${BRAND.cliCommand} setup' if you want to add @fragments-sdk/ui later`));
787
+ } else {
788
+ const entryFile = await findEntryFile(projectRoot, framework);
789
+
790
+ if (entryFile) {
791
+ try {
792
+ const stylesResult = await addStylesImport(projectRoot, entryFile);
793
+ if (stylesResult.modified) {
794
+ console.log(pc.green(` ✓ Added styles import to ${entryFile}`));
795
+ } else {
796
+ console.log(pc.dim(` · ${stylesResult.message}`));
797
+ }
798
+ } catch (e) {
799
+ errors.push(`Failed to add styles import: ${e instanceof Error ? e.message : e}`);
800
+ }
683
801
 
684
- try {
685
- const providerResult = await addThemeProvider(projectRoot, entryFile, framework);
686
- if (providerResult.modified) {
687
- console.log(pc.green(` ✓ Added ThemeProvider to ${entryFile}`));
688
- } else {
689
- console.log(pc.dim(` · ${providerResult.message}`));
802
+ try {
803
+ const providerResult = await addThemeProvider(projectRoot, entryFile, framework);
804
+ if (providerResult.modified) {
805
+ console.log(pc.green(` ✓ Added ThemeProvider to ${entryFile}`));
806
+ } else {
807
+ console.log(pc.dim(` · ${providerResult.message}`));
808
+ }
809
+ } catch (e) {
810
+ errors.push(`Failed to add ThemeProvider: ${e instanceof Error ? e.message : e}`);
690
811
  }
691
- } catch (e) {
692
- errors.push(`Failed to add ThemeProvider: ${e instanceof Error ? e.message : e}`);
812
+ } else {
813
+ console.log(pc.yellow(` ! Could not detect entry file add styles import manually`));
814
+ console.log(pc.dim(` import '@fragments-sdk/ui/styles'`));
693
815
  }
694
- } else {
695
- console.log(pc.yellow(` ! Could not detect entry file — add styles import manually`));
696
- console.log(pc.dim(` import '@fragments-sdk/ui/styles'`));
697
816
  }
698
817
 
699
818
  // Next.js: add transpilePackages
@@ -727,13 +846,13 @@ export async function init(options: InitOptions = {}): Promise<InitResult> {
727
846
  );
728
847
 
729
848
  await writeFile(
730
- join(exampleDir, "Button.fragment.tsx"),
731
- generateExampleFragment(),
849
+ join(exampleDir, "Button.contract.json"),
850
+ generateExampleContract(),
732
851
  "utf-8"
733
852
  );
734
853
  console.log(
735
854
  pc.green(
736
- ` ✓ Created ${relative(projectRoot, join(exampleDir, "Button.fragment.tsx"))}`
855
+ ` ✓ Created ${relative(projectRoot, join(exampleDir, "Button.contract.json"))}`
737
856
  )
738
857
  );
739
858
  } catch (e) {
@@ -775,9 +894,14 @@ export async function init(options: InitOptions = {}): Promise<InitResult> {
775
894
 
776
895
  if (startServer) {
777
896
  startDevServer(projectRoot);
897
+ } else if (skipUIRuntime) {
898
+ console.log(` ${pc.bold("Get started:")}`);
899
+ console.log(` ${pc.dim("$")} ${BRAND.cliCommand} build`);
900
+ console.log(` ${pc.dim("$")} ${BRAND.cliCommand} verify --ci`);
901
+ console.log();
778
902
  } else {
779
903
  console.log(` ${pc.bold("Get started:")}`);
780
- console.log(` ${pc.dim("$")} ${BRAND.cliCommand} dev`);
904
+ console.log(` ${pc.dim("$")} ${BRAND.cliCommand} build`);
781
905
  console.log();
782
906
 
783
907
  if (!options.configure) {
@@ -0,0 +1,290 @@
1
+ /**
2
+ * `fragments inspect` — inspect a single component from fragments.json.
3
+ *
4
+ * Loads the compiled output, finds the named component, and prints
5
+ * a human-readable summary or JSON depending on flags. Supports field
6
+ * filtering (dot notation), variant filtering, and verbosity levels.
7
+ */
8
+
9
+ import pc from 'picocolors';
10
+ import { readFile } from 'node:fs/promises';
11
+ import { resolve } from 'node:path';
12
+ import type { CompiledFragmentsFile } from '../core/index.js';
13
+ import { BRAND } from '../core/index.js';
14
+ import { loadConfig } from '../core/node.js';
15
+ import { projectFields } from '../shared/project-fields.js';
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Types
19
+ // ---------------------------------------------------------------------------
20
+
21
+ export interface InspectCommandOptions {
22
+ config?: string;
23
+ fields?: string;
24
+ variant?: string;
25
+ verbosity?: 'compact' | 'standard' | 'full';
26
+ maxExamples?: number;
27
+ json?: boolean;
28
+ }
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Helpers
32
+ // ---------------------------------------------------------------------------
33
+
34
+ /**
35
+ * Simple Levenshtein distance for "did you mean?" suggestions.
36
+ */
37
+ function levenshtein(a: string, b: string): number {
38
+ const m = a.length;
39
+ const n = b.length;
40
+ const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0) as number[]);
41
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
42
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
43
+ for (let i = 1; i <= m; i++) {
44
+ for (let j = 1; j <= n; j++) {
45
+ dp[i][j] = a[i - 1] === b[j - 1]
46
+ ? dp[i - 1][j - 1]
47
+ : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
48
+ }
49
+ }
50
+ return dp[m][n];
51
+ }
52
+
53
+ function findClosestMatch(input: string, candidates: string[], maxDistance = 3): string | null {
54
+ const inputLower = input.toLowerCase();
55
+ let bestMatch: string | null = null;
56
+ let bestDist = maxDistance + 1;
57
+
58
+ for (const candidate of candidates) {
59
+ const dist = levenshtein(inputLower, candidate.toLowerCase());
60
+ if (dist < bestDist) {
61
+ bestDist = dist;
62
+ bestMatch = candidate;
63
+ } else if (dist === bestDist && bestMatch) {
64
+ if (Math.abs(candidate.length - input.length) < Math.abs(bestMatch.length - input.length)) {
65
+ bestMatch = candidate;
66
+ }
67
+ }
68
+ }
69
+
70
+ return bestDist <= maxDistance ? bestMatch : null;
71
+ }
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // Command implementation
75
+ // ---------------------------------------------------------------------------
76
+
77
+ export async function inspect(
78
+ component: string,
79
+ options: InspectCommandOptions,
80
+ ): Promise<void> {
81
+ const { config, configDir } = await loadConfig(options.config);
82
+ const outputPath = resolve(configDir, config.outFile ?? BRAND.outFile);
83
+
84
+ let data: CompiledFragmentsFile;
85
+ try {
86
+ const content = await readFile(outputPath, 'utf-8');
87
+ data = JSON.parse(content) as CompiledFragmentsFile;
88
+ } catch {
89
+ console.error(
90
+ pc.red(`Error: Could not load ${BRAND.outFile}. Run \`${BRAND.cliCommand} build\` first.`),
91
+ );
92
+ process.exit(1);
93
+ }
94
+
95
+ // Find fragment (case-insensitive)
96
+ const fragment = Object.values(data.fragments).find(
97
+ (s) => s.meta.name.toLowerCase() === component.toLowerCase(),
98
+ );
99
+
100
+ if (!fragment) {
101
+ const allNames = Object.values(data.fragments).map((s) => s.meta.name);
102
+ const closest = findClosestMatch(component, allNames);
103
+ const suggestion = closest
104
+ ? ` Did you mean "${closest}"?`
105
+ : '';
106
+ console.error(
107
+ pc.red(`Error: Component "${component}" not found.${suggestion}`),
108
+ );
109
+ console.error(
110
+ pc.dim(`Use \`${BRAND.cliCommand} discover\` to see available components.`),
111
+ );
112
+ process.exit(1);
113
+ }
114
+
115
+ const verbosity = options.verbosity ?? 'standard';
116
+
117
+ // --- Variant filtering ---
118
+ let variants = fragment.variants;
119
+ if (options.variant) {
120
+ const query = options.variant.toLowerCase();
121
+ let filtered = variants.filter((v) => v.name.toLowerCase() === query);
122
+ if (filtered.length === 0) {
123
+ filtered = variants.filter((v) => v.name.toLowerCase().startsWith(query));
124
+ }
125
+ if (filtered.length === 0) {
126
+ filtered = variants.filter((v) => v.name.toLowerCase().includes(query));
127
+ }
128
+ if (filtered.length > 0) {
129
+ variants = filtered;
130
+ } else {
131
+ console.error(
132
+ pc.red(
133
+ `Error: Variant "${options.variant}" not found for ${component}. ` +
134
+ `Available: ${fragment.variants.map((v) => v.name).join(', ')}`,
135
+ ),
136
+ );
137
+ process.exit(1);
138
+ }
139
+ }
140
+ if (options.maxExamples && options.maxExamples > 0) {
141
+ variants = variants.slice(0, options.maxExamples);
142
+ }
143
+
144
+ // --- Build full result ---
145
+ const propsReference = Object.entries(fragment.props ?? {}).map(([propName, prop]) => ({
146
+ name: propName,
147
+ type: prop.type,
148
+ required: prop.required,
149
+ default: prop.default,
150
+ description: prop.description,
151
+ }));
152
+
153
+ const propConstraints = Object.entries(fragment.props ?? {})
154
+ .filter(([, prop]) => prop.constraints && prop.constraints.length > 0)
155
+ .map(([pName, prop]) => ({
156
+ prop: pName,
157
+ constraints: prop.constraints,
158
+ }));
159
+
160
+ const examples = variants.map((variant) => ({
161
+ variant: variant.name,
162
+ description: variant.description,
163
+ code: variant.code ?? `<${fragment.meta.name} />`,
164
+ }));
165
+
166
+ const fullResult = {
167
+ meta: fragment.meta,
168
+ props: fragment.props,
169
+ variants: fragment.variants,
170
+ relations: fragment.relations,
171
+ contract: fragment.contract,
172
+ generated: fragment._generated,
173
+ guidelines: {
174
+ when: fragment.usage?.when ?? [],
175
+ whenNot: fragment.usage?.whenNot ?? [],
176
+ guidelines: fragment.usage?.guidelines ?? [],
177
+ accessibility: fragment.usage?.accessibility ?? [],
178
+ propConstraints,
179
+ alternatives: fragment.relations
180
+ ?.filter((r) => r.relationship === 'alternative')
181
+ .map((r) => ({ component: r.component, note: r.note })) ?? [],
182
+ },
183
+ examples: {
184
+ import: `import { ${fragment.meta.name} } from '${data.packageName ?? BRAND.nameLower}';`,
185
+ code: examples,
186
+ propsReference,
187
+ },
188
+ };
189
+
190
+ // --- Apply verbosity + field filtering ---
191
+ const fieldsArray = options.fields
192
+ ? options.fields.split(',').map((f) => f.trim())
193
+ : undefined;
194
+
195
+ // Alias legacy field paths
196
+ const aliasMap: Record<string, string> = { usage: 'guidelines' };
197
+ const resolvedFields = fieldsArray?.map((f) => {
198
+ const parts = f.split('.');
199
+ if (aliasMap[parts[0]]) parts[0] = aliasMap[parts[0]];
200
+ return parts.join('.');
201
+ });
202
+
203
+ let result: unknown;
204
+ if (verbosity === 'compact' && !resolvedFields?.length) {
205
+ result = {
206
+ meta: fullResult.meta,
207
+ propNames: Object.keys(fragment.props ?? {}),
208
+ variantNames: fragment.variants.map((v) => v.name),
209
+ };
210
+ } else {
211
+ result = resolvedFields && resolvedFields.length > 0
212
+ ? projectFields(fullResult as unknown as Record<string, unknown>, resolvedFields)
213
+ : fullResult;
214
+ }
215
+
216
+ // --- Output ---
217
+ if (options.json) {
218
+ console.log(JSON.stringify(result, null, 2));
219
+ return;
220
+ }
221
+
222
+ // Human-readable output
223
+ const meta = fragment.meta;
224
+ console.log(pc.bold(`\n${meta.name}\n`));
225
+ console.log(` ${pc.cyan('Category:')} ${meta.category}`);
226
+ console.log(` ${pc.cyan('Status:')} ${meta.status ?? 'stable'}`);
227
+ if (meta.description) {
228
+ console.log(` ${pc.cyan('Description:')} ${meta.description}`);
229
+ }
230
+ if (meta.tags && meta.tags.length > 0) {
231
+ console.log(` ${pc.cyan('Tags:')} ${meta.tags.join(', ')}`);
232
+ }
233
+
234
+ // Props table
235
+ const propEntries = Object.entries(fragment.props ?? {});
236
+ if (propEntries.length > 0) {
237
+ console.log(pc.bold('\n Props\n'));
238
+ console.log(
239
+ pc.dim(
240
+ ` ${'Name'.padEnd(24)} ${'Type'.padEnd(20)} ${'Required'.padEnd(10)} ${'Default'}`,
241
+ ),
242
+ );
243
+ console.log(pc.dim(` ${'─'.repeat(70)}`));
244
+
245
+ const displayProps = verbosity === 'compact' ? propEntries.slice(0, 5) : propEntries;
246
+ for (const [name, prop] of displayProps) {
247
+ const propName = name.length > 22 ? name.slice(0, 19) + '...' : name;
248
+ const propType = (prop.type ?? '').length > 18
249
+ ? (prop.type ?? '').slice(0, 15) + '...'
250
+ : (prop.type ?? '');
251
+ console.log(
252
+ ` ${pc.cyan(propName.padEnd(24))} ${propType.padEnd(20)} ${(prop.required ? 'yes' : 'no').padEnd(10)} ${pc.dim(String(prop.default ?? '-'))}`,
253
+ );
254
+ }
255
+ if (verbosity === 'compact' && propEntries.length > 5) {
256
+ console.log(pc.dim(` ... and ${propEntries.length - 5} more`));
257
+ }
258
+ }
259
+
260
+ // Variants list
261
+ if (variants.length > 0) {
262
+ console.log(pc.bold('\n Variants\n'));
263
+ for (const v of variants) {
264
+ console.log(` ${pc.yellow(v.name)}${v.description ? pc.dim(` — ${v.description}`) : ''}`);
265
+ }
266
+ }
267
+
268
+ // Usage guidelines (standard/full only)
269
+ if (verbosity !== 'compact') {
270
+ const when = fragment.usage?.when ?? [];
271
+ const whenNot = fragment.usage?.whenNot ?? [];
272
+ if (when.length > 0 || whenNot.length > 0) {
273
+ console.log(pc.bold('\n Usage Guidelines\n'));
274
+ if (when.length > 0) {
275
+ console.log(` ${pc.green('When to use:')}`);
276
+ for (const w of when) {
277
+ console.log(` ${pc.dim('•')} ${w}`);
278
+ }
279
+ }
280
+ if (whenNot.length > 0) {
281
+ console.log(` ${pc.red('When NOT to use:')}`);
282
+ for (const w of whenNot) {
283
+ console.log(` ${pc.dim('•')} ${w}`);
284
+ }
285
+ }
286
+ }
287
+ }
288
+
289
+ console.log();
290
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * CLI command: fragments migrate-contract
3
+ *
4
+ * Converts .fragment.tsx files to .contract.json format.
5
+ */
6
+
7
+ import pc from 'picocolors';
8
+ import fg from 'fast-glob';
9
+ import { resolve } from 'node:path';
10
+ import { loadConfig } from '../core/node.js';
11
+ import { migrateFragmentToContract } from '../migrate/fragment-to-contract.js';
12
+
13
+ export interface MigrateContractOptions {
14
+ config?: string;
15
+ glob?: string;
16
+ dryRun?: boolean;
17
+ tsconfig?: string;
18
+ }
19
+
20
+ export async function migrateContract(options: MigrateContractOptions): Promise<{
21
+ migrated: number;
22
+ failed: number;
23
+ warnings: number;
24
+ }> {
25
+ const { config, configDir } = await loadConfig(options.config);
26
+ const pattern = options.glob ?? 'src/**/*.fragment.tsx';
27
+
28
+ console.log(pc.blue(`Migrating fragment files matching: ${pattern}`));
29
+ if (options.dryRun) {
30
+ console.log(pc.yellow('(dry run — no files will be written)'));
31
+ }
32
+
33
+ const files = await fg(pattern, { cwd: configDir, absolute: true });
34
+
35
+ if (files.length === 0) {
36
+ console.log(pc.yellow('No fragment files found matching pattern.'));
37
+ return { migrated: 0, failed: 0, warnings: 0 };
38
+ }
39
+
40
+ console.log(pc.dim(`Found ${files.length} fragment file(s)`));
41
+
42
+ let migrated = 0;
43
+ let failed = 0;
44
+ let totalWarnings = 0;
45
+
46
+ // Find tsconfig
47
+ const tsconfigPath = options.tsconfig
48
+ ? resolve(options.tsconfig)
49
+ : undefined;
50
+
51
+ for (const file of files) {
52
+ try {
53
+ const result = await migrateFragmentToContract(file, configDir, {
54
+ dryRun: options.dryRun,
55
+ tsconfigPath,
56
+ });
57
+
58
+ migrated++;
59
+ const verb = options.dryRun ? 'Would migrate' : 'Migrated';
60
+ console.log(pc.green(` ✓ ${verb}: ${result.contractPath}`));
61
+
62
+ if (result.warnings.length > 0) {
63
+ totalWarnings += result.warnings.length;
64
+ for (const w of result.warnings) {
65
+ console.log(pc.yellow(` ⚠ ${w}`));
66
+ }
67
+ }
68
+ } catch (error) {
69
+ failed++;
70
+ console.log(pc.red(` ✗ Failed: ${file}`));
71
+ console.log(pc.dim(` ${error instanceof Error ? error.message : String(error)}`));
72
+ }
73
+ }
74
+
75
+ console.log('');
76
+ console.log(pc.bold('Migration summary:'));
77
+ console.log(` ${pc.green(`${migrated} migrated`)} ${pc.red(`${failed} failed`)} ${pc.yellow(`${totalWarnings} warnings`)}`);
78
+
79
+ if (!options.dryRun && migrated > 0) {
80
+ console.log('');
81
+ console.log(pc.dim('Run `fragments build` to verify migrated contracts compile correctly.'));
82
+ }
83
+
84
+ return { migrated, failed, warnings: totalWarnings };
85
+ }