@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
@@ -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,565 @@
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
+ computeComponentHealth,
94
+ } = await import('@fragments-sdk/govern');
95
+
96
+ const { scanCodebase } = await import(
97
+ '../service/enhance/codebase-scanner.js'
98
+ );
99
+ const { usagesToSpec } = await import(
100
+ '../service/enhance/converter.js'
101
+ );
102
+
103
+ const format = options.format ?? 'summary';
104
+ const quiet = options.quiet ?? false;
105
+
106
+ if (!quiet) {
107
+ console.log(pc.cyan(`\n${BRAND.name} Governance Scan\n`));
108
+ }
109
+
110
+ // 1. Resolve root directory
111
+ const rootDir = resolve(options.dir ?? detectRootDir(process.cwd()));
112
+ if (!quiet) {
113
+ console.log(pc.dim(` Root: ${rootDir}\n`));
114
+ }
115
+
116
+ // 2. Load policy — use scan defaults if no config exists
117
+ let policy = await loadPolicy(options.config);
118
+ const hasRules = Object.keys(policy.rules).length > 0;
119
+
120
+ if (!hasRules) {
121
+ policy = { ...policy, rules: SCAN_DEFAULT_RULES };
122
+ if (!quiet) {
123
+ console.log(pc.dim(' No config found — using scan defaults (safety + tokens)\n'));
124
+ }
125
+ }
126
+
127
+ // 3. Extract code tokens for cloud ingest
128
+ let codeTokens: string | undefined;
129
+ if (process.env.FRAGMENTS_API_KEY) {
130
+ codeTokens = await extractCodeTokens(rootDir, options.config, quiet);
131
+ }
132
+
133
+ // 3b. Load contract registry for governance + cloud ingest (if fragments.json exists)
134
+ let contractRegistry: string | undefined;
135
+ let registryMap: Record<string, unknown> | undefined;
136
+ {
137
+ const { readFileSync, existsSync } = await import('node:fs');
138
+ const fragmentsJsonPath = resolve(rootDir, 'fragments.json');
139
+ if (existsSync(fragmentsJsonPath)) {
140
+ try {
141
+ const raw = readFileSync(fragmentsJsonPath, 'utf-8');
142
+ const parsed = JSON.parse(raw);
143
+ if (parsed.fragments && Array.isArray(parsed.fragments)) {
144
+ // Build name-keyed map for engine registry injection
145
+ const map: Record<string, unknown> = {};
146
+ for (const f of parsed.fragments) {
147
+ if (f.meta?.name) {
148
+ map[f.meta.name] = f;
149
+ }
150
+ }
151
+ registryMap = map;
152
+
153
+ if (process.env.FRAGMENTS_API_KEY) {
154
+ contractRegistry = JSON.stringify({ fragments: parsed.fragments });
155
+ }
156
+ if (!quiet) {
157
+ console.log(pc.dim(` Contract registry loaded (${parsed.fragments.length} components)\n`));
158
+ }
159
+ }
160
+ } catch {
161
+ // Invalid fragments.json — skip
162
+ }
163
+ }
164
+ }
165
+
166
+ // 4. Build adapters. Auto-add cloud if FRAGMENTS_API_KEY is set
167
+ const adapters = buildAdaptersFromConfig(policy.audit);
168
+ const hasCloudAdapter = adapters.length > 0 && policy.audit?.cloud;
169
+ if (!hasCloudAdapter && process.env.FRAGMENTS_API_KEY) {
170
+ adapters.push(createCloudAdapter({ codeTokens, contractRegistry }));
171
+ if (!quiet) {
172
+ console.log(pc.dim(' Cloud audit enabled (FRAGMENTS_API_KEY detected)\n'));
173
+ }
174
+ }
175
+
176
+ // 5. Create engine (with registry for contract-aware validators)
177
+ const engine = createEngine(
178
+ policy,
179
+ adapters,
180
+ registryMap
181
+ ? { registry: { fragments: registryMap as Record<string, Record<string, unknown>> } }
182
+ : undefined,
183
+ );
184
+
185
+ // 6. Scan codebase
186
+ if (!quiet) {
187
+ console.log(pc.dim(' Scanning files...\n'));
188
+ }
189
+
190
+ const analysis = await scanCodebase({
191
+ rootDir,
192
+ useCache: true,
193
+ onProgress: quiet
194
+ ? undefined
195
+ : (progress) => {
196
+ if (progress.phase === 'scanning') {
197
+ process.stdout.write(
198
+ `\r ${pc.dim(`[${progress.current}/${progress.total}]`)} ${pc.dim(relative(rootDir, progress.currentFile))}`,
199
+ );
200
+ }
201
+ },
202
+ });
203
+
204
+ if (!quiet) {
205
+ // Clear progress line
206
+ process.stdout.write('\r' + ' '.repeat(80) + '\r');
207
+ console.log(
208
+ pc.dim(` Scanned ${analysis.totalFiles} files, found ${analysis.totalComponents} component types\n`),
209
+ );
210
+ }
211
+
212
+ // 7. Collect all usages across components
213
+ const allUsages: ComponentUsage[] = [];
214
+ for (const comp of Object.values(analysis.components)) {
215
+ allUsages.push(...comp.usages);
216
+ }
217
+
218
+ if (allUsages.length === 0) {
219
+ if (!quiet) {
220
+ console.log(pc.yellow(' No component usages found.\n'));
221
+ }
222
+ return { exitCode: 0 };
223
+ }
224
+
225
+ // 8. Group by file and run checks
226
+ const grouped = groupByFile(allUsages);
227
+ let totalFiles = 0;
228
+ let passedFiles = 0;
229
+ let totalViolations = 0;
230
+ const violationCounts = new Map<string, number>();
231
+ const allVerdicts: Awaited<ReturnType<typeof engine.check>>[] = [];
232
+
233
+ // Build per-file usage snapshot for cloud
234
+ const usageSnapshot: Array<{
235
+ file: string;
236
+ components: Array<{
237
+ name: string;
238
+ line: number;
239
+ props: { static: Record<string, unknown>; dynamic: string[] };
240
+ }>;
241
+ }> = [];
242
+
243
+ for (const [filePath, usages] of grouped) {
244
+ const spec = usagesToSpec(usages, filePath, rootDir);
245
+ const relPath = relative(rootDir, filePath);
246
+
247
+ const verdict = await engine.check(spec, {
248
+ runner: 'cli',
249
+ input: relPath,
250
+ });
251
+ allVerdicts.push(verdict);
252
+
253
+ // Collect per-file usage snapshot
254
+ usageSnapshot.push({
255
+ file: relPath,
256
+ components: usages.map((u) => ({
257
+ name: u.componentName,
258
+ line: u.line,
259
+ props: {
260
+ static: u.props.static,
261
+ dynamic: u.props.dynamic,
262
+ },
263
+ })),
264
+ });
265
+
266
+ totalFiles++;
267
+
268
+ if (verdict.passed) {
269
+ passedFiles++;
270
+ } else {
271
+ if (!quiet) {
272
+ console.log(pc.red(` ✗ ${relPath}`));
273
+ if (format === 'summary') {
274
+ for (const result of verdict.results) {
275
+ for (const v of result.violations) {
276
+ const count = violationCounts.get(v.rule) ?? 0;
277
+ violationCounts.set(v.rule, count + 1);
278
+ totalViolations++;
279
+ console.log(
280
+ pc.dim(` ${v.severity} `) +
281
+ pc.yellow(v.rule) +
282
+ pc.dim(` — ${v.message}`),
283
+ );
284
+ if (v.nodeId) {
285
+ console.log(pc.dim(` at ${v.nodeId}`));
286
+ }
287
+ }
288
+ }
289
+ }
290
+ }
291
+ }
292
+
293
+ if (verdict.passed && !quiet && format === 'summary') {
294
+ console.log(pc.green(` ✓ ${relPath}`) + pc.dim(` (${usages.length} components, score: ${verdict.score}/100)`));
295
+ }
296
+
297
+ // JSON/SARIF: print per-file
298
+ if (format === 'json' || format === 'sarif') {
299
+ const output = formatVerdict(verdict, format);
300
+ console.log(output);
301
+ }
302
+ }
303
+
304
+ // 8b. Compute component health
305
+ const health = computeComponentHealth(allVerdicts, registryMap ?? {});
306
+
307
+ // 9. Summary
308
+ if (!quiet && format === 'summary') {
309
+ console.log(pc.dim('\n ─────────────────────────────────────\n'));
310
+ console.log(` Files checked: ${totalFiles}`);
311
+ console.log(` Passed: ${passedFiles}/${totalFiles}`);
312
+ console.log(` Violations: ${totalViolations}`);
313
+
314
+ if (violationCounts.size > 0) {
315
+ console.log(pc.dim('\n Top violations:'));
316
+ const sorted = [...violationCounts.entries()].sort((a, b) => b[1] - a[1]);
317
+ for (const [rule, count] of sorted.slice(0, 5)) {
318
+ console.log(pc.dim(` ${count}× `) + pc.yellow(rule));
319
+ }
320
+ }
321
+
322
+ // Component health
323
+ console.log(pc.dim('\n Component Health:'));
324
+ console.log(` Contract coverage: ${health.contractCoverage}% (${health.contractedComponents}/${health.totalComponents})`);
325
+ console.log(` Compliance rate: ${health.overallCompliance}%`);
326
+
327
+ if (health.uncontracted.length > 0) {
328
+ console.log(pc.dim(` Uncontracted: ${health.uncontracted.slice(0, 5).join(', ')}${health.uncontracted.length > 5 ? ` (+${health.uncontracted.length - 5} more)` : ''}`));
329
+ }
330
+
331
+ console.log();
332
+
333
+ if (passedFiles === totalFiles) {
334
+ console.log(pc.green(` ✓ All files passed governance checks\n`));
335
+ } else {
336
+ console.log(
337
+ pc.red(` ✗ ${totalFiles - passedFiles} file(s) failed governance checks\n`),
338
+ );
339
+ }
340
+ }
341
+
342
+ // 10. Push component usage snapshot + health to Cloud (if API key set)
343
+ if (process.env.FRAGMENTS_API_KEY && usageSnapshot.length > 0) {
344
+ try {
345
+ const apiKey = process.env.FRAGMENTS_API_KEY;
346
+ const url = process.env.FRAGMENTS_URL ?? 'https://app.usefragments.com';
347
+ await fetch(`${url}/api/ingest`, {
348
+ method: 'POST',
349
+ headers: {
350
+ 'Content-Type': 'application/json',
351
+ Authorization: `Bearer ${apiKey}`,
352
+ },
353
+ body: JSON.stringify({
354
+ componentUsage: JSON.stringify(usageSnapshot),
355
+ componentHealth: JSON.stringify(health),
356
+ }),
357
+ });
358
+ } catch {
359
+ // Non-critical — don't fail the scan
360
+ }
361
+ }
362
+
363
+ return { exitCode: passedFiles === totalFiles ? 0 : 1 };
364
+ }
365
+
366
+ // ---------------------------------------------------------------------------
367
+ // Token extraction for cloud ingest
368
+ // ---------------------------------------------------------------------------
369
+
370
+ /**
371
+ * Auto-detect and extract code tokens to send alongside governance verdicts.
372
+ * Tries: 1) tokens config from fragments.config.ts, 2) Tailwind config.
373
+ * Returns a flat JSON string of token name → value, or undefined if nothing found.
374
+ */
375
+ async function extractCodeTokens(
376
+ rootDir: string,
377
+ configPath?: string,
378
+ quiet?: boolean,
379
+ ): Promise<string | undefined> {
380
+ try {
381
+ // 1. Try fragments.config.ts tokens config
382
+ try {
383
+ const { loadConfig } = await import('../core/node.js');
384
+ const { parseTokenFiles } = await import('../service/index.js');
385
+
386
+ const { config, configDir } = await loadConfig(configPath);
387
+ if (config.tokens?.include?.length) {
388
+ const result = await parseTokenFiles(config.tokens, configDir);
389
+ if (result.tokens.length > 0) {
390
+ const flat: Record<string, string> = {};
391
+ for (const token of result.tokens) {
392
+ flat[token.name] = token.resolvedValue;
393
+ }
394
+ if (!quiet) {
395
+ console.log(
396
+ pc.dim(` Extracted ${result.tokens.length} code tokens from config\n`),
397
+ );
398
+ }
399
+ return JSON.stringify(flat);
400
+ }
401
+ }
402
+ } catch {
403
+ // No config or no tokens section — fall through
404
+ }
405
+
406
+ // 2. Try Tailwind config
407
+ const {
408
+ findTailwindConfig,
409
+ loadTailwindConfig,
410
+ } = await import('../service/token-normalizer.js');
411
+
412
+ const tailwindPath = findTailwindConfig(rootDir);
413
+ if (tailwindPath) {
414
+ const tokens = await loadTailwindConfig(tailwindPath);
415
+ if (tokens.length > 0) {
416
+ const flat: Record<string, string> = {};
417
+ for (const token of tokens) {
418
+ flat[token.name] = token.value;
419
+ }
420
+ if (!quiet) {
421
+ console.log(
422
+ pc.dim(` Extracted ${tokens.length} tokens from Tailwind config\n`),
423
+ );
424
+ }
425
+ return JSON.stringify(flat);
426
+ }
427
+ }
428
+ } catch (error) {
429
+ if (!quiet) {
430
+ console.log(
431
+ pc.dim(
432
+ ` Token extraction skipped: ${error instanceof Error ? error.message : 'unknown error'}\n`,
433
+ ),
434
+ );
435
+ }
436
+ }
437
+
438
+ return undefined;
439
+ }
440
+
441
+ // ---------------------------------------------------------------------------
442
+ // governWatch
443
+ // ---------------------------------------------------------------------------
444
+
445
+ export async function governWatch(
446
+ options: GovernWatchOptions = {},
447
+ ): Promise<void> {
448
+ const {
449
+ loadPolicy,
450
+ createEngine,
451
+ buildAdaptersFromConfig,
452
+ createCloudAdapter,
453
+ formatVerdict,
454
+ } = await import('@fragments-sdk/govern');
455
+
456
+ const { scanFile } = await import('../service/enhance/scanner.js');
457
+ const { usagesToSpec } = await import(
458
+ '../service/enhance/converter.js'
459
+ );
460
+
461
+ const quiet = options.quiet ?? false;
462
+ const debounceMs = options.debounce ?? 300;
463
+ const format = options.format ?? 'summary';
464
+
465
+ // 1. Run initial scan
466
+ console.log(pc.cyan(`\n${BRAND.name} Governance Watch\n`));
467
+
468
+ const { exitCode } = await governScan(options);
469
+ if (!quiet) {
470
+ console.log(
471
+ pc.dim(` Initial scan ${exitCode === 0 ? 'passed' : 'completed with violations'}\n`),
472
+ );
473
+ }
474
+
475
+ // 2. Set up engine for incremental checks
476
+ const rootDir = resolve(options.dir ?? detectRootDir(process.cwd()));
477
+ let policy = await loadPolicy(options.config);
478
+ if (Object.keys(policy.rules).length === 0) {
479
+ policy = { ...policy, rules: SCAN_DEFAULT_RULES };
480
+ }
481
+ const adapters = buildAdaptersFromConfig(policy.audit);
482
+ if (!adapters.some(() => policy.audit?.cloud) && process.env.FRAGMENTS_API_KEY) {
483
+ adapters.push(createCloudAdapter());
484
+ }
485
+ const engine = createEngine(policy, adapters);
486
+
487
+ // 3. Watch for changes
488
+ console.log(pc.dim(' Watching for changes... (Ctrl+C to stop)\n'));
489
+
490
+ const chokidar = await import('chokidar');
491
+
492
+ const watcher = chokidar.watch(
493
+ ['**/*.tsx', '**/*.ts', '**/*.jsx', '**/*.js'],
494
+ {
495
+ cwd: rootDir,
496
+ ignoreInitial: true,
497
+ ignored: [
498
+ '**/node_modules/**',
499
+ '**/dist/**',
500
+ '**/build/**',
501
+ '**/.next/**',
502
+ '**/*.test.*',
503
+ '**/*.spec.*',
504
+ '**/*.stories.*',
505
+ ],
506
+ awaitWriteFinish: { stabilityThreshold: debounceMs },
507
+ },
508
+ );
509
+
510
+ const handleChange = async (changedRelPath: string) => {
511
+ const absolutePath = resolve(rootDir, changedRelPath);
512
+
513
+ try {
514
+ const { usages } = await scanFile(absolutePath);
515
+
516
+ if (usages.length === 0) {
517
+ if (!quiet) {
518
+ console.log(pc.dim(` ○ ${changedRelPath} — no component usages`));
519
+ }
520
+ return;
521
+ }
522
+
523
+ const spec = usagesToSpec(usages, absolutePath, rootDir);
524
+ const verdict = await engine.check(spec, {
525
+ runner: 'cli',
526
+ input: changedRelPath,
527
+ });
528
+
529
+ if (verdict.passed) {
530
+ console.log(
531
+ pc.green(` ✓ ${changedRelPath}`) +
532
+ pc.dim(` (${usages.length} components, score: ${verdict.score}/100)`),
533
+ );
534
+ } else {
535
+ console.log(pc.red(` ✗ ${changedRelPath}`));
536
+ if (format === 'summary') {
537
+ for (const result of verdict.results) {
538
+ for (const v of result.violations) {
539
+ console.log(
540
+ pc.dim(` ${v.severity} `) +
541
+ pc.yellow(v.rule) +
542
+ pc.dim(` — ${v.message}`),
543
+ );
544
+ }
545
+ }
546
+ } else {
547
+ console.log(formatVerdict(verdict, format));
548
+ }
549
+ }
550
+ } catch (error) {
551
+ if (!quiet) {
552
+ console.log(
553
+ pc.dim(` ⚠ ${changedRelPath} — `) +
554
+ pc.yellow(error instanceof Error ? error.message : 'parse error'),
555
+ );
556
+ }
557
+ }
558
+ };
559
+
560
+ watcher.on('change', handleChange);
561
+ watcher.on('add', handleChange);
562
+
563
+ // Keep process alive
564
+ await new Promise(() => {});
565
+ }
@@ -62,14 +62,77 @@ export async function governCheck(options: GovernCheckOptions = {}): Promise<{ e
62
62
 
63
63
  export async function governInit(options: GovernInitOptions = {}): Promise<void> {
64
64
  const { writeFile } = await import('node:fs/promises');
65
+ const { existsSync } = await import('node:fs');
65
66
  const { resolve } = await import('node:path');
66
67
  const { generateConfigTemplate } = await import('@fragments-sdk/govern');
68
+ const { findTailwindConfig } = await import(
69
+ '../service/token-normalizer.js'
70
+ );
71
+
72
+ const cwd = process.cwd();
73
+
74
+ // Auto-detect token sources
75
+ const detected: { tokenIncludes: string[]; projectType: string } = {
76
+ tokenIncludes: [],
77
+ projectType: 'unknown',
78
+ };
79
+
80
+ // Check for Tailwind
81
+ const tailwindPath = findTailwindConfig(cwd);
82
+ if (tailwindPath) {
83
+ const { relative } = await import('node:path');
84
+ detected.tokenIncludes.push(relative(cwd, tailwindPath));
85
+ detected.projectType = 'tailwind';
86
+ console.log(pc.dim(` Detected Tailwind config: ${detected.tokenIncludes[0]}`));
87
+ }
88
+
89
+ // Check for common SCSS/CSS token files
90
+ if (detected.tokenIncludes.length === 0) {
91
+ const candidates = [
92
+ 'src/styles/tokens.scss',
93
+ 'src/styles/variables.scss',
94
+ 'src/styles/theme.scss',
95
+ 'src/tokens.css',
96
+ 'src/styles/tokens.css',
97
+ 'src/styles/variables.css',
98
+ 'styles/tokens.scss',
99
+ 'styles/variables.scss',
100
+ 'tokens.json',
101
+ 'src/tokens.json',
102
+ ];
103
+ for (const candidate of candidates) {
104
+ if (existsSync(resolve(cwd, candidate))) {
105
+ detected.tokenIncludes.push(candidate);
106
+ detected.projectType = candidate.endsWith('.scss')
107
+ ? 'scss'
108
+ : candidate.endsWith('.json')
109
+ ? 'dtcg'
110
+ : 'css';
111
+ console.log(pc.dim(` Detected token source: ${candidate}`));
112
+ }
113
+ }
114
+ }
115
+
116
+ if (detected.tokenIncludes.length === 0) {
117
+ console.log(pc.dim(' No token sources auto-detected. You can add them manually.'));
118
+ }
67
119
 
68
120
  const outputPath = resolve(options.output ?? 'fragments.config.ts');
69
- const template = generateConfigTemplate();
121
+ const template = generateConfigTemplate({ tokenIncludes: detected.tokenIncludes });
70
122
 
71
123
  await writeFile(outputPath, template, 'utf-8');
72
- console.log(pc.green(`✓ Created ${outputPath}\n`));
124
+ console.log(pc.green(`\n✓ Created ${outputPath}\n`));
125
+
126
+ if (detected.tokenIncludes.length > 0) {
127
+ console.log(
128
+ pc.dim(` Token sources configured: ${detected.tokenIncludes.join(', ')}`),
129
+ );
130
+ }
131
+ console.log(
132
+ pc.dim(' Run ') +
133
+ pc.cyan('fragments govern scan') +
134
+ pc.dim(' to check your project.\n'),
135
+ );
73
136
  }
74
137
 
75
138
  // ---------------------------------------------------------------------------
@@ -95,7 +158,7 @@ export async function governConnect(): Promise<void> {
95
158
  // ── Step 1: Get API key ──────────────────────────────────────────────────
96
159
  console.log(pc.bold(' Step 1 of 3: Get your API key\n'));
97
160
 
98
- const dashboardUrl = `${cloudUrl}/dashboard/settings`;
161
+ const dashboardUrl = `${cloudUrl}/api-keys`;
99
162
  console.log(pc.dim(` → Opening the dashboard in your browser...`));
100
163
  console.log(pc.dim(` Copy your API key from Settings → API Keys\n`));
101
164
 
@@ -226,7 +289,7 @@ export async function governConnect(): Promise<void> {
226
289
  // ── Done ────────────────────────────────────────────────────────────────
227
290
  console.log(pc.dim('\n ─────────────────────────────────────\n'));
228
291
  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`));
292
+ console.log(pc.dim(` Dashboard: ${cloudUrl}/overview\n`));
230
293
  }
231
294
 
232
295
  // ---------------------------------------------------------------------------