@fragments-sdk/cli 0.14.2 → 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
@@ -266,6 +266,7 @@ export async function enhance(options: EnhanceOptions = {}): Promise<EnhanceResu
266
266
  if (fs.existsSync(srcPath)) {
267
267
  const extraction = await extractPropsFromFile(srcPath, {
268
268
  propsTypeName: `${compName}Props`,
269
+ componentName: compName,
269
270
  });
270
271
  if (extraction.success) {
271
272
  propsExtractions.set(compName, extraction);
@@ -763,7 +764,8 @@ function calculateCost(provider: AIProvider, tokens: number): number {
763
764
  */
764
765
  async function findFragmentFiles(dir: string): Promise<string[]> {
765
766
  const fg = await import('fast-glob');
766
- return fg.default(['**/*.fragment.tsx', '**/*.fragment.ts'], {
767
+ // Search for both .contract.json (preferred) and legacy .fragment.tsx files
768
+ return fg.default(['**/*.contract.json', '**/*.fragment.tsx', '**/*.fragment.ts'], {
767
769
  cwd: dir,
768
770
  absolute: true,
769
771
  ignore: ['**/node_modules/**', '**/dist/**'],
@@ -0,0 +1,386 @@
1
+ /**
2
+ * govern scan / govern watch — Zero-config governance scanning
3
+ *
4
+ * Parses real JSX/TSX files via the existing codebase scanner, converts
5
+ * component usages to UISpec, and runs governance checks per file.
6
+ * Optionally submits results to Fragments Cloud.
7
+ */
8
+
9
+ import pc from 'picocolors';
10
+ import { resolve, relative } from 'node:path';
11
+ import { existsSync } from 'node:fs';
12
+ import { BRAND } from '../core/index.js';
13
+ import type { ComponentUsage } from '../service/enhance/types.js';
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Options
17
+ // ---------------------------------------------------------------------------
18
+
19
+ export interface GovernScanOptions {
20
+ /** Root directory to scan (default: auto-detect) */
21
+ dir?: string;
22
+ /** Path to govern.config.ts */
23
+ config?: string;
24
+ /** Output format */
25
+ format?: 'summary' | 'json' | 'sarif';
26
+ /** Suppress non-error output */
27
+ quiet?: boolean;
28
+ }
29
+
30
+ export interface GovernWatchOptions extends GovernScanOptions {
31
+ /** Debounce interval in ms (default: 300) */
32
+ debounce?: number;
33
+ }
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Scan defaults — applied when no config file exists
37
+ // ---------------------------------------------------------------------------
38
+
39
+ const SCAN_DEFAULT_RULES: Record<string, boolean | object> = {
40
+ 'safety/no-dangerous-html': true,
41
+ 'safety/sanitize-hrefs': true,
42
+ 'safety/no-inline-scripts': true,
43
+ 'safety/no-exposed-secrets': true,
44
+ 'tokens/require-design-tokens': true,
45
+ };
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Helpers
49
+ // ---------------------------------------------------------------------------
50
+
51
+ /**
52
+ * Auto-detect root directory by looking for common React project dirs
53
+ */
54
+ function detectRootDir(cwd: string): string {
55
+ const candidates = ['src', 'app', 'pages', 'components'];
56
+ for (const dir of candidates) {
57
+ if (existsSync(resolve(cwd, dir))) {
58
+ return cwd;
59
+ }
60
+ }
61
+ return cwd;
62
+ }
63
+
64
+ /**
65
+ * Group component usages by their source file
66
+ */
67
+ function groupByFile(usages: ComponentUsage[]): Map<string, ComponentUsage[]> {
68
+ const grouped = new Map<string, ComponentUsage[]>();
69
+ for (const usage of usages) {
70
+ const existing = grouped.get(usage.filePath);
71
+ if (existing) {
72
+ existing.push(usage);
73
+ } else {
74
+ grouped.set(usage.filePath, [usage]);
75
+ }
76
+ }
77
+ return grouped;
78
+ }
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // governScan
82
+ // ---------------------------------------------------------------------------
83
+
84
+ export async function governScan(
85
+ options: GovernScanOptions = {},
86
+ ): Promise<{ exitCode: number }> {
87
+ const {
88
+ loadPolicy,
89
+ createEngine,
90
+ buildAdaptersFromConfig,
91
+ createCloudAdapter,
92
+ formatVerdict,
93
+ } = await import('@fragments-sdk/govern');
94
+
95
+ const { scanCodebase } = await import(
96
+ '../service/enhance/codebase-scanner.js'
97
+ );
98
+ const { usagesToSpec } = await import(
99
+ '../service/enhance/converter.js'
100
+ );
101
+
102
+ const format = options.format ?? 'summary';
103
+ const quiet = options.quiet ?? false;
104
+
105
+ if (!quiet) {
106
+ console.log(pc.cyan(`\n${BRAND.name} Governance Scan\n`));
107
+ }
108
+
109
+ // 1. Resolve root directory
110
+ const rootDir = resolve(options.dir ?? detectRootDir(process.cwd()));
111
+ if (!quiet) {
112
+ console.log(pc.dim(` Root: ${rootDir}\n`));
113
+ }
114
+
115
+ // 2. Load policy — use scan defaults if no config exists
116
+ let policy = await loadPolicy(options.config);
117
+ const hasRules = Object.keys(policy.rules).length > 0;
118
+
119
+ if (!hasRules) {
120
+ policy = { ...policy, rules: SCAN_DEFAULT_RULES };
121
+ if (!quiet) {
122
+ console.log(pc.dim(' No config found — using scan defaults (safety + tokens)\n'));
123
+ }
124
+ }
125
+
126
+ // 3. Build adapters. Auto-add cloud if FRAGMENTS_API_KEY is set
127
+ const adapters = buildAdaptersFromConfig(policy.audit);
128
+ const hasCloudAdapter = adapters.length > 0 && policy.audit?.cloud;
129
+ if (!hasCloudAdapter && process.env.FRAGMENTS_API_KEY) {
130
+ adapters.push(createCloudAdapter());
131
+ if (!quiet) {
132
+ console.log(pc.dim(' Cloud audit enabled (FRAGMENTS_API_KEY detected)\n'));
133
+ }
134
+ }
135
+
136
+ // 4. Create engine
137
+ const engine = createEngine(policy, adapters);
138
+
139
+ // 5. Scan codebase
140
+ if (!quiet) {
141
+ console.log(pc.dim(' Scanning files...\n'));
142
+ }
143
+
144
+ const analysis = await scanCodebase({
145
+ rootDir,
146
+ useCache: true,
147
+ onProgress: quiet
148
+ ? undefined
149
+ : (progress) => {
150
+ if (progress.phase === 'scanning') {
151
+ process.stdout.write(
152
+ `\r ${pc.dim(`[${progress.current}/${progress.total}]`)} ${pc.dim(relative(rootDir, progress.currentFile))}`,
153
+ );
154
+ }
155
+ },
156
+ });
157
+
158
+ if (!quiet) {
159
+ // Clear progress line
160
+ process.stdout.write('\r' + ' '.repeat(80) + '\r');
161
+ console.log(
162
+ pc.dim(` Scanned ${analysis.totalFiles} files, found ${analysis.totalComponents} component types\n`),
163
+ );
164
+ }
165
+
166
+ // 6. Collect all usages across components
167
+ const allUsages: ComponentUsage[] = [];
168
+ for (const comp of Object.values(analysis.components)) {
169
+ allUsages.push(...comp.usages);
170
+ }
171
+
172
+ if (allUsages.length === 0) {
173
+ if (!quiet) {
174
+ console.log(pc.yellow(' No component usages found.\n'));
175
+ }
176
+ return { exitCode: 0 };
177
+ }
178
+
179
+ // 7. Group by file and run checks
180
+ const grouped = groupByFile(allUsages);
181
+ let totalFiles = 0;
182
+ let passedFiles = 0;
183
+ let totalViolations = 0;
184
+ const violationCounts = new Map<string, number>();
185
+
186
+ for (const [filePath, usages] of grouped) {
187
+ const spec = usagesToSpec(usages, filePath, rootDir);
188
+ const relPath = relative(rootDir, filePath);
189
+
190
+ const verdict = await engine.check(spec, {
191
+ runner: 'cli',
192
+ input: relPath,
193
+ });
194
+
195
+ totalFiles++;
196
+
197
+ if (verdict.passed) {
198
+ passedFiles++;
199
+ } else {
200
+ if (!quiet) {
201
+ console.log(pc.red(` ✗ ${relPath}`));
202
+ if (format === 'summary') {
203
+ for (const result of verdict.results) {
204
+ for (const v of result.violations) {
205
+ const count = violationCounts.get(v.rule) ?? 0;
206
+ violationCounts.set(v.rule, count + 1);
207
+ totalViolations++;
208
+ console.log(
209
+ pc.dim(` ${v.severity} `) +
210
+ pc.yellow(v.rule) +
211
+ pc.dim(` — ${v.message}`),
212
+ );
213
+ if (v.nodeId) {
214
+ console.log(pc.dim(` at ${v.nodeId}`));
215
+ }
216
+ }
217
+ }
218
+ }
219
+ }
220
+ }
221
+
222
+ if (verdict.passed && !quiet && format === 'summary') {
223
+ console.log(pc.green(` ✓ ${relPath}`) + pc.dim(` (${usages.length} components, score: ${verdict.score}/100)`));
224
+ }
225
+
226
+ // JSON/SARIF: print per-file
227
+ if (format === 'json' || format === 'sarif') {
228
+ const output = formatVerdict(verdict, format);
229
+ console.log(output);
230
+ }
231
+ }
232
+
233
+ // 8. Summary
234
+ if (!quiet && format === 'summary') {
235
+ console.log(pc.dim('\n ─────────────────────────────────────\n'));
236
+ console.log(` Files checked: ${totalFiles}`);
237
+ console.log(` Passed: ${passedFiles}/${totalFiles}`);
238
+ console.log(` Violations: ${totalViolations}`);
239
+
240
+ if (violationCounts.size > 0) {
241
+ console.log(pc.dim('\n Top violations:'));
242
+ const sorted = [...violationCounts.entries()].sort((a, b) => b[1] - a[1]);
243
+ for (const [rule, count] of sorted.slice(0, 5)) {
244
+ console.log(pc.dim(` ${count}× `) + pc.yellow(rule));
245
+ }
246
+ }
247
+
248
+ console.log();
249
+
250
+ if (passedFiles === totalFiles) {
251
+ console.log(pc.green(` ✓ All files passed governance checks\n`));
252
+ } else {
253
+ console.log(
254
+ pc.red(` ✗ ${totalFiles - passedFiles} file(s) failed governance checks\n`),
255
+ );
256
+ }
257
+ }
258
+
259
+ return { exitCode: passedFiles === totalFiles ? 0 : 1 };
260
+ }
261
+
262
+ // ---------------------------------------------------------------------------
263
+ // governWatch
264
+ // ---------------------------------------------------------------------------
265
+
266
+ export async function governWatch(
267
+ options: GovernWatchOptions = {},
268
+ ): Promise<void> {
269
+ const {
270
+ loadPolicy,
271
+ createEngine,
272
+ buildAdaptersFromConfig,
273
+ createCloudAdapter,
274
+ formatVerdict,
275
+ } = await import('@fragments-sdk/govern');
276
+
277
+ const { scanFile } = await import('../service/enhance/scanner.js');
278
+ const { usagesToSpec } = await import(
279
+ '../service/enhance/converter.js'
280
+ );
281
+
282
+ const quiet = options.quiet ?? false;
283
+ const debounceMs = options.debounce ?? 300;
284
+ const format = options.format ?? 'summary';
285
+
286
+ // 1. Run initial scan
287
+ console.log(pc.cyan(`\n${BRAND.name} Governance Watch\n`));
288
+
289
+ const { exitCode } = await governScan(options);
290
+ if (!quiet) {
291
+ console.log(
292
+ pc.dim(` Initial scan ${exitCode === 0 ? 'passed' : 'completed with violations'}\n`),
293
+ );
294
+ }
295
+
296
+ // 2. Set up engine for incremental checks
297
+ const rootDir = resolve(options.dir ?? detectRootDir(process.cwd()));
298
+ let policy = await loadPolicy(options.config);
299
+ if (Object.keys(policy.rules).length === 0) {
300
+ policy = { ...policy, rules: SCAN_DEFAULT_RULES };
301
+ }
302
+ const adapters = buildAdaptersFromConfig(policy.audit);
303
+ if (!adapters.some(() => policy.audit?.cloud) && process.env.FRAGMENTS_API_KEY) {
304
+ adapters.push(createCloudAdapter());
305
+ }
306
+ const engine = createEngine(policy, adapters);
307
+
308
+ // 3. Watch for changes
309
+ console.log(pc.dim(' Watching for changes... (Ctrl+C to stop)\n'));
310
+
311
+ const chokidar = await import('chokidar');
312
+
313
+ const watcher = chokidar.watch(
314
+ ['**/*.tsx', '**/*.ts', '**/*.jsx', '**/*.js'],
315
+ {
316
+ cwd: rootDir,
317
+ ignoreInitial: true,
318
+ ignored: [
319
+ '**/node_modules/**',
320
+ '**/dist/**',
321
+ '**/build/**',
322
+ '**/.next/**',
323
+ '**/*.test.*',
324
+ '**/*.spec.*',
325
+ '**/*.stories.*',
326
+ ],
327
+ awaitWriteFinish: { stabilityThreshold: debounceMs },
328
+ },
329
+ );
330
+
331
+ const handleChange = async (changedRelPath: string) => {
332
+ const absolutePath = resolve(rootDir, changedRelPath);
333
+
334
+ try {
335
+ const { usages } = await scanFile(absolutePath);
336
+
337
+ if (usages.length === 0) {
338
+ if (!quiet) {
339
+ console.log(pc.dim(` ○ ${changedRelPath} — no component usages`));
340
+ }
341
+ return;
342
+ }
343
+
344
+ const spec = usagesToSpec(usages, absolutePath, rootDir);
345
+ const verdict = await engine.check(spec, {
346
+ runner: 'cli',
347
+ input: changedRelPath,
348
+ });
349
+
350
+ if (verdict.passed) {
351
+ console.log(
352
+ pc.green(` ✓ ${changedRelPath}`) +
353
+ pc.dim(` (${usages.length} components, score: ${verdict.score}/100)`),
354
+ );
355
+ } else {
356
+ console.log(pc.red(` ✗ ${changedRelPath}`));
357
+ if (format === 'summary') {
358
+ for (const result of verdict.results) {
359
+ for (const v of result.violations) {
360
+ console.log(
361
+ pc.dim(` ${v.severity} `) +
362
+ pc.yellow(v.rule) +
363
+ pc.dim(` — ${v.message}`),
364
+ );
365
+ }
366
+ }
367
+ } else {
368
+ console.log(formatVerdict(verdict, format));
369
+ }
370
+ }
371
+ } catch (error) {
372
+ if (!quiet) {
373
+ console.log(
374
+ pc.dim(` ⚠ ${changedRelPath} — `) +
375
+ pc.yellow(error instanceof Error ? error.message : 'parse error'),
376
+ );
377
+ }
378
+ }
379
+ };
380
+
381
+ watcher.on('change', handleChange);
382
+ watcher.on('add', handleChange);
383
+
384
+ // Keep process alive
385
+ await new Promise(() => {});
386
+ }
@@ -95,7 +95,7 @@ export async function governConnect(): Promise<void> {
95
95
  // ── Step 1: Get API key ──────────────────────────────────────────────────
96
96
  console.log(pc.bold(' Step 1 of 3: Get your API key\n'));
97
97
 
98
- const dashboardUrl = `${cloudUrl}/dashboard/settings`;
98
+ const dashboardUrl = `${cloudUrl}/api-keys`;
99
99
  console.log(pc.dim(` → Opening the dashboard in your browser...`));
100
100
  console.log(pc.dim(` Copy your API key from Settings → API Keys\n`));
101
101
 
@@ -226,7 +226,7 @@ export async function governConnect(): Promise<void> {
226
226
  // ── Done ────────────────────────────────────────────────────────────────
227
227
  console.log(pc.dim('\n ─────────────────────────────────────\n'));
228
228
  console.log(pc.green(' ✓ All set!') + ' Run `fragments govern check` to send your first audit.\n');
229
- console.log(pc.dim(` Dashboard: ${cloudUrl}/dashboard\n`));
229
+ console.log(pc.dim(` Dashboard: ${cloudUrl}/overview\n`));
230
230
  }
231
231
 
232
232
  // ---------------------------------------------------------------------------
@@ -20,6 +20,49 @@ import {
20
20
  addTranspilePackages,
21
21
  } from "./setup.js";
22
22
 
23
+ /**
24
+ * Detect existing UI component libraries by reading package.json.
25
+ * Returns the library display name or null if none found.
26
+ */
27
+ async function detectExistingUILibrary(projectRoot: string): Promise<string | null> {
28
+ let pkg: Record<string, unknown>;
29
+ try {
30
+ const raw = await readFile(join(projectRoot, "package.json"), "utf-8");
31
+ pkg = JSON.parse(raw);
32
+ } catch {
33
+ return null;
34
+ }
35
+
36
+ const allDeps: Record<string, string> = {
37
+ ...(pkg.dependencies as Record<string, string> || {}),
38
+ ...(pkg.devDependencies as Record<string, string> || {}),
39
+ };
40
+
41
+ // shadcn/ui: tailwindcss present + components/ui/ directory
42
+ if (allDeps["tailwindcss"]) {
43
+ const shadcnFiles = await fg(["**/components/ui/*.tsx", "**/components/ui/*.ts"], {
44
+ cwd: projectRoot,
45
+ ignore: ["**/node_modules/**"],
46
+ });
47
+ if (shadcnFiles.length > 0) {
48
+ return "shadcn/ui";
49
+ }
50
+ }
51
+
52
+ if (allDeps["@mui/material"]) return "Material UI";
53
+ if (allDeps["@chakra-ui/react"]) return "Chakra UI";
54
+ if (allDeps["@mantine/core"]) return "Mantine";
55
+ if (allDeps["antd"]) return "Ant Design";
56
+
57
+ // Radix UI: has @radix-ui/react-* packages but NOT @fragments-sdk/ui
58
+ const hasRadix = Object.keys(allDeps).some((dep) => dep.startsWith("@radix-ui/react-"));
59
+ if (hasRadix && !allDeps["@fragments-sdk/ui"]) {
60
+ return "Radix UI";
61
+ }
62
+
63
+ return null;
64
+ }
65
+
23
66
  export interface InitOptions {
24
67
  /** Project root directory */
25
68
  projectRoot?: string;
@@ -43,6 +86,10 @@ export interface InitOptions {
43
86
  apiKey?: string;
44
87
  /** Override AI model for enrichment */
45
88
  model?: string;
89
+ /** Generate metadata/governance files without injecting runtime UI */
90
+ metadataOnly?: boolean;
91
+ /** Alias for metadataOnly */
92
+ govern?: boolean;
46
93
  }
47
94
 
48
95
  export interface InitResult {
@@ -342,6 +389,49 @@ export default defineFragment({
342
389
  `;
343
390
  }
344
391
 
392
+ function generateExampleContract(): string {
393
+ return JSON.stringify({
394
+ $schema: 'https://usefragments.com/schemas/contract.v1.json',
395
+ name: 'Button',
396
+ description: 'Interactive button for triggering actions',
397
+ category: 'Actions',
398
+ status: 'stable',
399
+ sourcePath: 'src/components/Button.tsx',
400
+ exportName: 'Button',
401
+ propsSummary: [
402
+ 'variant: primary|secondary|ghost (default: primary)',
403
+ 'size: sm|md|lg (default: md)',
404
+ 'children: node (required)',
405
+ ],
406
+ props: {
407
+ children: { type: 'node', required: true, description: 'Button label content' },
408
+ variant: {
409
+ type: 'enum',
410
+ values: ['primary', 'secondary', 'ghost'],
411
+ default: 'primary',
412
+ description: 'Visual style variant',
413
+ },
414
+ size: {
415
+ type: 'enum',
416
+ values: ['sm', 'md', 'lg'],
417
+ default: 'md',
418
+ description: 'Button size',
419
+ },
420
+ },
421
+ usage: {
422
+ when: ['Triggering an action (save, submit, delete)', 'Form submission', 'Opening dialogs or menus'],
423
+ whenNot: ['Simple navigation (use Link)', 'Toggling state (use Switch)'],
424
+ guidelines: ['Use Primary for the main action in a context', 'Only one Primary button per section'],
425
+ },
426
+ examples: [
427
+ { name: 'Primary', description: 'Default action button', code: '<Button variant="primary">Save Changes</Button>' },
428
+ { name: 'Secondary', description: 'Less prominent action', code: '<Button variant="secondary">Cancel</Button>' },
429
+ { name: 'Ghost', description: 'Minimal visual weight', code: '<Button variant="ghost">Learn More</Button>' },
430
+ ],
431
+ provenance: { source: 'manual', verified: false },
432
+ }, null, 2) + '\n';
433
+ }
434
+
345
435
  /**
346
436
  * Start the dev server
347
437
  */
@@ -539,7 +629,7 @@ export async function init(options: InitOptions = {}): Promise<InitResult> {
539
629
  const relScanPath = relative(projectRoot, scanPath);
540
630
  const configPath = join(projectRoot, BRAND.configFile);
541
631
  const configContent = generateConfig({
542
- includePaths: [`${relScanPath}/**/*.fragment.tsx`],
632
+ includePaths: [`${relScanPath}/**/*.contract.json`],
543
633
  componentPaths: [`${relScanPath}/**/*.tsx`],
544
634
  framework: "react",
545
635
  });
@@ -642,7 +732,7 @@ export async function init(options: InitOptions = {}): Promise<InitResult> {
642
732
 
643
733
  // Step 5: Create configuration file
644
734
  const includePaths: string[] = [
645
- `${componentPath}/**/*.fragment.tsx`,
735
+ `${componentPath}/**/*.contract.json`,
646
736
  ];
647
737
 
648
738
  if (scenario === 'stories') {
@@ -667,33 +757,62 @@ export async function init(options: InitOptions = {}): Promise<InitResult> {
667
757
  }
668
758
 
669
759
  // Step 6: Auto-inject styles + framework config
670
- const entryFile = await findEntryFile(projectRoot, framework);
760
+ // Detect existing UI libraries — skip @fragments-sdk/ui runtime if one is found
761
+ const detectedUILib = await detectExistingUILibrary(projectRoot);
671
762
 
672
- if (entryFile) {
673
- try {
674
- const stylesResult = await addStylesImport(projectRoot, entryFile);
675
- if (stylesResult.modified) {
676
- console.log(pc.green(` ✓ Added styles import to ${entryFile}`));
677
- } else {
678
- console.log(pc.dim(` · ${stylesResult.message}`));
679
- }
680
- } catch (e) {
681
- errors.push(`Failed to add styles import: ${e instanceof Error ? e.message : e}`);
763
+ // Check if @fragments-sdk/ui is already a dependency
764
+ let hasFragmentsUI = false;
765
+ try {
766
+ const pkgRaw = await readFile(join(projectRoot, "package.json"), "utf-8");
767
+ const pkgJson = JSON.parse(pkgRaw);
768
+ const allDeps = {
769
+ ...(pkgJson.dependencies || {}),
770
+ ...(pkgJson.devDependencies || {}),
771
+ };
772
+ hasFragmentsUI = !!allDeps["@fragments-sdk/ui"];
773
+ } catch {
774
+ // no package.json — can't determine, default to injecting
775
+ }
776
+
777
+ const explicitMetadataOnly = !!options.metadataOnly || !!options.govern;
778
+ const skipUIRuntime = explicitMetadataOnly || (detectedUILib !== null && !hasFragmentsUI);
779
+
780
+ if (skipUIRuntime) {
781
+ if (explicitMetadataOnly) {
782
+ console.log(pc.dim(` · Metadata-only mode — skipping @fragments-sdk/ui runtime setup`));
783
+ } else {
784
+ console.log(pc.dim(` · Detected ${detectedUILib} — skipping @fragments-sdk/ui runtime setup`));
682
785
  }
786
+ console.log(pc.dim(` · Run '${BRAND.cliCommand} setup' if you want to add @fragments-sdk/ui later`));
787
+ } else {
788
+ const entryFile = await findEntryFile(projectRoot, framework);
789
+
790
+ if (entryFile) {
791
+ try {
792
+ const stylesResult = await addStylesImport(projectRoot, entryFile);
793
+ if (stylesResult.modified) {
794
+ console.log(pc.green(` ✓ Added styles import to ${entryFile}`));
795
+ } else {
796
+ console.log(pc.dim(` · ${stylesResult.message}`));
797
+ }
798
+ } catch (e) {
799
+ errors.push(`Failed to add styles import: ${e instanceof Error ? e.message : e}`);
800
+ }
683
801
 
684
- try {
685
- const providerResult = await addThemeProvider(projectRoot, entryFile, framework);
686
- if (providerResult.modified) {
687
- console.log(pc.green(` ✓ Added ThemeProvider to ${entryFile}`));
688
- } else {
689
- console.log(pc.dim(` · ${providerResult.message}`));
802
+ try {
803
+ const providerResult = await addThemeProvider(projectRoot, entryFile, framework);
804
+ if (providerResult.modified) {
805
+ console.log(pc.green(` ✓ Added ThemeProvider to ${entryFile}`));
806
+ } else {
807
+ console.log(pc.dim(` · ${providerResult.message}`));
808
+ }
809
+ } catch (e) {
810
+ errors.push(`Failed to add ThemeProvider: ${e instanceof Error ? e.message : e}`);
690
811
  }
691
- } catch (e) {
692
- errors.push(`Failed to add ThemeProvider: ${e instanceof Error ? e.message : e}`);
812
+ } else {
813
+ console.log(pc.yellow(` ! Could not detect entry file add styles import manually`));
814
+ console.log(pc.dim(` import '@fragments-sdk/ui/styles'`));
693
815
  }
694
- } else {
695
- console.log(pc.yellow(` ! Could not detect entry file — add styles import manually`));
696
- console.log(pc.dim(` import '@fragments-sdk/ui/styles'`));
697
816
  }
698
817
 
699
818
  // Next.js: add transpilePackages
@@ -727,13 +846,13 @@ export async function init(options: InitOptions = {}): Promise<InitResult> {
727
846
  );
728
847
 
729
848
  await writeFile(
730
- join(exampleDir, "Button.fragment.tsx"),
731
- generateExampleFragment(),
849
+ join(exampleDir, "Button.contract.json"),
850
+ generateExampleContract(),
732
851
  "utf-8"
733
852
  );
734
853
  console.log(
735
854
  pc.green(
736
- ` ✓ Created ${relative(projectRoot, join(exampleDir, "Button.fragment.tsx"))}`
855
+ ` ✓ Created ${relative(projectRoot, join(exampleDir, "Button.contract.json"))}`
737
856
  )
738
857
  );
739
858
  } catch (e) {
@@ -775,9 +894,14 @@ export async function init(options: InitOptions = {}): Promise<InitResult> {
775
894
 
776
895
  if (startServer) {
777
896
  startDevServer(projectRoot);
897
+ } else if (skipUIRuntime) {
898
+ console.log(` ${pc.bold("Get started:")}`);
899
+ console.log(` ${pc.dim("$")} ${BRAND.cliCommand} build`);
900
+ console.log(` ${pc.dim("$")} ${BRAND.cliCommand} verify --ci`);
901
+ console.log();
778
902
  } else {
779
903
  console.log(` ${pc.bold("Get started:")}`);
780
- console.log(` ${pc.dim("$")} ${BRAND.cliCommand} dev`);
904
+ console.log(` ${pc.dim("$")} ${BRAND.cliCommand} build`);
781
905
  console.log();
782
906
 
783
907
  if (!options.configure) {