@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
@@ -22,6 +22,7 @@ import {
22
22
  import {
23
23
  loadConfig,
24
24
  discoverAllComponents,
25
+ findConfigFile,
25
26
  type DiscoveredComponent,
26
27
  } from "../core/node.js";
27
28
  import {
@@ -38,6 +39,7 @@ import {
38
39
  type PropsExtractionResult,
39
40
  type ParsedStoryFile,
40
41
  } from "../service/index.js";
42
+ import { getGeneratorVersion } from '@fragments-sdk/compiler';
41
43
 
42
44
  export interface ScanOptions {
43
45
  /** Path to config file */
@@ -146,6 +148,7 @@ export async function scan(options: ScanOptions = {}): Promise<ScanResult> {
146
148
  try {
147
149
  const extraction = await extractPropsFromFile(comp.sourcePath, {
148
150
  propsTypeName: `${comp.name}Props`,
151
+ componentName: comp.name,
149
152
  });
150
153
 
151
154
  propsResults.set(comp.name, extraction);
@@ -270,10 +273,21 @@ export async function scan(options: ScanOptions = {}): Promise<ScanResult> {
270
273
  // Write output
271
274
  const outputPath = resolve(configDir, outputFile);
272
275
  await mkdir(dirname(outputPath), { recursive: true });
276
+ const generatorVersion = await getGeneratorVersion();
277
+ const buildInputs = components
278
+ .map((component) => relative(configDir, component.sourcePath).split("\\").join("/"))
279
+ .sort();
280
+ const configPath = options.config ? resolve(process.cwd(), options.config) : findConfigFile(configDir);
281
+ if (configPath) {
282
+ buildInputs.push(relative(configDir, configPath).split("\\").join("/"));
283
+ buildInputs.sort();
284
+ }
273
285
 
274
286
  const output: CompiledFragmentsFile = {
275
287
  version: "1.0.0",
276
288
  generatedAt: new Date().toISOString(),
289
+ generatorVersion,
290
+ buildInputs,
277
291
  fragments,
278
292
  };
279
293
 
@@ -456,6 +456,28 @@ function generateScssSeedImport(brand?: string): string {
456
456
  `;
457
457
  }
458
458
 
459
+ // ============================================
460
+ // Step runner
461
+ // ============================================
462
+
463
+ async function runSetupStep(
464
+ fn: () => Promise<{ modified: boolean; message: string }>,
465
+ failureLabel: string,
466
+ actions: string[],
467
+ errors: string[],
468
+ ): Promise<void> {
469
+ try {
470
+ const result = await fn();
471
+ const icon = result.modified ? pc.green('+') : pc.dim('·');
472
+ console.log(` ${icon} ${result.message}`);
473
+ if (result.modified) actions.push(result.message);
474
+ } catch (error) {
475
+ const msg = `${failureLabel}: ${error instanceof Error ? error.message : error}`;
476
+ console.log(` ${pc.red('✗')} ${msg}`);
477
+ errors.push(msg);
478
+ }
479
+ }
480
+
459
481
  // ============================================
460
482
  // Main Setup Function
461
483
  // ============================================
@@ -491,72 +513,27 @@ export async function setup(options: SetupOptions = {}): Promise<SetupResult> {
491
513
 
492
514
  // 3. Add styles import
493
515
  if (entryFile) {
494
- try {
495
- const result = await addStylesImport(root, entryFile);
496
- const icon = result.modified ? pc.green('+') : pc.dim('·');
497
- console.log(` ${icon} ${result.message}`);
498
- if (result.modified) actions.push(result.message);
499
- } catch (error) {
500
- const msg = `Failed to add styles import: ${error instanceof Error ? error.message : error}`;
501
- console.log(` ${pc.red('✗')} ${msg}`);
502
- errors.push(msg);
503
- }
516
+ await runSetupStep(() => addStylesImport(root, entryFile), 'Failed to add styles import', actions, errors);
504
517
  }
505
518
 
506
519
  // 4. Add ThemeProvider imports
507
520
  if (entryFile) {
508
- try {
509
- const result = await addThemeProvider(root, entryFile, framework);
510
- const icon = result.modified ? pc.green('+') : pc.dim('·');
511
- console.log(` ${icon} ${result.message}`);
512
- if (result.modified) actions.push(result.message);
513
- } catch (error) {
514
- const msg = `Failed to add ThemeProvider: ${error instanceof Error ? error.message : error}`;
515
- console.log(` ${pc.red('✗')} ${msg}`);
516
- errors.push(msg);
517
- }
521
+ await runSetupStep(() => addThemeProvider(root, entryFile, framework), 'Failed to add ThemeProvider', actions, errors);
518
522
  }
519
523
 
520
524
  // 5. Next.js: add transpilePackages
521
525
  if (framework === 'nextjs-app' || framework === 'nextjs-pages') {
522
- try {
523
- const result = await addTranspilePackages(root);
524
- const icon = result.modified ? pc.green('+') : pc.dim('·');
525
- console.log(` ${icon} ${result.message}`);
526
- if (result.modified) actions.push(result.message);
527
- } catch (error) {
528
- const msg = `Failed to update next.config: ${error instanceof Error ? error.message : error}`;
529
- console.log(` ${pc.red('✗')} ${msg}`);
530
- errors.push(msg);
531
- }
526
+ await runSetupStep(() => addTranspilePackages(root), 'Failed to update next.config', actions, errors);
532
527
  }
533
528
 
534
529
  // 6. Create SCSS seeds file (if --scss flag or brand color specified)
535
530
  if (options.scss || options.brand) {
536
- try {
537
- const result = await createScssSeeds(root, options.brand);
538
- const icon = result.modified ? pc.green('+') : pc.dim('·');
539
- console.log(` ${icon} ${result.message}`);
540
- if (result.modified) actions.push(result.message);
541
- } catch (error) {
542
- const msg = `Failed to create SCSS seeds: ${error instanceof Error ? error.message : error}`;
543
- console.log(` ${pc.red('✗')} ${msg}`);
544
- errors.push(msg);
545
- }
531
+ await runSetupStep(() => createScssSeeds(root, options.brand), 'Failed to create SCSS seeds', actions, errors);
546
532
  }
547
533
 
548
534
  // 7. Configure MCP server (if --mcp flag)
549
535
  if (options.mcp) {
550
- try {
551
- const result = await setupMcpConfig(root);
552
- const icon = result.modified ? pc.green('+') : pc.dim('·');
553
- console.log(` ${icon} ${result.message}`);
554
- if (result.modified) actions.push(result.message);
555
- } catch (error) {
556
- const msg = `Failed to configure MCP: ${error instanceof Error ? error.message : error}`;
557
- console.log(` ${pc.red('✗')} ${msg}`);
558
- errors.push(msg);
559
- }
536
+ await runSetupStep(() => setupMcpConfig(root), 'Failed to configure MCP', actions, errors);
560
537
  }
561
538
 
562
539
  // Summary
@@ -13,8 +13,8 @@ import { BRAND } from '../core/index.js';
13
13
  import { loadConfig } from '../core/node.js';
14
14
  import { discoverFragmentFiles, loadFragmentFile } from '../core/node.js';
15
15
  import { parseFragmentFile } from '../core/parser.js';
16
- import { resolveComponentSourcePath } from '../core/auto-props.js';
17
- import { createComponentExtractor, type PropMeta, type CompositionMeta } from '../core/component-extractor.js';
16
+ import { resolveComponentSourcePath } from '@fragments-sdk/extract';
17
+ import { createComponentExtractor, type PropMeta, type CompositionMeta } from '@fragments-sdk/extract';
18
18
  import type { FragmentsConfig } from '@fragments-sdk/core';
19
19
 
20
20
  // ---------------------------------------------------------------------------
@@ -0,0 +1,113 @@
1
+ /**
2
+ * fragments tokens generate — Generate CSS, SCSS, Tailwind, or Figma output
3
+ * from a DTCG .tokens.json source file.
4
+ */
5
+
6
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
7
+ import { resolve, dirname, basename, extname } from 'node:path';
8
+ import pc from 'picocolors';
9
+ import {
10
+ generateCSSCustomProperties,
11
+ generateSCSSVariables,
12
+ generateTailwindConfig,
13
+ generateFigmaVariables,
14
+ } from '../core/index.js';
15
+ import type { DTCGTokenFile } from '../core/index.js';
16
+
17
+ export interface TokensGenerateOptions {
18
+ /** Path to DTCG .tokens.json source file */
19
+ from: string;
20
+ /** Output formats (comma-separated: css, scss, tailwind, figma) */
21
+ format: string;
22
+ /** Output directory */
23
+ out?: string;
24
+ /** Token name prefix */
25
+ prefix?: string;
26
+ /** CSS selector for custom properties (default: ':root') */
27
+ selector?: string;
28
+ /** Verbose output */
29
+ verbose?: boolean;
30
+ }
31
+
32
+ type OutputFormat = 'css' | 'scss' | 'tailwind' | 'figma';
33
+
34
+ const VALID_FORMATS = new Set<OutputFormat>(['css', 'scss', 'tailwind', 'figma']);
35
+
36
+ export async function tokensGenerate(options: TokensGenerateOptions): Promise<void> {
37
+ const { from, format, out, prefix, selector, verbose } = options;
38
+
39
+ // Parse formats
40
+ const formats = format.split(',').map((f) => f.trim().toLowerCase()) as OutputFormat[];
41
+ const invalidFormats = formats.filter((f) => !VALID_FORMATS.has(f));
42
+ if (invalidFormats.length > 0) {
43
+ console.error(pc.red(`Invalid format(s): ${invalidFormats.join(', ')}`));
44
+ console.error(`Valid formats: ${[...VALID_FORMATS].join(', ')}`);
45
+ process.exit(1);
46
+ }
47
+
48
+ // Read source file
49
+ const sourcePath = resolve(process.cwd(), from);
50
+ let content: string;
51
+ try {
52
+ content = await readFile(sourcePath, 'utf-8');
53
+ } catch {
54
+ console.error(pc.red(`Could not read file: ${sourcePath}`));
55
+ process.exit(1);
56
+ }
57
+
58
+ let tokens: DTCGTokenFile;
59
+ try {
60
+ tokens = JSON.parse(content) as DTCGTokenFile;
61
+ } catch {
62
+ console.error(pc.red(`Invalid JSON in: ${sourcePath}`));
63
+ process.exit(1);
64
+ }
65
+
66
+ // Determine output directory
67
+ const outDir = out ? resolve(process.cwd(), out) : dirname(sourcePath);
68
+ await mkdir(outDir, { recursive: true });
69
+
70
+ const baseName = basename(from, extname(from)).replace(/\.tokens$/, '');
71
+
72
+ // Generate each format
73
+ for (const fmt of formats) {
74
+ let output: string;
75
+ let fileName: string;
76
+
77
+ switch (fmt) {
78
+ case 'css': {
79
+ output = generateCSSCustomProperties(tokens, { prefix, selector });
80
+ fileName = `${baseName}.css`;
81
+ break;
82
+ }
83
+ case 'scss': {
84
+ output = generateSCSSVariables(tokens, { prefix });
85
+ fileName = `_${baseName}.scss`;
86
+ break;
87
+ }
88
+ case 'tailwind': {
89
+ const config = generateTailwindConfig(tokens);
90
+ output = `// Auto-generated Tailwind config from DTCG tokens\n// Do not edit directly\nexport default ${JSON.stringify(config, null, 2)};\n`;
91
+ fileName = `tailwind.tokens.js`;
92
+ break;
93
+ }
94
+ case 'figma': {
95
+ const collections = generateFigmaVariables(tokens);
96
+ output = JSON.stringify(collections, null, 2);
97
+ fileName = `${baseName}.figma-variables.json`;
98
+ break;
99
+ }
100
+ default:
101
+ continue;
102
+ }
103
+
104
+ const outputPath = resolve(outDir, fileName);
105
+ await writeFile(outputPath, output, 'utf-8');
106
+
107
+ if (verbose) {
108
+ console.log(pc.green(` ✓ ${fmt}`), pc.dim(outputPath));
109
+ }
110
+ }
111
+
112
+ console.log(pc.green(`Generated ${formats.length} output(s) from ${from}`));
113
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * fragments tokens push — Push code tokens to Fragments Cloud for drift comparison.
3
+ *
4
+ * Extracts CSS custom properties from Tailwind v4 @theme blocks (or config-based
5
+ * token files) and POSTs them to the Fragments Cloud /api/ingest endpoint.
6
+ * Supports --dry-run for local inspection without pushing.
7
+ */
8
+
9
+ import { resolve } from 'node:path';
10
+ import pc from 'picocolors';
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Options
14
+ // ---------------------------------------------------------------------------
15
+
16
+ export interface TokensPushOptions {
17
+ /** Path to fragments config file */
18
+ config?: string;
19
+ /** Path to Tailwind v4 CSS file with @theme block */
20
+ tailwindV4?: string;
21
+ /** Parse and display tokens without pushing */
22
+ dryRun?: boolean;
23
+ /** Show detailed output */
24
+ verbose?: boolean;
25
+ }
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // tokensPush
29
+ // ---------------------------------------------------------------------------
30
+
31
+ export async function tokensPush(options: TokensPushOptions): Promise<void> {
32
+ const startTime = Date.now();
33
+
34
+ // 1. Resolve API credentials
35
+ const apiKey = process.env.FRAGMENTS_API_KEY;
36
+ const baseUrl = process.env.FRAGMENTS_URL || 'https://app.usefragments.com';
37
+
38
+ if (!apiKey && !options.dryRun) {
39
+ console.error(pc.red('Error: FRAGMENTS_API_KEY environment variable is required'));
40
+ console.error(pc.dim('Get your API key from https://app.usefragments.com/api-keys'));
41
+ process.exit(1);
42
+ }
43
+
44
+ // 2. Token extraction
45
+ const flatMap: Record<string, string> = {};
46
+
47
+ if (options.tailwindV4) {
48
+ // Use @theme parser for Tailwind v4 CSS files
49
+ const { parseTailwindV4File } = await import('../service/token-parser.js');
50
+
51
+ const filePath = resolve(process.cwd(), options.tailwindV4);
52
+ const result = parseTailwindV4File(filePath);
53
+
54
+ if (result.errors.length > 0) {
55
+ for (const err of result.errors) {
56
+ console.error(pc.red(` Error: ${err.message}`));
57
+ }
58
+ }
59
+
60
+ if (options.verbose && result.warnings.length > 0) {
61
+ for (const warn of result.warnings) {
62
+ console.warn(pc.yellow(` Warning: ${warn}`));
63
+ }
64
+ }
65
+
66
+ for (const token of result.tokens) {
67
+ flatMap[token.name] = token.resolvedValue;
68
+ }
69
+
70
+ if (options.verbose) {
71
+ console.log(
72
+ pc.dim(` Parsed ${result.tokens.length} tokens from ${options.tailwindV4} (${result.parseTimeMs.toFixed(1)}ms)`),
73
+ );
74
+ }
75
+ } else if (options.config) {
76
+ // Use config-based token extraction
77
+ try {
78
+ const { loadConfig } = await import('../core/node.js');
79
+ const { parseTokenFiles } = await import('../service/index.js');
80
+
81
+ const { config, configDir } = await loadConfig(options.config);
82
+ if (config.tokens?.include?.length) {
83
+ const result = await parseTokenFiles(config.tokens, configDir);
84
+ for (const token of result.tokens) {
85
+ flatMap[token.name] = token.resolvedValue;
86
+ }
87
+
88
+ if (options.verbose) {
89
+ console.log(
90
+ pc.dim(` Parsed ${result.tokens.length} tokens from config (${result.parseTimeMs.toFixed(1)}ms)`),
91
+ );
92
+ }
93
+ } else {
94
+ console.error(pc.red('Error: Config file has no tokens.include patterns'));
95
+ process.exit(1);
96
+ }
97
+ } catch (error) {
98
+ console.error(pc.red('Error loading config:'), error instanceof Error ? error.message : error);
99
+ process.exit(1);
100
+ }
101
+ } else {
102
+ // No source specified — try auto-detection
103
+ try {
104
+ const { loadConfig } = await import('../core/node.js');
105
+ const { parseTokenFiles } = await import('../service/index.js');
106
+
107
+ const { config, configDir } = await loadConfig();
108
+ if (config.tokens?.include?.length) {
109
+ const result = await parseTokenFiles(config.tokens, configDir);
110
+ for (const token of result.tokens) {
111
+ flatMap[token.name] = token.resolvedValue;
112
+ }
113
+
114
+ if (options.verbose) {
115
+ console.log(
116
+ pc.dim(` Parsed ${result.tokens.length} tokens from auto-detected config (${result.parseTimeMs.toFixed(1)}ms)`),
117
+ );
118
+ }
119
+ }
120
+ } catch {
121
+ // No config found — fall through to error
122
+ }
123
+
124
+ if (Object.keys(flatMap).length === 0) {
125
+ console.error(pc.red('Error: No token source specified'));
126
+ console.error(pc.dim('Provide one of:'));
127
+ console.error(pc.dim(' --tailwind-v4 <path> Path to Tailwind v4 CSS file with @theme block'));
128
+ console.error(pc.dim(' --config <path> Path to fragments config with tokens.include'));
129
+ console.error(pc.dim('\nExample: fragments tokens push --tailwind-v4 ./app.css'));
130
+ process.exit(1);
131
+ }
132
+ }
133
+
134
+ const tokenCount = Object.keys(flatMap).length;
135
+
136
+ if (tokenCount === 0) {
137
+ console.log(pc.yellow('No tokens found.'));
138
+ return;
139
+ }
140
+
141
+ console.log(pc.cyan(`Found ${tokenCount} tokens`));
142
+
143
+ // 3. Dry run — display tokens and exit
144
+ if (options.dryRun) {
145
+ console.log(pc.dim('\nDry run — tokens that would be pushed:\n'));
146
+ const entries = Object.entries(flatMap);
147
+ for (const [name, value] of entries.slice(0, 20)) {
148
+ console.log(` ${pc.bold(name)}: ${value}`);
149
+ }
150
+ if (entries.length > 20) {
151
+ console.log(pc.dim(` ... and ${entries.length - 20} more`));
152
+ }
153
+ return;
154
+ }
155
+
156
+ // 4. POST to Fragments Cloud
157
+ try {
158
+ const response = await fetch(`${baseUrl}/api/ingest`, {
159
+ method: 'POST',
160
+ headers: {
161
+ 'Content-Type': 'application/json',
162
+ 'Authorization': `Bearer ${apiKey}`,
163
+ },
164
+ body: JSON.stringify({
165
+ codeTokens: JSON.stringify(flatMap),
166
+ }),
167
+ });
168
+
169
+ if (!response.ok) {
170
+ const errorText = await response.text();
171
+ console.error(pc.red(`Error: API returned ${response.status}`));
172
+ console.error(pc.dim(errorText));
173
+ process.exit(1);
174
+ }
175
+
176
+ const result = await response.json() as Record<string, unknown>;
177
+
178
+ console.log(pc.green(`\nPushed ${tokenCount} tokens to Fragments Cloud`));
179
+
180
+ if (result.tokenDrift) {
181
+ const drift = result.tokenDrift as Record<string, unknown>;
182
+ const summary = drift.summary as Record<string, number> | undefined;
183
+ if (summary) {
184
+ console.log(pc.cyan('\nDrift Summary:'));
185
+ if (summary.total !== undefined) console.log(pc.dim(` Total issues: ${summary.total}`));
186
+ if (summary.missingInCode > 0) console.log(pc.yellow(` Missing in code: ${summary.missingInCode}`));
187
+ if (summary.missingInFigma > 0) console.log(pc.yellow(` Missing in Figma: ${summary.missingInFigma}`));
188
+ if (summary.valueMismatch > 0) console.log(pc.yellow(` Value mismatches: ${summary.valueMismatch}`));
189
+ if (summary.score !== undefined) console.log(` Health score: ${summary.score}%`);
190
+ }
191
+ }
192
+
193
+ console.log(pc.dim(`\nView dashboard: ${baseUrl}/tokens`));
194
+ console.log(pc.dim(`Completed in ${Date.now() - startTime}ms`));
195
+ } catch (error) {
196
+ console.error(pc.red('Error pushing tokens:'), error instanceof Error ? error.message : error);
197
+ process.exit(1);
198
+ }
199
+ }
@@ -6,7 +6,14 @@
6
6
  */
7
7
 
8
8
  import pc from 'picocolors';
9
- import { BRAND } from '../core/index.js';
9
+ import { readFile } from 'node:fs/promises';
10
+ import { resolve } from 'node:path';
11
+ import { existsSync } from 'node:fs';
12
+ import {
13
+ BRAND,
14
+ type CompiledFragment,
15
+ type CompiledFragmentsFile,
16
+ } from '../core/index.js';
10
17
  import { loadConfig } from '../core/node.js';
11
18
  import {
12
19
  createDevServerClient,
@@ -86,6 +93,10 @@ export async function verify(
86
93
  console.log(pc.dim(`Minimum compliance: ${minCompliance}%\n`));
87
94
  }
88
95
 
96
+ if (ci) {
97
+ return verifyFromLocalFragments(configPath, minCompliance, component);
98
+ }
99
+
89
100
  // Check if dev server is reachable
90
101
  const isReachable = await client.ping();
91
102
  if (!isReachable) {
@@ -213,3 +224,186 @@ export async function verify(
213
224
 
214
225
  return summary;
215
226
  }
227
+
228
+ /**
229
+ * Compute metadata completeness score for a compiled fragment.
230
+ *
231
+ * Scoring breakdown (max 100):
232
+ * - Has description: +20
233
+ * - Has category that isn't "Components": +15
234
+ * - Has when/whenNot usage guidelines: +20
235
+ * - Has props with types: +20
236
+ * - Has variants: +15
237
+ * - Has relations: +10
238
+ */
239
+ function computeMetadataScore(fragment: CompiledFragment): {
240
+ score: number;
241
+ violations: ViolationItem[];
242
+ } {
243
+ let score = 0;
244
+ const violations: ViolationItem[] = [];
245
+
246
+ // Description (+20)
247
+ if (fragment.meta.description && fragment.meta.description.trim().length > 0) {
248
+ score += 20;
249
+ } else {
250
+ violations.push({
251
+ property: 'meta.description',
252
+ issue: 'Missing component description',
253
+ severity: 'warning',
254
+ suggestion: 'Add a description to the fragment definition',
255
+ });
256
+ }
257
+
258
+ // Category that isn't the default "Components" (+15)
259
+ if (fragment.meta.category && fragment.meta.category !== 'Components') {
260
+ score += 15;
261
+ } else {
262
+ violations.push({
263
+ property: 'meta.category',
264
+ issue: fragment.meta.category === 'Components'
265
+ ? 'Using default category "Components"'
266
+ : 'Missing category',
267
+ severity: 'warning',
268
+ suggestion: 'Set a specific category (e.g., "forms", "layout", "feedback")',
269
+ });
270
+ }
271
+
272
+ // Usage guidelines — when/whenNot (+20)
273
+ const hasWhen = fragment.usage?.when && fragment.usage.when.length > 0;
274
+ const hasWhenNot = fragment.usage?.whenNot && fragment.usage.whenNot.length > 0;
275
+ if (hasWhen || hasWhenNot) {
276
+ score += 20;
277
+ } else {
278
+ violations.push({
279
+ property: 'usage.when / usage.whenNot',
280
+ issue: 'Missing usage guidelines',
281
+ severity: 'warning',
282
+ suggestion: 'Add when[] and whenNot[] arrays to describe appropriate usage',
283
+ });
284
+ }
285
+
286
+ // Props with types (+20)
287
+ const propKeys = Object.keys(fragment.props ?? {});
288
+ if (propKeys.length > 0) {
289
+ score += 20;
290
+ } else {
291
+ violations.push({
292
+ property: 'props',
293
+ issue: 'No props defined',
294
+ severity: 'warning',
295
+ suggestion: 'Define props with types in the fragment definition',
296
+ });
297
+ }
298
+
299
+ // Variants (+15)
300
+ if (fragment.variants && fragment.variants.length > 0) {
301
+ score += 15;
302
+ } else {
303
+ violations.push({
304
+ property: 'variants',
305
+ issue: 'No variants defined',
306
+ severity: 'warning',
307
+ suggestion: 'Add at least one variant showing component usage',
308
+ });
309
+ }
310
+
311
+ // Relations (+10)
312
+ if (fragment.relations && fragment.relations.length > 0) {
313
+ score += 10;
314
+ } else {
315
+ violations.push({
316
+ property: 'relations',
317
+ issue: 'No component relations defined',
318
+ severity: 'warning',
319
+ suggestion: 'Define relations to related components (parent, child, alternative)',
320
+ });
321
+ }
322
+
323
+ return { score, violations };
324
+ }
325
+
326
+ /**
327
+ * CI fallback: verify components from the local fragments.json file
328
+ * without requiring a running dev server.
329
+ */
330
+ async function verifyFromLocalFragments(
331
+ configPath: string | undefined,
332
+ minCompliance: number,
333
+ component: string | undefined,
334
+ ): Promise<VerifySummary> {
335
+ // Load config to resolve outFile
336
+ const { config, configDir } = await loadConfig(configPath);
337
+ const outFile = config.outFile ?? 'fragments.json';
338
+ const fragmentsPath = resolve(configDir ?? process.cwd(), outFile);
339
+
340
+ if (!existsSync(fragmentsPath)) {
341
+ const error = {
342
+ error: `fragments.json not found at ${fragmentsPath}. Run "fragments build" first.`,
343
+ };
344
+ console.log(JSON.stringify(error));
345
+ process.exit(1);
346
+ }
347
+
348
+ const raw = await readFile(fragmentsPath, 'utf-8');
349
+ const data: CompiledFragmentsFile = JSON.parse(raw);
350
+
351
+ // Filter to only local fragments (skip anything from node_modules)
352
+ let entries = Object.entries(data.fragments).filter(
353
+ ([, frag]) => !frag.filePath.includes('node_modules')
354
+ );
355
+
356
+ // Filter by specific component if requested
357
+ if (component) {
358
+ entries = entries.filter(
359
+ ([name]) => name.toLowerCase() === component.toLowerCase()
360
+ );
361
+
362
+ if (entries.length === 0) {
363
+ const error = { error: `Component "${component}" not found in local fragments` };
364
+ console.log(JSON.stringify(error));
365
+ process.exit(1);
366
+ }
367
+ }
368
+
369
+ const results: VerifyResultItem[] = [];
370
+ let totalCompliance = 0;
371
+
372
+ for (const [name, fragment] of entries) {
373
+ const { score, violations } = computeMetadataScore(fragment);
374
+ const passed = score >= minCompliance;
375
+
376
+ results.push({
377
+ component: name,
378
+ compliance: score,
379
+ passed,
380
+ violations,
381
+ totalProperties: Object.keys(fragment.props ?? {}).length,
382
+ hardcoded: 0,
383
+ usingTokens: 0,
384
+ });
385
+
386
+ totalCompliance += score;
387
+ }
388
+
389
+ const componentCount = entries.length;
390
+ const averageCompliance = componentCount > 0 ? totalCompliance / componentCount : 100;
391
+ const allPassed = results.every(r => r.passed);
392
+
393
+ const summary: VerifySummary = {
394
+ passed: allPassed,
395
+ compliance: Math.round(averageCompliance * 100) / 100,
396
+ threshold: minCompliance,
397
+ totalComponents: componentCount,
398
+ passedComponents: results.filter(r => r.passed).length,
399
+ failedComponents: results.filter(r => !r.passed).length,
400
+ results,
401
+ violations: results.flatMap(r => r.violations.map(v => ({
402
+ component: r.component,
403
+ ...v,
404
+ }))),
405
+ };
406
+
407
+ console.log(JSON.stringify(summary, null, 2));
408
+ return summary;
409
+ }
@@ -0,0 +1,7 @@
1
+ import * as React from 'react';
2
+
3
+ function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
4
+ return <input className={className} type={type} {...props} />;
5
+ }
6
+
7
+ export { Input };