@fragments-sdk/cli 0.15.0 → 0.15.2

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 (120) hide show
  1. package/dist/{ai-client-I6MDWNYA.js → ai-client-LSLQGOMM.js} +1 -2
  2. package/dist/bin.js +565 -548
  3. package/dist/bin.js.map +1 -1
  4. package/dist/chunk-5JF26E55.js +1255 -0
  5. package/dist/chunk-5JF26E55.js.map +1 -0
  6. package/dist/{chunk-XJQ5BIWI.js → chunk-6SQPP47U.js} +30 -314
  7. package/dist/chunk-6SQPP47U.js.map +1 -0
  8. package/dist/{chunk-65WSVDV5.js → chunk-HQ6A6DTV.js} +1386 -1097
  9. package/dist/chunk-HQ6A6DTV.js.map +1 -0
  10. package/dist/chunk-MHIBEEW4.js +511 -0
  11. package/dist/chunk-MHIBEEW4.js.map +1 -0
  12. package/dist/{chunk-CZD3AD4Q.js → chunk-ONUP6Z4W.js} +17 -6
  13. package/dist/chunk-ONUP6Z4W.js.map +1 -0
  14. package/dist/{codebase-scanner-VOTPXRYW.js → codebase-scanner-MQHUZC2G.js} +1 -2
  15. package/dist/{converter-JLINP7CJ.js → converter-7XM3Y6NJ.js} +1 -2
  16. package/dist/{converter-JLINP7CJ.js.map → converter-7XM3Y6NJ.js.map} +1 -1
  17. package/dist/core/index.js +0 -1
  18. package/dist/create-JVAU3YKN.js +852 -0
  19. package/dist/create-JVAU3YKN.js.map +1 -0
  20. package/dist/doctor-BDPMYYE6.js +385 -0
  21. package/dist/doctor-BDPMYYE6.js.map +1 -0
  22. package/dist/{generate-A4FP5426.js → generate-PVOLUAAC.js} +3 -4
  23. package/dist/{generate-A4FP5426.js.map → generate-PVOLUAAC.js.map} +1 -1
  24. package/dist/{govern-scan-UCBZR6D6.js → govern-scan-OYFZYOQW.js} +142 -9
  25. package/dist/govern-scan-OYFZYOQW.js.map +1 -0
  26. package/dist/index.d.ts +2 -22
  27. package/dist/index.js +8 -7
  28. package/dist/index.js.map +1 -1
  29. package/dist/{init-HGSM35XA.js → init-SSGUSP7Z.js} +3 -4
  30. package/dist/{init-HGSM35XA.js.map → init-SSGUSP7Z.js.map} +1 -1
  31. package/dist/{init-cloud-MQ6GRJAZ.js → init-cloud-3DNKPWFB.js} +29 -4
  32. package/dist/{init-cloud-MQ6GRJAZ.js.map → init-cloud-3DNKPWFB.js.map} +1 -1
  33. package/dist/mcp-bin.js +1 -2
  34. package/dist/mcp-bin.js.map +1 -1
  35. package/dist/node-37AUE74M.js +65 -0
  36. package/dist/push-contracts-WY32TFP6.js +84 -0
  37. package/dist/push-contracts-WY32TFP6.js.map +1 -0
  38. package/dist/{scan-VNNKACG2.js → scan-PKSYSTRR.js} +5 -5
  39. package/dist/{scan-generate-TWRHNU5M.js → scan-generate-VY27PIOX.js} +8 -9
  40. package/dist/scan-generate-VY27PIOX.js.map +1 -0
  41. package/dist/{scanner-7LAZYPWZ.js → scanner-4KZNOXAK.js} +1 -2
  42. package/dist/{service-FHQU7YS7.js → service-QJGWUIVL.js} +16 -9
  43. package/dist/{snapshot-KQEQ6XHL.js → snapshot-WIJMEIFT.js} +1 -2
  44. package/dist/{snapshot-KQEQ6XHL.js.map → snapshot-WIJMEIFT.js.map} +1 -1
  45. package/dist/{static-viewer-63PG6FWY.js → static-viewer-7QIBQZRC.js} +1 -2
  46. package/dist/{test-UQYUCZIS.js → test-64Z5BKBA.js} +2 -3
  47. package/dist/{test-UQYUCZIS.js.map → test-64Z5BKBA.js.map} +1 -1
  48. package/dist/token-normalizer-TEPOVBPV.js +312 -0
  49. package/dist/token-normalizer-TEPOVBPV.js.map +1 -0
  50. package/dist/token-parser-32KOIOFN.js +22 -0
  51. package/dist/token-parser-32KOIOFN.js.map +1 -0
  52. package/dist/{tokens-6GYKDV6U.js → tokens-NZWFQIAB.js} +7 -7
  53. package/dist/{tokens-generate-VTZV5EEW.js → tokens-generate-5JQSJ27E.js} +1 -2
  54. package/dist/{tokens-generate-VTZV5EEW.js.map → tokens-generate-5JQSJ27E.js.map} +1 -1
  55. package/dist/tokens-push-HY3KO36V.js +148 -0
  56. package/dist/tokens-push-HY3KO36V.js.map +1 -0
  57. package/package.json +18 -16
  58. package/src/bin.ts +94 -1
  59. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.contract.json +1 -1
  60. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.contract.json +1 -1
  61. package/src/commands/__tests__/build-freshness.test.ts +231 -0
  62. package/src/commands/__tests__/create.test.ts +71 -0
  63. package/src/commands/__tests__/drift-sync.test.ts +1 -1
  64. package/src/commands/__tests__/govern.test.ts +258 -0
  65. package/src/commands/__tests__/init.test.ts +9 -1
  66. package/src/commands/__tests__/scan-generate.test.ts +1 -1
  67. package/src/commands/build.ts +54 -1
  68. package/src/commands/context.ts +1 -1
  69. package/src/commands/create.ts +590 -0
  70. package/src/commands/doctor.ts +3 -2
  71. package/src/commands/govern-scan.ts +187 -8
  72. package/src/commands/govern.ts +65 -2
  73. package/src/commands/init-cloud.ts +32 -4
  74. package/src/commands/push-contracts.ts +112 -0
  75. package/src/commands/scan-generate.ts +1 -1
  76. package/src/commands/scan.ts +13 -0
  77. package/src/commands/sync.ts +2 -2
  78. package/src/commands/tokens-push.ts +199 -0
  79. package/src/core/__tests__/token-resolver.test.ts +1 -1
  80. package/src/core/component-extractor.test.ts +1 -1
  81. package/src/core/drift-verifier.ts +1 -1
  82. package/src/core/extractor-adapter.ts +1 -1
  83. package/src/index.ts +3 -3
  84. package/src/migrate/fragment-to-contract.ts +2 -2
  85. package/src/service/index.ts +8 -0
  86. package/src/service/tailwind-v4-parser.ts +314 -0
  87. package/src/service/token-parser.ts +56 -0
  88. package/src/setup.ts +10 -39
  89. package/src/theme/__tests__/component-contrast.test.ts +2 -2
  90. package/src/theme/__tests__/serializer.test.ts +1 -1
  91. package/src/theme/generator.ts +30 -1
  92. package/src/theme/schema.ts +8 -0
  93. package/src/theme/serializer.ts +13 -9
  94. package/src/theme/types.ts +8 -0
  95. package/src/validators.ts +1 -2
  96. package/dist/chunk-65WSVDV5.js.map +0 -1
  97. package/dist/chunk-7WHVW72L.js +0 -2664
  98. package/dist/chunk-7WHVW72L.js.map +0 -1
  99. package/dist/chunk-CZD3AD4Q.js.map +0 -1
  100. package/dist/chunk-MN3TJ3D5.js +0 -695
  101. package/dist/chunk-MN3TJ3D5.js.map +0 -1
  102. package/dist/chunk-XJQ5BIWI.js.map +0 -1
  103. package/dist/chunk-Z7EY4VHE.js +0 -50
  104. package/dist/govern-scan-UCBZR6D6.js.map +0 -1
  105. package/dist/sass.node-4XJK6YBF.js +0 -130708
  106. package/dist/sass.node-4XJK6YBF.js.map +0 -1
  107. package/dist/scan-generate-TWRHNU5M.js.map +0 -1
  108. package/src/build.ts +0 -736
  109. package/src/core/auto-props.ts +0 -464
  110. package/src/core/component-extractor.ts +0 -1121
  111. package/src/core/token-resolver.ts +0 -155
  112. package/src/viewer/preview-adapter.ts +0 -116
  113. /package/dist/{ai-client-I6MDWNYA.js.map → ai-client-LSLQGOMM.js.map} +0 -0
  114. /package/dist/{chunk-Z7EY4VHE.js.map → codebase-scanner-MQHUZC2G.js.map} +0 -0
  115. /package/dist/{codebase-scanner-VOTPXRYW.js.map → node-37AUE74M.js.map} +0 -0
  116. /package/dist/{scan-VNNKACG2.js.map → scan-PKSYSTRR.js.map} +0 -0
  117. /package/dist/{scanner-7LAZYPWZ.js.map → scanner-4KZNOXAK.js.map} +0 -0
  118. /package/dist/{service-FHQU7YS7.js.map → service-QJGWUIVL.js.map} +0 -0
  119. /package/dist/{static-viewer-63PG6FWY.js.map → static-viewer-7QIBQZRC.js.map} +0 -0
  120. /package/dist/{tokens-6GYKDV6U.js.map → tokens-NZWFQIAB.js.map} +0 -0
@@ -90,6 +90,7 @@ export async function governScan(
90
90
  buildAdaptersFromConfig,
91
91
  createCloudAdapter,
92
92
  formatVerdict,
93
+ computeComponentHealth,
93
94
  } = await import('@fragments-sdk/govern');
94
95
 
95
96
  const { scanCodebase } = await import(
@@ -123,20 +124,65 @@ export async function governScan(
123
124
  }
124
125
  }
125
126
 
126
- // 3. Build adapters. Auto-add cloud if FRAGMENTS_API_KEY is set
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
127
167
  const adapters = buildAdaptersFromConfig(policy.audit);
128
168
  const hasCloudAdapter = adapters.length > 0 && policy.audit?.cloud;
129
169
  if (!hasCloudAdapter && process.env.FRAGMENTS_API_KEY) {
130
- adapters.push(createCloudAdapter());
170
+ adapters.push(createCloudAdapter({ codeTokens, contractRegistry }));
131
171
  if (!quiet) {
132
172
  console.log(pc.dim(' Cloud audit enabled (FRAGMENTS_API_KEY detected)\n'));
133
173
  }
134
174
  }
135
175
 
136
- // 4. Create engine
137
- const engine = createEngine(policy, adapters);
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
+ );
138
184
 
139
- // 5. Scan codebase
185
+ // 6. Scan codebase
140
186
  if (!quiet) {
141
187
  console.log(pc.dim(' Scanning files...\n'));
142
188
  }
@@ -163,7 +209,7 @@ export async function governScan(
163
209
  );
164
210
  }
165
211
 
166
- // 6. Collect all usages across components
212
+ // 7. Collect all usages across components
167
213
  const allUsages: ComponentUsage[] = [];
168
214
  for (const comp of Object.values(analysis.components)) {
169
215
  allUsages.push(...comp.usages);
@@ -176,12 +222,23 @@ export async function governScan(
176
222
  return { exitCode: 0 };
177
223
  }
178
224
 
179
- // 7. Group by file and run checks
225
+ // 8. Group by file and run checks
180
226
  const grouped = groupByFile(allUsages);
181
227
  let totalFiles = 0;
182
228
  let passedFiles = 0;
183
229
  let totalViolations = 0;
184
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
+ }> = [];
185
242
 
186
243
  for (const [filePath, usages] of grouped) {
187
244
  const spec = usagesToSpec(usages, filePath, rootDir);
@@ -191,6 +248,20 @@ export async function governScan(
191
248
  runner: 'cli',
192
249
  input: relPath,
193
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
+ });
194
265
 
195
266
  totalFiles++;
196
267
 
@@ -230,7 +301,10 @@ export async function governScan(
230
301
  }
231
302
  }
232
303
 
233
- // 8. Summary
304
+ // 8b. Compute component health
305
+ const health = computeComponentHealth(allVerdicts, registryMap ?? {});
306
+
307
+ // 9. Summary
234
308
  if (!quiet && format === 'summary') {
235
309
  console.log(pc.dim('\n ─────────────────────────────────────\n'));
236
310
  console.log(` Files checked: ${totalFiles}`);
@@ -245,6 +319,15 @@ export async function governScan(
245
319
  }
246
320
  }
247
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
+
248
331
  console.log();
249
332
 
250
333
  if (passedFiles === totalFiles) {
@@ -256,9 +339,105 @@ export async function governScan(
256
339
  }
257
340
  }
258
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
+
259
363
  return { exitCode: passedFiles === totalFiles ? 0 : 1 };
260
364
  }
261
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
+
262
441
  // ---------------------------------------------------------------------------
263
442
  // governWatch
264
443
  // ---------------------------------------------------------------------------
@@ -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
  // ---------------------------------------------------------------------------
@@ -327,7 +327,34 @@ export async function initCloud(options: InitCloudOptions = {}): Promise<void> {
327
327
  console.log(pc.green(` ✓ Created ${BRAND.configFile}`));
328
328
  }
329
329
 
330
- // ── 6. Run first check ────────────────────────────────────────────
330
+ // ── 6. Push contracts if available ──────────────────────────────────
331
+ const fragmentsJsonPath = resolve('fragments.json');
332
+ if (existsSync(fragmentsJsonPath)) {
333
+ console.log(pc.dim('\n Found fragments.json — pushing contracts to Cloud...'));
334
+ try {
335
+ const { pushContracts } = await import('@fragments-sdk/govern');
336
+ const raw = readFileSync(fragmentsJsonPath, 'utf-8');
337
+ const parsed = JSON.parse(raw);
338
+ if (parsed.fragments && Array.isArray(parsed.fragments)) {
339
+ const result = await pushContracts({
340
+ contractRegistry: JSON.stringify({ fragments: parsed.fragments }),
341
+ apiKey: auth.apiKey,
342
+ url: cloudUrl,
343
+ });
344
+ if (result.ok) {
345
+ console.log(pc.green(` ✓ Pushed ${parsed.fragments.length} component contracts`));
346
+ } else {
347
+ console.log(pc.yellow(` ⚠ Contract push failed: ${result.error}`));
348
+ }
349
+ }
350
+ } catch {
351
+ console.log(pc.yellow(' ⚠ Could not push contracts'));
352
+ }
353
+ } else {
354
+ console.log(pc.dim('\n No fragments.json found — run `fragments scan` or `fragments build` to generate contracts'));
355
+ }
356
+
357
+ // ── 7. Run first check ────────────────────────────────────────────
331
358
  if (!options.skipCheck) {
332
359
  console.log(pc.dim('\n Running first governance check...\n'));
333
360
  try {
@@ -346,9 +373,10 @@ export async function initCloud(options: InitCloudOptions = {}): Promise<void> {
346
373
  }
347
374
  }
348
375
 
349
- // ── 7. Done ───────────────────────────────────────────────────────
376
+ // ── 8. Done ───────────────────────────────────────────────────────
350
377
  console.log(pc.green('\n ✓ All set!') + ' Your project is connected to Fragments Cloud.\n');
351
378
  console.log(pc.dim(` Dashboard: ${cloudUrl}`));
352
- console.log(pc.dim(' Run checks: fragments govern check --cloud'));
353
- console.log(pc.dim(' View config: fragments.config.ts\n'));
379
+ console.log(pc.dim(' Run checks: fragments govern scan'));
380
+ console.log(pc.dim(' Push contracts: fragments govern push-contracts'));
381
+ console.log(pc.dim(' View config: fragments.config.ts\n'));
354
382
  }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * fragments push-contracts
3
+ *
4
+ * Push compiled component contracts to Fragments Cloud.
5
+ * Reads fragments.json from the project root (or builds it from .contract.json files)
6
+ * and posts the contract registry to the Cloud ingest endpoint.
7
+ */
8
+
9
+ import { readFileSync, existsSync } from 'node:fs';
10
+ import { resolve } from 'node:path';
11
+ import pc from 'picocolors';
12
+ import { BRAND } from '../core/index.js';
13
+
14
+ export interface PushContractsOptions {
15
+ /** Path to fragments.json or directory containing .contract.json files */
16
+ input?: string;
17
+ /** Fragments Cloud URL */
18
+ url?: string;
19
+ /** API key (defaults to FRAGMENTS_API_KEY env var) */
20
+ apiKey?: string;
21
+ /** Suppress output */
22
+ quiet?: boolean;
23
+ }
24
+
25
+ export async function pushContracts(
26
+ options: PushContractsOptions = {},
27
+ ): Promise<{ exitCode: number }> {
28
+ const quiet = options.quiet ?? false;
29
+ const apiKey = options.apiKey ?? process.env.FRAGMENTS_API_KEY;
30
+
31
+ if (!apiKey) {
32
+ console.error(
33
+ pc.red('No API key found. Set FRAGMENTS_API_KEY or pass --api-key.'),
34
+ );
35
+ return { exitCode: 1 };
36
+ }
37
+
38
+ if (!quiet) {
39
+ console.log(pc.cyan(`\n${BRAND.name} Push Contracts\n`));
40
+ }
41
+
42
+ // 1. Find contract registry
43
+ const inputPath = options.input
44
+ ? resolve(options.input)
45
+ : resolve('fragments.json');
46
+
47
+ if (!existsSync(inputPath)) {
48
+ console.error(
49
+ pc.red(`Contract registry not found at ${inputPath}`),
50
+ );
51
+ console.error(
52
+ pc.dim('Run `fragments build` or `fragments scan` to generate fragments.json first.'),
53
+ );
54
+ return { exitCode: 1 };
55
+ }
56
+
57
+ // 2. Parse and validate
58
+ let contractRegistry: string;
59
+ let componentCount = 0;
60
+
61
+ try {
62
+ const raw = readFileSync(inputPath, 'utf-8');
63
+ const parsed = JSON.parse(raw);
64
+ const fragments = parsed.fragments ?? parsed;
65
+
66
+ if (!Array.isArray(fragments)) {
67
+ console.error(pc.red('Invalid contract registry: expected fragments array'));
68
+ return { exitCode: 1 };
69
+ }
70
+
71
+ componentCount = fragments.length;
72
+ contractRegistry = JSON.stringify({ fragments });
73
+ } catch (error) {
74
+ console.error(
75
+ pc.red('Failed to parse contract registry:'),
76
+ error instanceof Error ? error.message : 'unknown error',
77
+ );
78
+ return { exitCode: 1 };
79
+ }
80
+
81
+ if (componentCount === 0) {
82
+ console.warn(pc.yellow('No components found in contract registry. Nothing to push.'));
83
+ return { exitCode: 0 };
84
+ }
85
+
86
+ if (!quiet) {
87
+ console.log(pc.dim(` Found ${componentCount} component contracts\n`));
88
+ console.log(pc.dim(' Pushing to Fragments Cloud...\n'));
89
+ }
90
+
91
+ // 3. Push to Cloud
92
+ const { pushContracts: push } = await import('@fragments-sdk/govern');
93
+
94
+ const result = await push({
95
+ contractRegistry,
96
+ url: options.url,
97
+ apiKey,
98
+ });
99
+
100
+ if (!result.ok) {
101
+ console.error(pc.red(`Failed to push contracts: ${result.error}`));
102
+ return { exitCode: 1 };
103
+ }
104
+
105
+ if (!quiet) {
106
+ console.log(
107
+ pc.green(` ✓ Pushed ${componentCount} component contracts to Fragments Cloud\n`),
108
+ );
109
+ }
110
+
111
+ return { exitCode: 0 };
112
+ }
@@ -27,7 +27,7 @@ import {
27
27
  type ComponentMeta,
28
28
  type PropMeta,
29
29
  type CompositionMeta,
30
- } from "../core/component-extractor.js";
30
+ } from '@fragments-sdk/extract';
31
31
 
32
32
  // ---------------------------------------------------------------------------
33
33
  // Types
@@ -22,6 +22,7 @@ import {
22
22
  import {
23
23
  loadConfig,
24
24
  discoverAllComponents,
25
+ findConfigFile,
25
26
  type DiscoveredComponent,
26
27
  } from "../core/node.js";
27
28
  import {
@@ -38,6 +39,7 @@ import {
38
39
  type PropsExtractionResult,
39
40
  type ParsedStoryFile,
40
41
  } from "../service/index.js";
42
+ import { getGeneratorVersion } from '@fragments-sdk/compiler';
41
43
 
42
44
  export interface ScanOptions {
43
45
  /** Path to config file */
@@ -271,10 +273,21 @@ export async function scan(options: ScanOptions = {}): Promise<ScanResult> {
271
273
  // Write output
272
274
  const outputPath = resolve(configDir, outputFile);
273
275
  await mkdir(dirname(outputPath), { recursive: true });
276
+ const generatorVersion = await getGeneratorVersion();
277
+ const buildInputs = components
278
+ .map((component) => relative(configDir, component.sourcePath).split("\\").join("/"))
279
+ .sort();
280
+ const configPath = options.config ? resolve(process.cwd(), options.config) : findConfigFile(configDir);
281
+ if (configPath) {
282
+ buildInputs.push(relative(configDir, configPath).split("\\").join("/"));
283
+ buildInputs.sort();
284
+ }
274
285
 
275
286
  const output: CompiledFragmentsFile = {
276
287
  version: "1.0.0",
277
288
  generatedAt: new Date().toISOString(),
289
+ generatorVersion,
290
+ buildInputs,
278
291
  fragments,
279
292
  };
280
293
 
@@ -13,8 +13,8 @@ import { BRAND } from '../core/index.js';
13
13
  import { loadConfig } from '../core/node.js';
14
14
  import { discoverFragmentFiles, loadFragmentFile } from '../core/node.js';
15
15
  import { parseFragmentFile } from '../core/parser.js';
16
- import { resolveComponentSourcePath } from '../core/auto-props.js';
17
- import { createComponentExtractor, type PropMeta, type CompositionMeta } from '../core/component-extractor.js';
16
+ import { resolveComponentSourcePath } from '@fragments-sdk/extract';
17
+ import { createComponentExtractor, type PropMeta, type CompositionMeta } from '@fragments-sdk/extract';
18
18
  import type { FragmentsConfig } from '@fragments-sdk/core';
19
19
 
20
20
  // ---------------------------------------------------------------------------