@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
@@ -1,19 +1,21 @@
1
1
  import { describe, it, expect, beforeAll, afterAll, vi } from "vitest";
2
2
  import { mkdtemp, writeFile, mkdir, rm, readFile as rf } from "node:fs/promises";
3
- import { join } from "node:path";
3
+ import { join, resolve } from "node:path";
4
4
  import { tmpdir } from "node:os";
5
5
  import {
6
6
  extractComponentJSDocFromSource,
7
7
  detectCompoundComponentsFromSource,
8
8
  calculateFieldConfidence,
9
9
  scanGenerate,
10
+ detectPackageManagerForProject,
11
+ excludeFragmentsFromTsconfig,
10
12
  parseEnrichmentResponse,
11
13
  buildEnrichmentSystemPrompt,
12
14
  buildEnrichmentUserPrompt,
13
15
  buildEnrichedUsageBlock,
14
16
  type EnrichmentResult,
15
17
  } from "../scan-generate.js";
16
- import type { ComponentMeta } from "../../core/component-extractor.js";
18
+ import type { ComponentMeta } from '@fragments-sdk/extract';
17
19
 
18
20
  // ---------------------------------------------------------------------------
19
21
  // Helper to build a minimal ComponentMeta for tests
@@ -268,39 +270,28 @@ export function Button({ children, variant = 'primary', disabled, onClick }: But
268
270
  // Read the generated file and check structure
269
271
  const { readFile: rf } = await import("node:fs/promises");
270
272
  const content = await rf(
271
- join(tmpDir, "components", "Button", "Button.fragment.tsx"),
273
+ join(tmpDir, "components", "Button", "Button.contract.json"),
272
274
  "utf-8"
273
275
  );
274
276
 
275
- // Check import uses @fragments-sdk/core
276
- expect(content).toContain(
277
- "import { defineFragment } from '@fragments-sdk/core'"
278
- );
279
-
280
- // Check confidence header
281
- expect(content).toMatch(
282
- /\/\/ Auto-generated by fragments init --scan \| Confidence: \d+\/100/
283
- );
277
+ const contract = JSON.parse(content);
284
278
 
285
- // Check TODO markers exist
286
- expect(content).toContain("// TODO:");
287
-
288
- // Check component name in meta
289
- expect(content).toContain("name: 'Button'");
279
+ // Check required contract fields
280
+ expect(contract.$schema).toBe("https://usefragments.com/schemas/contract.v1.json");
281
+ expect(contract.name).toBe("Button");
290
282
 
291
283
  // Check JSDoc was picked up in description
292
- expect(content).toContain("A primary button for user interactions.");
284
+ expect(contract.description).toContain("A primary button for user interactions.");
293
285
 
294
286
  // Check category was inferred (Button → Actions)
295
- expect(content).toContain("category: 'Actions'");
287
+ expect(contract.category).toBe("Actions");
296
288
 
297
- // Check props block has content
298
- expect(content).toContain("children:");
299
- expect(content).toContain("variant:");
289
+ // Check props have content
290
+ expect(contract.props).toHaveProperty("variant");
300
291
 
301
- // Check _generated provenance block
302
- expect(content).toContain("_generated:");
303
- expect(content).toContain("source: 'ai'");
292
+ // Check provenance block
293
+ expect(contract.provenance).toBeDefined();
294
+ expect(contract.provenance.source).toBe("extracted");
304
295
  });
305
296
 
306
297
  it("skips existing fragments without --force", async () => {
@@ -326,17 +317,163 @@ export function Button({ children, variant = 'primary', disabled, onClick }: But
326
317
 
327
318
  const { readFile: rf } = await import("node:fs/promises");
328
319
  const content = await rf(
329
- join(tmpDir, "components", "Button", "Button.fragment.tsx"),
320
+ join(tmpDir, "components", "Button", "Button.contract.json"),
330
321
  "utf-8"
331
322
  );
332
323
 
333
- // Check contract block exists
334
- expect(content).toContain("contract:");
335
- expect(content).toContain("propsSummary:");
324
+ const contract = JSON.parse(content);
325
+
326
+ // Check propsSummary exists and has entries for local props
327
+ expect(contract.propsSummary).toBeDefined();
328
+ expect(contract.propsSummary.length).toBeGreaterThan(0);
329
+ expect(contract.propsSummary.some((s: string) => s.includes("variant"))).toBe(true);
330
+ });
331
+ });
332
+
333
+ describe("scanGenerate helpers", () => {
334
+ let tmpDir: string;
335
+
336
+ beforeAll(async () => {
337
+ tmpDir = await mkdtemp(join(tmpdir(), "scan-gen-helpers-"));
338
+ });
339
+
340
+ afterAll(async () => {
341
+ await rm(tmpDir, { recursive: true, force: true });
342
+ });
343
+
344
+ it("detects pnpm from an ancestor workspace root", async () => {
345
+ const workspaceRoot = join(tmpDir, "workspace-root");
346
+ const appDir = join(workspaceRoot, "apps", "demo");
347
+ await mkdir(appDir, { recursive: true });
348
+ await writeFile(join(workspaceRoot, "pnpm-workspace.yaml"), "packages:\n - 'apps/*'\n");
349
+ await writeFile(join(workspaceRoot, "pnpm-lock.yaml"), "lockfileVersion: '9.0'\n");
350
+
351
+ const result = await detectPackageManagerForProject(appDir);
352
+
353
+ expect(result.manager).toBe("pnpm");
354
+ expect(result.lockfileDir).toBe(workspaceRoot);
355
+ });
356
+
357
+ it("adds fragment exclusions to JSONC tsconfig files with aliases", async () => {
358
+ const projectDir = join(tmpDir, "jsonc-app");
359
+ const scanDir = join(projectDir, "src", "components");
360
+ await mkdir(scanDir, { recursive: true });
361
+ await writeFile(
362
+ join(projectDir, "package.json"),
363
+ JSON.stringify({ name: "jsonc-app", private: true }, null, 2)
364
+ );
365
+ await writeFile(
366
+ join(projectDir, "tsconfig.app.json"),
367
+ `{
368
+ "compilerOptions": {
369
+ "baseUrl": ".",
370
+ "paths": {
371
+ "@/*": ["./src/*"]
372
+ }
373
+ },
374
+ "include": ["src"]
375
+ }
376
+ `
377
+ );
378
+
379
+ await excludeFragmentsFromTsconfig(scanDir);
380
+
381
+ const updated = JSON.parse(await rf(join(projectDir, "tsconfig.app.json"), "utf-8"));
382
+ expect(updated.exclude).toContain("**/*.contract.json");
383
+ });
384
+
385
+ it("generates shadcn-style compound fragments without React imports or member syntax", async () => {
386
+ const projectDir = join(tmpDir, "shadcn-compound");
387
+ const componentDir = join(projectDir, "components");
388
+ const cardDir = join(componentDir, "Card");
389
+ await mkdir(cardDir, { recursive: true });
390
+
391
+ await writeFile(
392
+ join(projectDir, "package.json"),
393
+ JSON.stringify(
394
+ {
395
+ name: "shadcn-compound",
396
+ private: true,
397
+ devDependencies: {
398
+ "@fragments-sdk/core": "workspace:^",
399
+ },
400
+ },
401
+ null,
402
+ 2
403
+ )
404
+ );
405
+ await writeFile(
406
+ join(projectDir, "tsconfig.app.json"),
407
+ JSON.stringify(
408
+ {
409
+ compilerOptions: {
410
+ jsx: "react-jsx",
411
+ },
412
+ include: ["components"],
413
+ },
414
+ null,
415
+ 2
416
+ )
417
+ );
418
+ await writeFile(
419
+ join(cardDir, "Card.tsx"),
420
+ `export function Card(props: { children?: React.ReactNode; size?: 'default' | 'sm' }) {
421
+ return <div>{props.children}</div>;
422
+ }
423
+
424
+ export function CardHeader(props: { children?: React.ReactNode }) {
425
+ return <div>{props.children}</div>;
426
+ }
427
+
428
+ export function CardContent(props: { children?: React.ReactNode }) {
429
+ return <div>{props.children}</div>;
430
+ }
431
+
432
+ export function CardFooter(props: { children?: React.ReactNode }) {
433
+ return <div>{props.children}</div>;
434
+ }
435
+
436
+ export { Card as default };
437
+ `
438
+ );
439
+
440
+ const result = await scanGenerate({
441
+ scanPath: componentDir,
442
+ force: true,
443
+ });
444
+
445
+ expect(result.success).toBe(true);
446
+
447
+ const content = await rf(join(cardDir, "Card.contract.json"), "utf-8");
448
+ const contract = JSON.parse(content);
449
+ // Contract.json should have the component name and props
450
+ expect(contract.name).toBe("Card");
451
+ expect(contract.$schema).toBe("https://usefragments.com/schemas/contract.v1.json");
452
+ expect(contract.provenance).toBeDefined();
453
+ });
454
+
455
+ it("falls back to inherited wrapper props for shadcn-style Label primitives", async () => {
456
+ const projectDir = resolve(__dirname, "../__fixtures__/shadcn-label-wrapper");
457
+ const scanDir = join(projectDir, "src", "components", "ui");
458
+
459
+ const result = await scanGenerate({
460
+ scanPath: scanDir,
461
+ force: true,
462
+ tsconfig: join(projectDir, "tsconfig.app.json"),
463
+ });
464
+
465
+ expect(result.success).toBe(true);
336
466
 
337
- // Check propsSummary has entries for local props
338
- expect(content).toContain("variant:");
339
- expect(content).toContain("disabled:");
467
+ const fragment = await rf(join(scanDir, "label.contract.json"), "utf-8");
468
+ const contract = JSON.parse(fragment);
469
+ // Should have extracted inherited props like htmlFor and form
470
+ expect(contract.props).toBeDefined();
471
+ // Props may be in propsSummary or props object
472
+ const allKeys = [
473
+ ...Object.keys(contract.props ?? {}),
474
+ ...(contract.propsSummary ?? []).map((s: string) => s.split(':')[0]),
475
+ ];
476
+ expect(allKeys.some((k: string) => k.includes('htmlFor') || k.includes('form'))).toBe(true);
340
477
  });
341
478
  });
342
479
 
@@ -393,22 +530,16 @@ export function CardFooter({ children }: { children: React.ReactNode }) {
393
530
  expect(result.generated[0].name).toBe("Card");
394
531
 
395
532
  const content = await rf(
396
- join(tmpDir, "components", "Card", "Card.fragment.tsx"),
533
+ join(tmpDir, "components", "Card", "Card.contract.json"),
397
534
  "utf-8"
398
535
  );
399
536
 
400
- // Should have contract with compoundChildren
401
- expect(content).toContain("contract:");
402
- expect(content).toContain("compoundChildren:");
403
- expect(content).toContain("Header:");
404
- expect(content).toContain("Body:");
405
- expect(content).toContain("Footer:");
406
-
407
- // Should have canonicalUsage
408
- expect(content).toContain("canonicalUsage:");
409
- expect(content).toContain("Card.Header");
410
- expect(content).toContain("Card.Body");
411
- expect(content).toContain("Card.Footer");
537
+ const contract = JSON.parse(content);
538
+ // Should have AI metadata with compound composition
539
+ expect(contract.ai).toBeDefined();
540
+ expect(contract.ai.compositionPattern).toBe("compound");
541
+ expect(contract.ai.subComponents).toBeDefined();
542
+ expect(contract.ai.subComponents.length).toBeGreaterThan(0);
412
543
  });
413
544
  });
414
545
 
@@ -697,30 +828,17 @@ export function Badge({ children, variant = 'default' }: BadgeProps) {
697
828
  expect(result.generated[0].enriched).toBe(true);
698
829
 
699
830
  const content = await rf(
700
- join(tmpDir, "components", "Badge", "Badge.fragment.tsx"),
831
+ join(tmpDir, "components", "Badge", "Badge.contract.json"),
701
832
  "utf-8"
702
833
  );
703
834
 
704
- // Enriched usage fields should be present (not TODO markers)
705
- expect(content).toContain("'Showing status counts'");
706
- expect(content).toContain("'Labeling items'");
707
- expect(content).toContain("'Long text content — use a Tag instead'");
708
- expect(content).toContain("guidelines:");
709
- expect(content).toContain("'Keep text short (1-3 words)'");
710
-
711
- // Meta tags should be present
712
- expect(content).toContain("tags:");
713
- expect(content).toContain("'badge'");
714
-
715
- // Contract should have enriched a11yRules and scenarioTags
716
- expect(content).toContain("a11yRules:");
717
- expect(content).toContain("'Use aria-label for icon-only badges'");
718
- expect(content).toContain("scenarioTags:");
719
- expect(content).toContain("'feedback.indicator.badge'");
720
-
721
- // Should NOT have TODO markers for usage.when or usage.whenNot
722
- expect(content).not.toContain("// TODO: Describe when to use Badge");
723
- expect(content).not.toContain("// TODO: Describe when NOT to use Badge");
835
+ const contract = JSON.parse(content);
836
+ // Enriched usage fields should be present
837
+ expect(contract.usage.when.length).toBeGreaterThan(0);
838
+ expect(contract.usage.when).toContain("Showing status counts");
839
+ expect(contract.usage.whenNot).toContain("Long text content — use a Tag instead");
840
+ expect(contract.usage.guidelines).toBeDefined();
841
+ expect(contract.usage.guidelines).toContain("Keep text short (1-3 words)");
724
842
 
725
843
  vi.doUnmock("../../ai-client.js");
726
844
  });
@@ -747,13 +865,14 @@ export function Badge({ children, variant = 'default' }: BadgeProps) {
747
865
  expect(result.success).toBe(true);
748
866
 
749
867
  const content = await rf(
750
- join(tmpDir, "components", "Badge", "Badge.fragment.tsx"),
868
+ join(tmpDir, "components", "Badge", "Badge.contract.json"),
751
869
  "utf-8"
752
870
  );
753
871
 
754
- // Should have TODO markers (no enrichment)
755
- expect(content).toContain("// TODO: Describe when to use Badge");
756
- expect(content).toContain("// TODO: Describe when NOT to use Badge");
872
+ const contract = JSON.parse(content);
873
+ // Without enrichment, usage.when/whenNot should be empty arrays
874
+ expect(contract.usage.when).toEqual([]);
875
+ expect(contract.usage.whenNot).toEqual([]);
757
876
  expect(result.generated[0].enriched).toBeFalsy();
758
877
  expect(result.enrichmentCost).toBeUndefined();
759
878
  });
@@ -0,0 +1,91 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
2
+ import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+
6
+ import { verify } from "../verify.js";
7
+
8
+ describe("verify --ci", () => {
9
+ let tmpDir: string;
10
+
11
+ beforeAll(async () => {
12
+ tmpDir = await mkdtemp(join(tmpdir(), "verify-ci-test-"));
13
+ });
14
+
15
+ afterAll(async () => {
16
+ await rm(tmpDir, { recursive: true, force: true });
17
+ });
18
+
19
+ it("prefers local fragments.json and ignores installed package fragments", async () => {
20
+ const projectDir = join(tmpDir, "project");
21
+ await mkdir(projectDir, { recursive: true });
22
+
23
+ await writeFile(
24
+ join(projectDir, "fragments.config.ts"),
25
+ `export default {
26
+ include: ['src/**/*.fragment.tsx'],
27
+ exclude: [],
28
+ components: ['src/**/*.tsx'],
29
+ framework: 'react',
30
+ outFile: 'fragments.json',
31
+ };
32
+ `
33
+ );
34
+
35
+ await writeFile(
36
+ join(projectDir, "fragments.json"),
37
+ JSON.stringify(
38
+ {
39
+ fragments: {
40
+ LocalButton: {
41
+ meta: {
42
+ name: "LocalButton",
43
+ description: "Local button",
44
+ category: "Actions",
45
+ },
46
+ usage: {
47
+ when: ["Actions"],
48
+ whenNot: [],
49
+ },
50
+ props: {
51
+ variant: { type: "enum", required: false, values: ["primary"] },
52
+ },
53
+ variants: [{ name: "Default" }],
54
+ relations: [],
55
+ filePath: "src/components/LocalButton.fragment.tsx",
56
+ },
57
+ InstalledButton: {
58
+ meta: {
59
+ name: "InstalledButton",
60
+ description: "Installed button",
61
+ category: "Actions",
62
+ },
63
+ usage: {
64
+ when: ["Actions"],
65
+ whenNot: [],
66
+ },
67
+ props: {
68
+ variant: { type: "enum", required: false, values: ["primary"] },
69
+ },
70
+ variants: [{ name: "Default" }],
71
+ relations: [],
72
+ filePath: "node_modules/@fragments-sdk/ui/Button.fragment.tsx",
73
+ },
74
+ },
75
+ },
76
+ null,
77
+ 2
78
+ )
79
+ );
80
+
81
+ const summary = await verify(undefined, {
82
+ ci: true,
83
+ config: join(projectDir, "fragments.config.ts"),
84
+ port: 6553,
85
+ minCompliance: 60,
86
+ });
87
+
88
+ expect(summary.totalComponents).toBe(1);
89
+ expect(summary.results.map((result) => result.component)).toEqual(["LocalButton"]);
90
+ });
91
+ });
@@ -5,7 +5,7 @@
5
5
  import pc from 'picocolors';
6
6
  import { BRAND } from '../core/index.js';
7
7
  import { loadConfig } from '../core/node.js';
8
- import { buildFragments, buildFragmentsDir } from '../build.js';
8
+ import { buildFragments, buildFragmentsDir, getFragmentsJsonStatus } from '@fragments-sdk/compiler';
9
9
  import { scan } from './scan.js';
10
10
 
11
11
  /**
@@ -22,6 +22,10 @@ export interface BuildOptions {
22
22
  registryOnly?: boolean;
23
23
  /** Build from source code (zero-config, no fragment files needed) */
24
24
  fromSource?: boolean;
25
+ /** Skip work when fragments.json is already fresh */
26
+ ifNeeded?: boolean;
27
+ /** Check freshness and exit non-zero if fragments.json needs regeneration */
28
+ check?: boolean;
25
29
  /** Skip usage analysis when building from source */
26
30
  skipUsage?: boolean;
27
31
  /** Skip Storybook parsing when building from source */
@@ -88,6 +92,55 @@ export async function build(options: BuildOptions = {}): Promise<BuildResult> {
88
92
 
89
93
  // Build fragments.json unless --registry-only
90
94
  if (!options.registryOnly) {
95
+ const status = await getFragmentsJsonStatus(config, configDir, {
96
+ output: config.outFile,
97
+ configPath: options.config,
98
+ });
99
+
100
+ if (options.check) {
101
+ if (status.missing || status.stale) {
102
+ const message = status.missing
103
+ ? `${BRAND.outFile} is missing`
104
+ : `${BRAND.outFile} is stale${status.reason ? ` (${status.reason})` : ''}`;
105
+ console.log(pc.red(`✗ ${message}\n`));
106
+ errors.push({
107
+ file: status.outputPath,
108
+ error: message,
109
+ });
110
+ return {
111
+ success: false,
112
+ outputPath: status.outputPath,
113
+ errors,
114
+ };
115
+ }
116
+
117
+ console.log(pc.green(`✓ ${BRAND.outFile} is up to date`));
118
+ console.log(pc.dim(` Output: ${status.outputPath}\n`));
119
+
120
+ if (!options.registry) {
121
+ return {
122
+ success: true,
123
+ fragmentCount: Object.keys(status.data?.fragments ?? {}).length,
124
+ outputPath: status.outputPath,
125
+ errors,
126
+ };
127
+ }
128
+ }
129
+
130
+ if (options.ifNeeded && !status.missing && !status.stale) {
131
+ console.log(pc.green(`✓ ${BRAND.outFile} is up to date`));
132
+ console.log(pc.dim(` Output: ${status.outputPath}\n`));
133
+
134
+ if (!options.registry) {
135
+ return {
136
+ success: true,
137
+ fragmentCount: Object.keys(status.data?.fragments ?? {}).length,
138
+ outputPath: status.outputPath,
139
+ errors,
140
+ };
141
+ }
142
+ }
143
+
91
144
  console.log(pc.dim('Compiling fragments...\n'));
92
145
 
93
146
  const result = await buildFragments(config, configDir);
@@ -8,7 +8,7 @@ import pc from 'picocolors';
8
8
  import { generateContext } from '../core/index.js';
9
9
  import type { CompiledFragment, CompiledFragmentsFile } from '../core/index.js';
10
10
  import { loadConfig } from '../core/node.js';
11
- import { buildFragments } from '../build.js';
11
+ import { buildFragments } from '@fragments-sdk/compiler';
12
12
 
13
13
  /**
14
14
  * Options for context command