@fragments-sdk/cli 0.14.3 → 0.15.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 (135) hide show
  1. package/README.md +0 -3
  2. package/dist/bin.js +4290 -3754
  3. package/dist/bin.js.map +1 -1
  4. package/dist/{chunk-TXFCEDOC.js → chunk-2WXKALIG.js} +2 -2
  5. package/dist/{chunk-I34BC3CU.js → chunk-32LIWN2P.js} +1006 -3
  6. package/dist/chunk-32LIWN2P.js.map +1 -0
  7. package/dist/{chunk-55KERLWL.js → chunk-65WSVDV5.js} +314 -89
  8. package/dist/chunk-65WSVDV5.js.map +1 -0
  9. package/dist/chunk-7DZC4YEV.js +294 -0
  10. package/dist/chunk-7DZC4YEV.js.map +1 -0
  11. package/dist/{chunk-LOYS64QS.js → chunk-7WHVW72L.js} +230 -19
  12. package/dist/chunk-7WHVW72L.js.map +1 -0
  13. package/dist/{chunk-PJT5IZ37.js → chunk-BJE3425I.js} +19 -52
  14. package/dist/{chunk-PJT5IZ37.js.map → chunk-BJE3425I.js.map} +1 -1
  15. package/dist/{chunk-5A6X2Y73.js → chunk-CZD3AD4Q.js} +12 -11
  16. package/dist/chunk-CZD3AD4Q.js.map +1 -0
  17. package/dist/{chunk-EYXVAMEX.js → chunk-MN3TJ3D5.js} +72 -3
  18. package/dist/chunk-MN3TJ3D5.js.map +1 -0
  19. package/dist/chunk-QCN35LJU.js +630 -0
  20. package/dist/chunk-QCN35LJU.js.map +1 -0
  21. package/dist/chunk-T47OLCSF.js +36 -0
  22. package/dist/chunk-T47OLCSF.js.map +1 -0
  23. package/dist/{chunk-APTQIBS5.js → chunk-XJQ5BIWI.js} +144 -1049
  24. package/dist/chunk-XJQ5BIWI.js.map +1 -0
  25. package/dist/codebase-scanner-VOTPXRYW.js +22 -0
  26. package/dist/converter-JLINP7CJ.js +34 -0
  27. package/dist/converter-JLINP7CJ.js.map +1 -0
  28. package/dist/core/index.js +43 -1
  29. package/dist/{generate-RYWIPDN2.js → generate-A4FP5426.js} +3 -4
  30. package/dist/{generate-RYWIPDN2.js.map → generate-A4FP5426.js.map} +1 -1
  31. package/dist/govern-scan-UCBZR6D6.js +280 -0
  32. package/dist/govern-scan-UCBZR6D6.js.map +1 -0
  33. package/dist/index.d.ts +2 -1
  34. package/dist/index.js +11 -11
  35. package/dist/{init-WRUSW7R5.js → init-HGSM35XA.js} +131 -128
  36. package/dist/init-HGSM35XA.js.map +1 -0
  37. package/dist/{init-cloud-REQ3XLHO.js → init-cloud-MQ6GRJAZ.js} +2 -2
  38. package/dist/mcp-bin.js +5 -36
  39. package/dist/mcp-bin.js.map +1 -1
  40. package/dist/scan-VNNKACG2.js +15 -0
  41. package/dist/{scan-generate-TFZVL3BT.js → scan-generate-TWRHNU5M.js} +335 -46
  42. package/dist/scan-generate-TWRHNU5M.js.map +1 -0
  43. package/dist/scanner-7LAZYPWZ.js +13 -0
  44. package/dist/{service-HKJ6B7P7.js → service-FHQU7YS7.js} +27 -23
  45. package/dist/{snapshot-C5DYIGIV.js → snapshot-KQEQ6XHL.js} +2 -2
  46. package/dist/{static-viewer-DUVC4UIM.js → static-viewer-63PG6FWY.js} +3 -3
  47. package/dist/static-viewer-63PG6FWY.js.map +1 -0
  48. package/dist/{test-JW7JIDFG.js → test-UQYUCZIS.js} +4 -6
  49. package/dist/{test-JW7JIDFG.js.map → test-UQYUCZIS.js.map} +1 -1
  50. package/dist/{tokens-KE73G5JC.js → tokens-6GYKDV6U.js} +6 -5
  51. package/dist/{tokens-KE73G5JC.js.map → tokens-6GYKDV6U.js.map} +1 -1
  52. package/dist/tokens-generate-VTZV5EEW.js +86 -0
  53. package/dist/tokens-generate-VTZV5EEW.js.map +1 -0
  54. package/package.json +6 -6
  55. package/src/bin.ts +210 -48
  56. package/src/build.ts +130 -6
  57. package/src/commands/__fixtures__/shadcn-label-wrapper/package.json +7 -0
  58. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.contract.json +42 -0
  59. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.tsx +11 -0
  60. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.contract.json +20 -0
  61. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.tsx +14 -0
  62. package/src/commands/__fixtures__/shadcn-label-wrapper/tsconfig.app.json +23 -0
  63. package/src/commands/__tests__/init.test.ts +113 -0
  64. package/src/commands/__tests__/scan-generate.test.ts +188 -69
  65. package/src/commands/__tests__/verify.test.ts +91 -0
  66. package/src/commands/discover.ts +151 -0
  67. package/src/commands/enhance.ts +3 -1
  68. package/src/commands/govern-scan.ts +386 -0
  69. package/src/commands/govern.ts +2 -2
  70. package/src/commands/init.ts +152 -28
  71. package/src/commands/inspect.ts +290 -0
  72. package/src/commands/migrate-contract.ts +85 -0
  73. package/src/commands/scan-generate.ts +438 -50
  74. package/src/commands/scan.ts +1 -0
  75. package/src/commands/setup.ts +27 -50
  76. package/src/commands/tokens-generate.ts +113 -0
  77. package/src/commands/verify.ts +195 -1
  78. package/src/core/__fixtures__/shadcn-input/input.tsx +7 -0
  79. package/src/core/__fixtures__/shadcn-input/tsconfig.json +14 -0
  80. package/src/core/__fixtures__/shadcn-label/label.tsx +11 -0
  81. package/src/core/__fixtures__/shadcn-label/primitive.tsx +14 -0
  82. package/src/core/__fixtures__/shadcn-label/tsconfig.json +14 -0
  83. package/src/core/__fixtures__/shadcn-radix-label/label.tsx +11 -0
  84. package/src/core/__fixtures__/shadcn-radix-label/node_modules/radix-ui/index.d.ts +12 -0
  85. package/src/core/__fixtures__/shadcn-radix-label/tsconfig.json +14 -0
  86. package/src/core/__tests__/contract-parity.test.ts +316 -0
  87. package/src/core/component-extractor.test.ts +39 -0
  88. package/src/core/component-extractor.ts +92 -1
  89. package/src/core/config.ts +2 -1
  90. package/src/core/discovery.ts +13 -2
  91. package/src/core/drift-verifier.ts +123 -0
  92. package/src/core/extractor-adapter.ts +80 -0
  93. package/src/mcp/__tests__/projectFields.test.ts +1 -1
  94. package/src/mcp/utils.ts +1 -50
  95. package/src/migrate/converter.ts +3 -3
  96. package/src/migrate/fragment-to-contract.ts +253 -0
  97. package/src/migrate/report.ts +1 -1
  98. package/src/scripts/token-benchmark.ts +121 -0
  99. package/src/service/__tests__/props-extractor.test.ts +94 -0
  100. package/src/service/__tests__/token-normalizer.test.ts +690 -0
  101. package/src/service/ast-utils.ts +4 -23
  102. package/src/service/babel-config.ts +23 -0
  103. package/src/service/enhance/converter.ts +61 -0
  104. package/src/service/enhance/props-extractor.ts +25 -8
  105. package/src/service/enhance/scanner.ts +5 -24
  106. package/src/service/snippet-validation.ts +9 -3
  107. package/src/service/token-normalizer.ts +510 -0
  108. package/src/shared/index.ts +1 -0
  109. package/src/shared/project-fields.ts +46 -0
  110. package/src/viewer/__tests__/viewer-integration.test.ts +8 -8
  111. package/src/viewer/preview-adapter.ts +116 -0
  112. package/src/viewer/style-utils.ts +27 -412
  113. package/src/viewer/vite-plugin.ts +2 -2
  114. package/dist/chunk-55KERLWL.js.map +0 -1
  115. package/dist/chunk-5A6X2Y73.js.map +0 -1
  116. package/dist/chunk-APTQIBS5.js.map +0 -1
  117. package/dist/chunk-EYXVAMEX.js.map +0 -1
  118. package/dist/chunk-I34BC3CU.js.map +0 -1
  119. package/dist/chunk-LOYS64QS.js.map +0 -1
  120. package/dist/chunk-ZKTFKHWN.js +0 -324
  121. package/dist/chunk-ZKTFKHWN.js.map +0 -1
  122. package/dist/discovery-VDANZAJ2.js +0 -28
  123. package/dist/init-WRUSW7R5.js.map +0 -1
  124. package/dist/scan-YJHQIRKG.js +0 -14
  125. package/dist/scan-generate-TFZVL3BT.js.map +0 -1
  126. package/dist/viewer-2TZS3NDL.js +0 -2730
  127. package/dist/viewer-2TZS3NDL.js.map +0 -1
  128. package/src/commands/dev.ts +0 -107
  129. /package/dist/{chunk-TXFCEDOC.js.map → chunk-2WXKALIG.js.map} +0 -0
  130. /package/dist/{discovery-VDANZAJ2.js.map → codebase-scanner-VOTPXRYW.js.map} +0 -0
  131. /package/dist/{init-cloud-REQ3XLHO.js.map → init-cloud-MQ6GRJAZ.js.map} +0 -0
  132. /package/dist/{scan-YJHQIRKG.js.map → scan-VNNKACG2.js.map} +0 -0
  133. /package/dist/{service-HKJ6B7P7.js.map → scanner-7LAZYPWZ.js.map} +0 -0
  134. /package/dist/{static-viewer-DUVC4UIM.js.map → service-FHQU7YS7.js.map} +0 -0
  135. /package/dist/{snapshot-C5DYIGIV.js.map → snapshot-KQEQ6XHL.js.map} +0 -0
package/src/bin.ts CHANGED
@@ -17,13 +17,14 @@ import { loadConfig } from './core/node.js';
17
17
  const __dirname = dirname(fileURLToPath(import.meta.url));
18
18
  const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8')) as { version: string };
19
19
 
20
+ const EXPERIMENTAL = process.env.FRAGMENTS_EXPERIMENTAL === '1';
21
+
20
22
  // Import command implementations
21
23
  import { validate } from './commands/validate.js';
22
24
  import { build } from './commands/build.js';
23
25
  import { context } from './commands/context.js';
24
26
  import { list } from './commands/list.js';
25
27
  import { reset } from './commands/reset.js';
26
- import { dev } from './commands/dev.js';
27
28
  import { compare } from './commands/compare.js';
28
29
  import { verify } from './commands/verify.js';
29
30
  import { audit } from './commands/audit.js';
@@ -36,11 +37,14 @@ import { linkFigma, linkStorybook } from './commands/link/index.js';
36
37
  import { enhance } from './commands/enhance.js';
37
38
  import { scan } from './commands/scan.js';
38
39
  import { graph } from './commands/graph.js';
40
+ import { inspect } from './commands/inspect.js';
41
+ import { discover } from './commands/discover.js';
39
42
  import { perf } from './commands/perf.js';
40
43
  import { doctor } from './commands/doctor.js';
41
44
  import { setup } from './commands/setup.js';
42
45
  import { sync } from './commands/sync.js';
43
46
  import { governCheck, governInit, governReport, governConnect } from './commands/govern.js';
47
+ import { migrateContract } from './commands/migrate-contract.js';
44
48
 
45
49
  // Import existing commands that were already extracted
46
50
  import { runScreenshotCommand } from './screenshot.js';
@@ -304,37 +308,6 @@ linkCommand
304
308
  }
305
309
  });
306
310
 
307
- // ============================================================================
308
- // DEV COMMAND
309
- // ============================================================================
310
- program
311
- .command('dev')
312
- .description('Start the development server with live component rendering')
313
- .option('-p, --port <port>', 'Port to run on', '6006')
314
- .option('--no-open', 'Do not open browser')
315
- .option('--skip-setup', 'Skip auto-setup (Storybook import, build, Figma link)')
316
- .option('--skip-storybook', 'Skip auto-importing from Storybook')
317
- .option('--skip-figma', 'Skip Figma link check')
318
- .option('--skip-build', `Skip auto-building ${BRAND.outFile}`)
319
- .action(async (options) => {
320
- try {
321
- await dev({
322
- port: options.port,
323
- open: options.open,
324
- skipSetup: options.skipSetup,
325
- skipStorybook: options.skipStorybook,
326
- skipFigma: options.skipFigma,
327
- skipBuild: options.skipBuild,
328
- });
329
- } catch (error) {
330
- console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
331
- if (error instanceof Error && error.stack) {
332
- console.error(pc.dim(error.stack));
333
- }
334
- process.exit(1);
335
- }
336
- });
337
-
338
311
  // ============================================================================
339
312
  // SCREENSHOT COMMAND
340
313
  // ============================================================================
@@ -433,7 +406,7 @@ program
433
406
  }
434
407
  } catch (error) {
435
408
  console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
436
- console.log(pc.dim(`\nMake sure the dev server is running: ${BRAND.cliCommand} dev`));
409
+ console.log(pc.dim(`\nMake sure a dev server is running on the expected port.`));
437
410
  process.exit(1);
438
411
  }
439
412
  });
@@ -642,6 +615,33 @@ program
642
615
  }
643
616
  });
644
617
 
618
+ // ============================================================================
619
+ // MIGRATE-CONTRACT COMMAND
620
+ // ============================================================================
621
+ program
622
+ .command('migrate-contract')
623
+ .description('Migrate .fragment.tsx files to .contract.json format')
624
+ .option('-c, --config <path>', 'Path to config file')
625
+ .option('--glob <pattern>', 'Glob pattern for fragment files', 'src/**/*.fragment.tsx')
626
+ .option('--dry-run', 'Preview migration without writing files')
627
+ .option('--tsconfig <path>', 'Path to tsconfig.json')
628
+ .action(async (options) => {
629
+ try {
630
+ const result = await migrateContract({
631
+ config: options.config,
632
+ glob: options.glob,
633
+ dryRun: options.dryRun,
634
+ tsconfig: options.tsconfig,
635
+ });
636
+ if (result.failed > 0) {
637
+ process.exit(1);
638
+ }
639
+ } catch (error) {
640
+ console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
641
+ process.exit(1);
642
+ }
643
+ });
644
+
645
645
  // ============================================================================
646
646
  // STORYGEN COMMAND
647
647
  // ============================================================================
@@ -714,7 +714,7 @@ program
714
714
  } catch (error) {
715
715
  console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
716
716
  if (action === 'update') {
717
- console.log(pc.dim(`\nMake sure the dev server is running: ${BRAND.cliCommand} dev`));
717
+ console.log(pc.dim(`\nMake sure a dev server is running on the expected port.`));
718
718
  }
719
719
  process.exit(1);
720
720
  }
@@ -823,27 +823,40 @@ program
823
823
  // ============================================================================
824
824
  // INIT COMMAND
825
825
  // ============================================================================
826
- program
826
+ const initCmd = program
827
827
  .command('init')
828
828
  .description('Initialize fragments in a project (zero-config by default)')
829
829
  .option('--force', 'Overwrite existing config')
830
830
  .option('-y, --yes', 'Non-interactive mode (now the default)')
831
- .option('--cloud', 'Set up Fragments Cloud governance (zero-config browser auth)')
832
- .option('--cloud-url <url>', 'Cloud dashboard URL (default: https://app.usefragments.com)')
833
- .option('--port <port>', 'Localhost port for auth callback (default: 9876)')
834
- .option('--auth-only', 'Only authenticate, skip project setup')
835
- .option('--skip-check', 'Skip running the first governance check')
836
831
  .option('--configure', 'Interactive mode for theme seeds, snapshots, etc.')
832
+ .option('--metadata-only', 'Generate config and metadata without modifying runtime app setup')
833
+ .option('--govern', 'Alias for --metadata-only')
837
834
  .option('--scan <path>', 'Scan a TypeScript component directory and generate fragment files')
838
835
  .option('--enrich', 'Use AI to fill knowledge fields during --scan (requires API key)')
839
836
  .option('--dry-run', 'Show what --enrich would generate without calling API')
840
837
  .option('--provider <provider>', 'AI provider for enrichment: anthropic or openai')
841
838
  .option('--api-key <key>', 'API key for AI enrichment')
842
- .option('--model <model>', 'Override AI model for enrichment')
843
- .action(async (options) => {
839
+ .option('--model <model>', 'Override AI model for enrichment');
840
+
841
+ // Cloud governance flags — only visible with FRAGMENTS_EXPERIMENTAL=1
842
+ if (EXPERIMENTAL) {
843
+ initCmd
844
+ .option('--cloud', 'Set up Fragments Cloud governance (zero-config browser auth)')
845
+ .option('--cloud-url <url>', 'Cloud dashboard URL (default: https://app.usefragments.com)')
846
+ .option('--port <port>', 'Localhost port for auth callback (default: 9876)')
847
+ .option('--auth-only', 'Only authenticate, skip project setup')
848
+ .option('--skip-check', 'Skip running the first governance check');
849
+ }
850
+
851
+ initCmd.action(async (options) => {
844
852
  try {
845
- // Cloud init — separate flow
853
+ // Cloud init — experimental, requires FRAGMENTS_EXPERIMENTAL=1
846
854
  if (options.cloud) {
855
+ if (!EXPERIMENTAL) {
856
+ console.log(pc.yellow(`\n Fragments Cloud is not yet publicly available.`));
857
+ console.log(pc.dim(` Set FRAGMENTS_EXPERIMENTAL=1 to enable preview features.\n`));
858
+ process.exit(1);
859
+ }
847
860
  const { initCloud } = await import('./commands/init-cloud.js');
848
861
  await initCloud({
849
862
  url: options.cloudUrl,
@@ -866,6 +879,8 @@ program
866
879
  provider: options.provider,
867
880
  apiKey: options.apiKey,
868
881
  model: options.model,
882
+ metadataOnly: options.metadataOnly,
883
+ govern: options.govern,
869
884
  });
870
885
 
871
886
  if (!result.success) {
@@ -912,11 +927,15 @@ program
912
927
  });
913
928
 
914
929
  // ============================================================================
915
- // TOKENS COMMAND
930
+ // TOKENS COMMAND GROUP
916
931
  // ============================================================================
917
- program
932
+ const tokensCmd = program
918
933
  .command('tokens')
919
- .description('Discover and list design tokens from CSS/SCSS files')
934
+ .description('Design token discovery, listing, and generation');
935
+
936
+ tokensCmd
937
+ .command('list', { isDefault: true })
938
+ .description('Discover and list design tokens from CSS/SCSS/DTCG files')
920
939
  .option('-c, --config <path>', 'Path to config file')
921
940
  .option('--json', 'Output as JSON')
922
941
  .option('--categories', 'Group tokens by category')
@@ -944,6 +963,32 @@ program
944
963
  }
945
964
  });
946
965
 
966
+ tokensCmd
967
+ .command('generate')
968
+ .description('Generate CSS, SCSS, Tailwind, or Figma output from a DTCG .tokens.json file')
969
+ .requiredOption('--from <path>', 'Path to DTCG .tokens.json source file')
970
+ .requiredOption('--format <formats>', 'Output formats (comma-separated: css, scss, tailwind, figma)')
971
+ .option('--out <dir>', 'Output directory (default: same directory as source)')
972
+ .option('--prefix <prefix>', 'Token name prefix')
973
+ .option('--selector <selector>', 'CSS selector for custom properties (default: :root)')
974
+ .option('--verbose', 'Verbose output')
975
+ .action(async (options) => {
976
+ try {
977
+ const { tokensGenerate } = await import('./commands/tokens-generate.js');
978
+ await tokensGenerate({
979
+ from: options.from,
980
+ format: options.format,
981
+ out: options.out,
982
+ prefix: options.prefix,
983
+ selector: options.selector,
984
+ verbose: options.verbose,
985
+ });
986
+ } catch (error) {
987
+ console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
988
+ process.exit(1);
989
+ }
990
+ });
991
+
947
992
  // ============================================================================
948
993
  // GENERATE COMMAND
949
994
  // ============================================================================
@@ -995,6 +1040,71 @@ program
995
1040
  }
996
1041
  });
997
1042
 
1043
+ // ============================================================================
1044
+ // INSPECT COMMAND
1045
+ // ============================================================================
1046
+ program
1047
+ .command('inspect')
1048
+ .description('Inspect a single component from compiled fragments data')
1049
+ .argument('<component>', 'Component name to inspect')
1050
+ .option('-c, --config <path>', 'Path to config file')
1051
+ .option('--fields <fields>', 'Comma-separated fields to include (dot notation: meta, props, examples)')
1052
+ .option('--variant <name>', 'Filter to a specific variant')
1053
+ .option('--verbosity <level>', 'Output verbosity: compact, standard, full', 'standard')
1054
+ .option('--maxExamples <n>', 'Max variant examples to include', (v) => Number.parseInt(v, 10))
1055
+ .option('--json', 'Output as JSON')
1056
+ .action(async (component, options) => {
1057
+ try {
1058
+ await inspect(component, {
1059
+ config: options.config,
1060
+ fields: options.fields,
1061
+ variant: options.variant,
1062
+ verbosity: options.verbosity,
1063
+ maxExamples: options.maxExamples,
1064
+ json: options.json,
1065
+ });
1066
+ } catch (error) {
1067
+ if (options.json) {
1068
+ console.log(JSON.stringify({ error: error instanceof Error ? error.message : 'Inspect failed' }));
1069
+ } else {
1070
+ console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
1071
+ }
1072
+ process.exit(1);
1073
+ }
1074
+ });
1075
+
1076
+ // ============================================================================
1077
+ // DISCOVER COMMAND
1078
+ // ============================================================================
1079
+ program
1080
+ .command('discover')
1081
+ .description('List and filter components from compiled fragments data')
1082
+ .option('-c, --config <path>', 'Path to config file')
1083
+ .option('--search <term>', 'Search by name, description, or tags')
1084
+ .option('--category <category>', 'Filter by category')
1085
+ .option('--status <status>', 'Filter by status: stable, beta, deprecated, experimental')
1086
+ .option('--compact', 'Output component names only')
1087
+ .option('--json', 'Output as JSON')
1088
+ .action(async (options) => {
1089
+ try {
1090
+ await discover({
1091
+ config: options.config,
1092
+ search: options.search,
1093
+ category: options.category,
1094
+ status: options.status,
1095
+ compact: options.compact,
1096
+ json: options.json,
1097
+ });
1098
+ } catch (error) {
1099
+ if (options.json) {
1100
+ console.log(JSON.stringify({ error: error instanceof Error ? error.message : 'Discover failed' }));
1101
+ } else {
1102
+ console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
1103
+ }
1104
+ process.exit(1);
1105
+ }
1106
+ });
1107
+
998
1108
  // ============================================================================
999
1109
  // PERF COMMAND
1000
1110
  // ============================================================================
@@ -1134,11 +1244,18 @@ program
1134
1244
  });
1135
1245
 
1136
1246
  // ============================================================================
1137
- // GOVERN COMMAND
1247
+ // GOVERN COMMAND (experimental — FRAGMENTS_EXPERIMENTAL=1)
1138
1248
  // ============================================================================
1139
1249
  const governCmd = program
1140
1250
  .command('govern')
1141
- .description('AI UI governance checks');
1251
+ .description(EXPERIMENTAL ? 'AI UI governance checks' : 'AI UI governance checks (preview — set FRAGMENTS_EXPERIMENTAL=1)')
1252
+ .hook('preAction', () => {
1253
+ if (!EXPERIMENTAL) {
1254
+ console.log(pc.yellow(`\n Fragments governance is not yet publicly available.`));
1255
+ console.log(pc.dim(` Set FRAGMENTS_EXPERIMENTAL=1 to enable preview features.\n`));
1256
+ process.exit(1);
1257
+ }
1258
+ });
1142
1259
 
1143
1260
  governCmd
1144
1261
  .command('check')
@@ -1199,5 +1316,50 @@ governCmd
1199
1316
  }
1200
1317
  });
1201
1318
 
1319
+ governCmd
1320
+ .command('scan')
1321
+ .description('Scan JSX/TSX codebase for governance violations')
1322
+ .option('-d, --dir <path>', 'Root directory (default: auto-detect)')
1323
+ .option('-c, --config <path>', 'Path to govern.config.ts')
1324
+ .option('-f, --format <format>', 'Output format: summary, json, sarif', 'summary')
1325
+ .option('-q, --quiet', 'Suppress non-error output')
1326
+ .action(async (options) => {
1327
+ try {
1328
+ const { governScan } = await import('./commands/govern-scan.js');
1329
+ const { exitCode } = await governScan({
1330
+ dir: options.dir,
1331
+ config: options.config,
1332
+ format: options.format,
1333
+ quiet: options.quiet,
1334
+ });
1335
+ process.exit(exitCode);
1336
+ } catch (error) {
1337
+ console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
1338
+ process.exit(1);
1339
+ }
1340
+ });
1341
+
1342
+ governCmd
1343
+ .command('watch')
1344
+ .description('Watch JSX/TSX files and re-check on changes')
1345
+ .option('-d, --dir <path>', 'Root directory (default: auto-detect)')
1346
+ .option('-c, --config <path>', 'Path to govern.config.ts')
1347
+ .option('-q, --quiet', 'Suppress non-error output')
1348
+ .option('--debounce <ms>', 'Debounce interval in ms', '300')
1349
+ .action(async (options) => {
1350
+ try {
1351
+ const { governWatch } = await import('./commands/govern-scan.js');
1352
+ await governWatch({
1353
+ dir: options.dir,
1354
+ config: options.config,
1355
+ quiet: options.quiet,
1356
+ debounce: parseInt(options.debounce, 10),
1357
+ });
1358
+ } catch (error) {
1359
+ console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
1360
+ process.exit(1);
1361
+ }
1362
+ });
1363
+
1202
1364
  // Parse command line arguments
1203
1365
  program.parse();
package/src/build.ts CHANGED
@@ -8,7 +8,7 @@ import type {
8
8
  CompiledBlock,
9
9
  CompiledTokenData,
10
10
  } from "./core/index.js";
11
- import { BRAND, compileBlock, parseTokenFile } from "./core/index.js";
11
+ import { BRAND, compileBlock, parseTokenFile, parseDTCGFile, isDTCGFile, isContractFile, parseComponentContract } from "./core/index.js";
12
12
  import { resolveTokensWithSass } from "./core/token-resolver.js";
13
13
  import type { BlockDefinition } from "./core/index.js";
14
14
  import {
@@ -28,6 +28,8 @@ import {
28
28
  type PropMeta,
29
29
  type ComponentMeta,
30
30
  } from "./core/component-extractor.js";
31
+ import { createExtractorAdapter } from "./core/extractor-adapter.js";
32
+ import { verifyContractDrift, formatDriftReport } from "./core/drift-verifier.js";
31
33
  import { buildComponentGraph } from "./core/graph-extractor.js";
32
34
  import { serializeGraph } from "@fragments-sdk/context/graph";
33
35
  import { resolvePerformanceConfig } from "./core/index.js";
@@ -128,6 +130,7 @@ export async function buildFragments(
128
130
  const errors: Array<{ file: string; error: string }> = [];
129
131
  const warnings: Array<{ file: string; warning: string }> = [];
130
132
  const fragments: CompiledFragmentsFile["fragments"] = {};
133
+ const contractSourcedNames = new Set<string>();
131
134
 
132
135
  // Create a persistent extractor — shared LanguageService across all fragments
133
136
  // Try to find a tsconfig.json in the config directory
@@ -143,6 +146,105 @@ export async function buildFragments(
143
146
  // Read file content as text
144
147
  const content = await readFile(file.absolutePath, "utf-8");
145
148
 
149
+ // Handle .contract.json files (framework-agnostic component contracts)
150
+ if (isContractFile(file.absolutePath)) {
151
+ try {
152
+ const parsed = parseComponentContract(content, file.relativePath);
153
+ const framework = parsed.framework ?? config.framework ?? 'react';
154
+ const contractAdapter = createExtractorAdapter(framework, tsconfigPath);
155
+
156
+ // Extract props from source (if adapter supports it)
157
+ let extractedMeta: ComponentMeta | null = null;
158
+ if (parsed.sourcePath && parsed.exportName && contractAdapter.canHandle(framework)) {
159
+ try {
160
+ // sourcePath is relative to fragments.config.ts root
161
+ const absSourcePath = resolve(configDir, parsed.sourcePath);
162
+ extractedMeta = contractAdapter.extract(absSourcePath, parsed.exportName);
163
+ } catch {
164
+ // Extraction failure is non-fatal for contracts
165
+ }
166
+ }
167
+
168
+ // Merge authored + extracted props
169
+ let mergedProps = parsed.props as Record<string, CompiledProp>;
170
+ if (extractedMeta && Object.keys(extractedMeta.props).length > 0) {
171
+ mergedProps = mergeDocumentedAndAutoProps(mergedProps, extractedMeta.props);
172
+ }
173
+
174
+ // Auto-compile propsSummary if not manually authored
175
+ let contractPropsSummary = parsed.propsSummary;
176
+ if ((!contractPropsSummary || contractPropsSummary.length === 0) && extractedMeta) {
177
+ contractPropsSummary = compilePropsSummary(extractedMeta.props);
178
+ }
179
+
180
+ // Auto-enrich AI metadata from composition detection
181
+ let ai = parsed.ai;
182
+ if (extractedMeta?.composition) {
183
+ const comp = extractedMeta.composition;
184
+ ai = {
185
+ compositionPattern: comp.pattern,
186
+ subComponents: comp.parts.map((p) => p.name),
187
+ ...ai,
188
+ };
189
+ }
190
+
191
+ // Drift verification for verified contracts
192
+ if (parsed.provenance?.verified && extractedMeta) {
193
+ const drift = verifyContractDrift(parsed, extractedMeta);
194
+ if (!drift.isClean) {
195
+ errors.push({
196
+ file: file.relativePath,
197
+ error: formatDriftReport(drift),
198
+ });
199
+ contractAdapter.dispose();
200
+ continue;
201
+ }
202
+ }
203
+
204
+ // Build compiled fragment with all enrichments
205
+ const compiled: CompiledFragment = {
206
+ filePath: file.relativePath,
207
+ meta: {
208
+ name: parsed.meta.name,
209
+ description: parsed.meta.description,
210
+ category: parsed.meta.category,
211
+ tags: parsed.meta.tags,
212
+ status: parsed.meta.status,
213
+ figma: parsed.meta.figma,
214
+ },
215
+ usage: parsed.usage,
216
+ props: mergedProps,
217
+ relations: parsed.relations,
218
+ variants: parsed.variants,
219
+ ...(parsed.contract && { contract: parsed.contract }),
220
+ ...(ai && { ai }),
221
+ framework: parsed.framework,
222
+ propsSummary: contractPropsSummary,
223
+ sourcePath: parsed.sourcePath,
224
+ exportName: parsed.exportName,
225
+ provenance: parsed.provenance,
226
+ };
227
+
228
+ fragments[compiled.meta.name] = compiled;
229
+ contractSourcedNames.add(compiled.meta.name);
230
+ contractAdapter.dispose();
231
+ } catch (error) {
232
+ errors.push({
233
+ file: file.relativePath,
234
+ error: `Contract parse error: ${error instanceof Error ? error.message : String(error)}`,
235
+ });
236
+ }
237
+ continue;
238
+ }
239
+
240
+ // Deprecation notice for .fragment.tsx files
241
+ if (file.absolutePath.endsWith('.fragment.tsx') || file.absolutePath.endsWith('.fragment.ts')) {
242
+ warnings.push({
243
+ file: file.relativePath,
244
+ warning: 'Deprecated format: .fragment.tsx — run `fragments migrate-contract` to generate .contract.json',
245
+ });
246
+ }
247
+
146
248
  // Skip files that don't contain defineFragment() — e.g., story files
147
249
  // that were accidentally included in the config
148
250
  if (!content.includes("defineFragment")) {
@@ -285,9 +387,22 @@ export async function buildFragments(
285
387
  ...(ai && { ai }),
286
388
  // Include contract metadata (auto-compiled or manual)
287
389
  ...(contract && { contract }),
390
+ // Provenance from TSX path
391
+ provenance: {
392
+ source: extractedMeta ? 'extracted' : 'manual',
393
+ verified: !!extractedMeta,
394
+ },
288
395
  };
289
396
 
290
- fragments[parsed.meta.name] = compiled;
397
+ // When both .contract.json and .fragment.tsx exist, prefer contract
398
+ if (contractSourcedNames.has(parsed.meta.name)) {
399
+ warnings.push({
400
+ file: file.relativePath,
401
+ warning: `Duplicate: "${parsed.meta.name}" already loaded from .contract.json — skipping .fragment.tsx`,
402
+ });
403
+ } else {
404
+ fragments[parsed.meta.name] = compiled;
405
+ }
291
406
  } catch (error) {
292
407
  errors.push({
293
408
  file: file.relativePath,
@@ -349,10 +464,19 @@ export async function buildFragments(
349
464
  const allContent = fileContents.map((f) => f.content).join('\n');
350
465
 
351
466
  for (const { content, path } of fileContents) {
352
- // Parse with the combined content to enable cross-file SCSS var resolution
353
- const parsed = parseTokenFile(allContent, path);
354
- // But only use tokens from THIS file's content to avoid duplicates
355
- const fileParsed = parseTokenFile(content, path);
467
+ // Route to the correct parser based on file extension
468
+ let fileParsed;
469
+ let parsed;
470
+ if (isDTCGFile(path)) {
471
+ // DTCG files are self-contained — no cross-file resolution needed
472
+ fileParsed = parseDTCGFile(content, path);
473
+ parsed = fileParsed;
474
+ } else {
475
+ // Parse SCSS/CSS with the combined content to enable cross-file var resolution
476
+ parsed = parseTokenFile(allContent, path);
477
+ // But only use tokens from THIS file's content to avoid duplicates
478
+ fileParsed = parseTokenFile(content, path);
479
+ }
356
480
  prefix = fileParsed.prefix;
357
481
  total += fileParsed.total;
358
482
  for (const [cat, catTokens] of Object.entries(fileParsed.categories)) {
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "shadcn-label-wrapper",
3
+ "private": true,
4
+ "devDependencies": {
5
+ "@fragments-sdk/core": "^1.0.1"
6
+ }
7
+ }
@@ -0,0 +1,42 @@
1
+ {
2
+ "$schema": "https://usefragments.com/schemas/contract.v1.json",
3
+ "name": "Label",
4
+ "description": "Label component",
5
+ "category": "Typography",
6
+ "sourcePath": "label.tsx",
7
+ "exportName": "Label",
8
+ "propsSummary": [
9
+ "className: string",
10
+ "htmlFor: string",
11
+ "form: string",
12
+ "children: node"
13
+ ],
14
+ "props": {
15
+ "className": {
16
+ "type": "string",
17
+ "description": ""
18
+ },
19
+ "htmlFor": {
20
+ "type": "string",
21
+ "description": ""
22
+ },
23
+ "form": {
24
+ "type": "string",
25
+ "description": ""
26
+ },
27
+ "children": {
28
+ "type": "node",
29
+ "description": ""
30
+ }
31
+ },
32
+ "usage": {
33
+ "when": [],
34
+ "whenNot": []
35
+ },
36
+ "provenance": {
37
+ "source": "extracted",
38
+ "verified": false,
39
+ "frameworkSupport": "native",
40
+ "extractedAt": "2026-03-13T23:33:02.488Z"
41
+ }
42
+ }
@@ -0,0 +1,11 @@
1
+ import * as React from "react";
2
+ import { Primitive } from "./primitive";
3
+
4
+ function Label({
5
+ className,
6
+ ...props
7
+ }: React.ComponentProps<typeof Primitive.Root>) {
8
+ return <Primitive.Root data-class={className} {...props} />;
9
+ }
10
+
11
+ export { Label };
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://usefragments.com/schemas/contract.v1.json",
3
+ "name": "Primitive",
4
+ "description": "Primitive component",
5
+ "category": "Components",
6
+ "sourcePath": "primitive.tsx",
7
+ "exportName": "Primitive",
8
+ "propsSummary": [],
9
+ "props": {},
10
+ "usage": {
11
+ "when": [],
12
+ "whenNot": []
13
+ },
14
+ "provenance": {
15
+ "source": "extracted",
16
+ "verified": false,
17
+ "frameworkSupport": "native",
18
+ "extractedAt": "2026-03-13T23:33:02.489Z"
19
+ }
20
+ }
@@ -0,0 +1,14 @@
1
+ import * as React from "react";
2
+
3
+ export interface PrimitiveLabelProps {
4
+ className?: string;
5
+ htmlFor?: string;
6
+ form?: string;
7
+ children?: React.ReactNode;
8
+ }
9
+
10
+ function Root(props: PrimitiveLabelProps) {
11
+ return <label {...props} />;
12
+ }
13
+
14
+ export const Primitive = { Root };
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "jsx": "react-jsx",
7
+ "allowSyntheticDefaultImports": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "noEmit": true,
11
+ "types": [
12
+ "react",
13
+ "react-dom"
14
+ ]
15
+ },
16
+ "include": [
17
+ "src/**/*.tsx"
18
+ ],
19
+ "exclude": [
20
+ "**/*.fragment.tsx",
21
+ "**/*.contract.json"
22
+ ]
23
+ }