@fragments-sdk/cli 0.16.0 → 0.17.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 (62) hide show
  1. package/dist/bin.js +24 -21
  2. package/dist/bin.js.map +1 -1
  3. package/dist/{chunk-77AAP6R6.js → chunk-ANTWP3UG.js} +2 -2
  4. package/dist/{chunk-G6UVWMFU.js → chunk-B4A4ZEGS.js} +5 -5
  5. package/dist/{chunk-SJFSG7QF.js → chunk-FFCI6OVZ.js} +3 -1
  6. package/dist/chunk-FFCI6OVZ.js.map +1 -0
  7. package/dist/{chunk-XRADMHMV.js → chunk-HNHE64CR.js} +2 -2
  8. package/dist/{chunk-ACX7YWZW.js → chunk-MN3B2EE6.js} +2 -2
  9. package/dist/{chunk-QCN35LJU.js → chunk-SAQW37L5.js} +3 -2
  10. package/dist/chunk-SAQW37L5.js.map +1 -0
  11. package/dist/{chunk-OZZ4SVZX.js → chunk-SNZXGHL2.js} +2 -2
  12. package/dist/{chunk-ACFVKMVZ.js → chunk-VT2J62ND.js} +4 -4
  13. package/dist/{codebase-scanner-MQHUZC2G.js → codebase-scanner-2T5QIDBA.js} +2 -2
  14. package/dist/core/index.js +1 -1
  15. package/dist/{create-3ZFYQB3T.js → create-D44QD7MV.js} +2 -2
  16. package/dist/{doctor-4IDUM7HI.js → doctor-7B5N4JYU.js} +2 -2
  17. package/dist/{generate-VNUUWVWQ.js → generate-T47JZRVU.js} +3 -3
  18. package/dist/{govern-scan-HTACKYPF.js → govern-scan-KTGPDZVK.js} +187 -7
  19. package/dist/govern-scan-KTGPDZVK.js.map +1 -0
  20. package/dist/index.js +6 -6
  21. package/dist/{init-PXFRAQ64.js → init-2RGAY4W6.js} +5 -5
  22. package/dist/mcp-bin.js +2 -2
  23. package/dist/{scan-L4GWGEZX.js → scan-A2WJM54L.js} +6 -6
  24. package/dist/{scan-generate-74EYSAGH.js → scan-generate-LUSOHT36.js} +3 -3
  25. package/dist/{service-VELQHEWV.js → service-ROCP7TKG.js} +4 -4
  26. package/dist/{snapshot-DT4B6DPR.js → snapshot-B3SAW74Y.js} +2 -2
  27. package/dist/{static-viewer-E4OJWFDJ.js → static-viewer-7L6UEYTJ.js} +3 -3
  28. package/dist/{test-QJY2QO4X.js → test-PQDVDURE.js} +3 -3
  29. package/dist/{token-normalizer-56H4242J.js → token-normalizer-7TFCVDZL.js} +2 -2
  30. package/dist/{tokens-K6URXFPK.js → tokens-64FG5FDP.js} +5 -5
  31. package/dist/{tokens-generate-EL6IN536.js → tokens-generate-CL4LBBQA.js} +2 -2
  32. package/package.json +8 -8
  33. package/src/bin.ts +6 -0
  34. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.contract.json +1 -1
  35. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.contract.json +1 -1
  36. package/src/commands/govern-scan.ts +257 -5
  37. package/src/mcp/__tests__/server.integration.test.ts +9 -20
  38. package/src/service/enhance/codebase-scanner.ts +3 -2
  39. package/src/service/enhance/types.ts +3 -0
  40. package/dist/chunk-QCN35LJU.js.map +0 -1
  41. package/dist/chunk-SJFSG7QF.js.map +0 -1
  42. package/dist/govern-scan-HTACKYPF.js.map +0 -1
  43. /package/dist/{chunk-77AAP6R6.js.map → chunk-ANTWP3UG.js.map} +0 -0
  44. /package/dist/{chunk-G6UVWMFU.js.map → chunk-B4A4ZEGS.js.map} +0 -0
  45. /package/dist/{chunk-XRADMHMV.js.map → chunk-HNHE64CR.js.map} +0 -0
  46. /package/dist/{chunk-ACX7YWZW.js.map → chunk-MN3B2EE6.js.map} +0 -0
  47. /package/dist/{chunk-OZZ4SVZX.js.map → chunk-SNZXGHL2.js.map} +0 -0
  48. /package/dist/{chunk-ACFVKMVZ.js.map → chunk-VT2J62ND.js.map} +0 -0
  49. /package/dist/{codebase-scanner-MQHUZC2G.js.map → codebase-scanner-2T5QIDBA.js.map} +0 -0
  50. /package/dist/{create-3ZFYQB3T.js.map → create-D44QD7MV.js.map} +0 -0
  51. /package/dist/{doctor-4IDUM7HI.js.map → doctor-7B5N4JYU.js.map} +0 -0
  52. /package/dist/{generate-VNUUWVWQ.js.map → generate-T47JZRVU.js.map} +0 -0
  53. /package/dist/{init-PXFRAQ64.js.map → init-2RGAY4W6.js.map} +0 -0
  54. /package/dist/{scan-L4GWGEZX.js.map → scan-A2WJM54L.js.map} +0 -0
  55. /package/dist/{scan-generate-74EYSAGH.js.map → scan-generate-LUSOHT36.js.map} +0 -0
  56. /package/dist/{service-VELQHEWV.js.map → service-ROCP7TKG.js.map} +0 -0
  57. /package/dist/{snapshot-DT4B6DPR.js.map → snapshot-B3SAW74Y.js.map} +0 -0
  58. /package/dist/{static-viewer-E4OJWFDJ.js.map → static-viewer-7L6UEYTJ.js.map} +0 -0
  59. /package/dist/{test-QJY2QO4X.js.map → test-PQDVDURE.js.map} +0 -0
  60. /package/dist/{token-normalizer-56H4242J.js.map → token-normalizer-7TFCVDZL.js.map} +0 -0
  61. /package/dist/{tokens-K6URXFPK.js.map → tokens-64FG5FDP.js.map} +0 -0
  62. /package/dist/{tokens-generate-EL6IN536.js.map → tokens-generate-CL4LBBQA.js.map} +0 -0
@@ -5,11 +5,11 @@ import {
5
5
  addTranspilePackages,
6
6
  detectSetupFramework,
7
7
  findEntryFile
8
- } from "./chunk-ACX7YWZW.js";
8
+ } from "./chunk-MN3B2EE6.js";
9
9
  import "./chunk-D2CDBRNU.js";
10
10
  import {
11
11
  BRAND
12
- } from "./chunk-SJFSG7QF.js";
12
+ } from "./chunk-FFCI6OVZ.js";
13
13
 
14
14
  // src/commands/init.ts
15
15
  import { readFile, writeFile, mkdir, access } from "fs/promises";
@@ -370,7 +370,7 @@ Scan path not found: ${scanPath}
370
370
  errors: [`Scan path not found: ${scanPath}`]
371
371
  };
372
372
  }
373
- const { scanGenerate } = await import("./scan-generate-74EYSAGH.js");
373
+ const { scanGenerate } = await import("./scan-generate-LUSOHT36.js");
374
374
  const scanResult = await scanGenerate({
375
375
  scanPath,
376
376
  force: options.force,
@@ -578,7 +578,7 @@ ${BRAND.name} init
578
578
  }
579
579
  if (scenario === "components" || scenario === "stories") {
580
580
  try {
581
- const { scan } = await import("./scan-L4GWGEZX.js");
581
+ const { scan } = await import("./scan-A2WJM54L.js");
582
582
  const scanResult = await scan({
583
583
  config: configPath,
584
584
  verbose: false,
@@ -635,4 +635,4 @@ ${BRAND.name} init
635
635
  export {
636
636
  init
637
637
  };
638
- //# sourceMappingURL=init-PXFRAQ64.js.map
638
+ //# sourceMappingURL=init-2RGAY4W6.js.map
package/dist/mcp-bin.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  BRAND,
9
9
  DEFAULTS,
10
10
  generateContext
11
- } from "./chunk-SJFSG7QF.js";
11
+ } from "./chunk-FFCI6OVZ.js";
12
12
 
13
13
  // src/mcp/server.ts
14
14
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -42,7 +42,7 @@ var _service = null;
42
42
  async function getService() {
43
43
  if (!_service) {
44
44
  try {
45
- _service = await import("./service-VELQHEWV.js");
45
+ _service = await import("./service-ROCP7TKG.js");
46
46
  } catch {
47
47
  throw new Error(
48
48
  "Visual tools require playwright. Install it with: npm install playwright"
@@ -1,14 +1,14 @@
1
1
  import { createRequire as __banner_createRequire } from 'module'; const require = __banner_createRequire(import.meta.url);
2
2
  import {
3
3
  scan
4
- } from "./chunk-G6UVWMFU.js";
5
- import "./chunk-XRADMHMV.js";
6
- import "./chunk-77AAP6R6.js";
4
+ } from "./chunk-B4A4ZEGS.js";
5
+ import "./chunk-HNHE64CR.js";
6
+ import "./chunk-ANTWP3UG.js";
7
7
  import "./chunk-D2CDBRNU.js";
8
- import "./chunk-SJFSG7QF.js";
9
- import "./chunk-QCN35LJU.js";
8
+ import "./chunk-FFCI6OVZ.js";
9
+ import "./chunk-SAQW37L5.js";
10
10
  import "./chunk-7DZC4YEV.js";
11
11
  export {
12
12
  scan
13
13
  };
14
- //# sourceMappingURL=scan-L4GWGEZX.js.map
14
+ //# sourceMappingURL=scan-A2WJM54L.js.map
@@ -1,11 +1,11 @@
1
1
  import { createRequire as __banner_createRequire } from 'module'; const require = __banner_createRequire(import.meta.url);
2
2
  import {
3
3
  discoverAllComponents
4
- } from "./chunk-XRADMHMV.js";
4
+ } from "./chunk-HNHE64CR.js";
5
5
  import "./chunk-D2CDBRNU.js";
6
6
  import {
7
7
  BRAND
8
- } from "./chunk-SJFSG7QF.js";
8
+ } from "./chunk-FFCI6OVZ.js";
9
9
 
10
10
  // src/commands/scan-generate.ts
11
11
  import { readFile, writeFile, access, mkdir } from "fs/promises";
@@ -1400,4 +1400,4 @@ export {
1400
1400
  resolveCoreInstallCommand,
1401
1401
  scanGenerate
1402
1402
  };
1403
- //# sourceMappingURL=scan-generate-74EYSAGH.js.map
1403
+ //# sourceMappingURL=scan-generate-LUSOHT36.js.map
@@ -81,12 +81,12 @@ import {
81
81
  sanitizeFilename,
82
82
  shutdownSharedPool,
83
83
  sleep
84
- } from "./chunk-77AAP6R6.js";
84
+ } from "./chunk-ANTWP3UG.js";
85
85
  import "./chunk-D2CDBRNU.js";
86
86
  import {
87
87
  BRAND,
88
88
  DEFAULTS
89
- } from "./chunk-SJFSG7QF.js";
89
+ } from "./chunk-FFCI6OVZ.js";
90
90
  import {
91
91
  aggregateAllUsages,
92
92
  aggregateComponentUsages,
@@ -104,7 +104,7 @@ import {
104
104
  saveCache,
105
105
  scanCodebase,
106
106
  summarizePatternsForPrompt
107
- } from "./chunk-QCN35LJU.js";
107
+ } from "./chunk-SAQW37L5.js";
108
108
  import {
109
109
  scanFile,
110
110
  scanFileForImports,
@@ -215,4 +215,4 @@ export {
215
215
  sleep,
216
216
  summarizePatternsForPrompt
217
217
  };
218
- //# sourceMappingURL=service-VELQHEWV.js.map
218
+ //# sourceMappingURL=service-ROCP7TKG.js.map
@@ -2,7 +2,7 @@ import { createRequire as __banner_createRequire } from 'module'; const require
2
2
  import "./chunk-D2CDBRNU.js";
3
3
  import {
4
4
  BRAND
5
- } from "./chunk-SJFSG7QF.js";
5
+ } from "./chunk-FFCI6OVZ.js";
6
6
 
7
7
  // src/commands/snapshot.ts
8
8
  import { resolve } from "path";
@@ -135,4 +135,4 @@ ${BRAND.name} Visual Snapshots
135
135
  export {
136
136
  snapshot
137
137
  };
138
- //# sourceMappingURL=snapshot-DT4B6DPR.js.map
138
+ //# sourceMappingURL=snapshot-B3SAW74Y.js.map
@@ -2,11 +2,11 @@ import { createRequire as __banner_createRequire } from 'module'; const require
2
2
  import {
3
3
  generateStaticViewer,
4
4
  generateViewerFromJson
5
- } from "./chunk-OZZ4SVZX.js";
5
+ } from "./chunk-SNZXGHL2.js";
6
6
  import "./chunk-D2CDBRNU.js";
7
- import "./chunk-SJFSG7QF.js";
7
+ import "./chunk-FFCI6OVZ.js";
8
8
  export {
9
9
  generateStaticViewer,
10
10
  generateViewerFromJson
11
11
  };
12
- //# sourceMappingURL=static-viewer-E4OJWFDJ.js.map
12
+ //# sourceMappingURL=static-viewer-7L6UEYTJ.js.map
@@ -2,8 +2,8 @@ import { createRequire as __banner_createRequire } from 'module'; const require
2
2
  import {
3
3
  discoverFragmentFiles,
4
4
  parseFragmentFile
5
- } from "./chunk-XRADMHMV.js";
6
- import "./chunk-SJFSG7QF.js";
5
+ } from "./chunk-HNHE64CR.js";
6
+ import "./chunk-FFCI6OVZ.js";
7
7
 
8
8
  // src/test/index.ts
9
9
  import { resolve as resolve2, join as join2 } from "path";
@@ -1068,4 +1068,4 @@ export {
1068
1068
  listTests,
1069
1069
  runTestCommand
1070
1070
  };
1071
- //# sourceMappingURL=test-QJY2QO4X.js.map
1071
+ //# sourceMappingURL=test-PQDVDURE.js.map
@@ -1,7 +1,7 @@
1
1
  import { createRequire as __banner_createRequire } from 'module'; const require = __banner_createRequire(import.meta.url);
2
2
  import {
3
3
  parseColor
4
- } from "./chunk-SJFSG7QF.js";
4
+ } from "./chunk-FFCI6OVZ.js";
5
5
 
6
6
  // src/service/token-normalizer.ts
7
7
  import { existsSync } from "fs";
@@ -309,4 +309,4 @@ export {
309
309
  normalizeCSSVarTokens,
310
310
  normalizeTailwindTheme
311
311
  };
312
- //# sourceMappingURL=token-normalizer-56H4242J.js.map
312
+ //# sourceMappingURL=token-normalizer-7TFCVDZL.js.map
@@ -1,15 +1,15 @@
1
1
  import { createRequire as __banner_createRequire } from 'module'; const require = __banner_createRequire(import.meta.url);
2
2
  import {
3
3
  loadConfig
4
- } from "./chunk-XRADMHMV.js";
4
+ } from "./chunk-HNHE64CR.js";
5
5
  import {
6
6
  parseTokenFiles
7
- } from "./chunk-77AAP6R6.js";
7
+ } from "./chunk-ANTWP3UG.js";
8
8
  import "./chunk-D2CDBRNU.js";
9
9
  import {
10
10
  BRAND
11
- } from "./chunk-SJFSG7QF.js";
12
- import "./chunk-QCN35LJU.js";
11
+ } from "./chunk-FFCI6OVZ.js";
12
+ import "./chunk-SAQW37L5.js";
13
13
  import "./chunk-7DZC4YEV.js";
14
14
 
15
15
  // src/commands/tokens.ts
@@ -172,4 +172,4 @@ export {
172
172
  tokens_default as default,
173
173
  tokens
174
174
  };
175
- //# sourceMappingURL=tokens-K6URXFPK.js.map
175
+ //# sourceMappingURL=tokens-64FG5FDP.js.map
@@ -5,7 +5,7 @@ import {
5
5
  generateFigmaVariables,
6
6
  generateSCSSVariables,
7
7
  generateTailwindConfig
8
- } from "./chunk-SJFSG7QF.js";
8
+ } from "./chunk-FFCI6OVZ.js";
9
9
 
10
10
  // src/commands/tokens-generate.ts
11
11
  import { readFile, writeFile, mkdir } from "fs/promises";
@@ -82,4 +82,4 @@ export default ${JSON.stringify(config, null, 2)};
82
82
  export {
83
83
  tokensGenerate
84
84
  };
85
- //# sourceMappingURL=tokens-generate-EL6IN536.js.map
85
+ //# sourceMappingURL=tokens-generate-CL4LBBQA.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fragments-sdk/cli",
3
- "version": "0.16.0",
3
+ "version": "0.17.1",
4
4
  "license": "FSL-1.1-MIT",
5
5
  "description": "CLI, MCP server, and dev tools for Fragments design system",
6
6
  "author": "Conan McNicholl",
@@ -82,13 +82,13 @@
82
82
  "vite": "^6.0.0",
83
83
  "vite-plugin-svgr": "^4.5.0",
84
84
  "zod": "^3.24.1",
85
- "@fragments-sdk/compiler": "0.2.1",
86
- "@fragments-sdk/extract": "0.1.1",
87
- "@fragments-sdk/core": "2.1.0",
88
- "@fragments-sdk/context": "0.6.1",
89
- "@fragments-sdk/govern": "^0.3.1",
90
- "@fragments-sdk/viewer": "0.2.10",
91
- "@fragments-sdk/webmcp": "3.0.0"
85
+ "@fragments-sdk/compiler": "0.2.2",
86
+ "@fragments-sdk/context": "0.7.0",
87
+ "@fragments-sdk/extract": "0.1.2",
88
+ "@fragments-sdk/core": "3.0.0",
89
+ "@fragments-sdk/viewer": "0.2.11",
90
+ "@fragments-sdk/webmcp": "4.0.0",
91
+ "@fragments-sdk/govern": "^0.3.2"
92
92
  },
93
93
  "devDependencies": {
94
94
  "@types/babel__generator": "^7.6.8",
package/src/bin.ts CHANGED
@@ -1376,6 +1376,9 @@ governCmd
1376
1376
  .option('-f, --format <format>', 'Output format: summary, json, sarif', 'summary')
1377
1377
  .option('-r, --report <path>', 'Write an aggregated machine-readable JSON report')
1378
1378
  .option('-q, --quiet', 'Suppress non-error output')
1379
+ .option('--api-key <key>', 'Fragments Cloud API key — report findings to Cloud')
1380
+ .option('--cloud-url <url>', 'Fragments Cloud URL (default: https://app.usefragments.com)')
1381
+ .option('--diff [base]', 'Only scan files changed vs a base ref (default: auto-detect merge base)')
1379
1382
  .action(async (options) => {
1380
1383
  try {
1381
1384
  const { governScan } = await import('./commands/govern-scan.js');
@@ -1385,6 +1388,9 @@ governCmd
1385
1388
  format: options.format,
1386
1389
  report: options.report,
1387
1390
  quiet: options.quiet,
1391
+ apiKey: options.apiKey || process.env.FRAGMENTS_API_KEY || undefined,
1392
+ cloudUrl: options.cloudUrl || process.env.FRAGMENTS_CLOUD_URL || undefined,
1393
+ diff: options.diff,
1388
1394
  });
1389
1395
  process.exit(exitCode);
1390
1396
  } catch (error) {
@@ -37,6 +37,6 @@
37
37
  "source": "extracted",
38
38
  "verified": false,
39
39
  "frameworkSupport": "native",
40
- "extractedAt": "2026-04-01T21:45:25.486Z"
40
+ "extractedAt": "2026-04-04T15:06:36.231Z"
41
41
  }
42
42
  }
@@ -15,6 +15,6 @@
15
15
  "source": "extracted",
16
16
  "verified": false,
17
17
  "frameworkSupport": "native",
18
- "extractedAt": "2026-04-01T21:45:25.487Z"
18
+ "extractedAt": "2026-04-04T15:06:36.234Z"
19
19
  }
20
20
  }
@@ -7,9 +7,11 @@
7
7
 
8
8
  import pc from 'picocolors';
9
9
  import { resolve, relative } from 'node:path';
10
- import { existsSync } from 'node:fs';
10
+ import { existsSync, readFileSync } from 'node:fs';
11
+ import { execSync } from 'node:child_process';
11
12
  import { BRAND } from '../core/index.js';
12
13
  import type { ComponentUsage } from '../service/enhance/types.js';
14
+ import type { GovernanceVerdict } from '@fragments-sdk/govern';
13
15
  import {
14
16
  aggregateVerdicts,
15
17
  flattenComponentUsage,
@@ -33,6 +35,12 @@ export interface GovernScanOptions {
33
35
  report?: string;
34
36
  /** Suppress non-error output */
35
37
  quiet?: boolean;
38
+ /** Fragments Cloud API key — reports findings to Cloud */
39
+ apiKey?: string;
40
+ /** Fragments Cloud base URL (default: https://app.usefragments.com) */
41
+ cloudUrl?: string;
42
+ /** Only scan files changed vs a base ref (default base: auto-detected merge base) */
43
+ diff?: boolean | string;
36
44
  }
37
45
 
38
46
  export interface GovernWatchOptions extends GovernScanOptions {
@@ -40,6 +48,53 @@ export interface GovernWatchOptions extends GovernScanOptions {
40
48
  debounce?: number;
41
49
  }
42
50
 
51
+ // ---------------------------------------------------------------------------
52
+ // Git diff helpers
53
+ // ---------------------------------------------------------------------------
54
+
55
+ const SCANNABLE_EXTENSIONS = new Set([
56
+ '.tsx', '.ts', '.jsx', '.js',
57
+ ]);
58
+
59
+ function getChangedFiles(rootDir: string, base?: string): string[] | null {
60
+ try {
61
+ const baseRef = base || detectMergeBase(rootDir);
62
+ if (!baseRef) return null;
63
+
64
+ const output = execSync(
65
+ `git diff --name-only --diff-filter=ACMR ${baseRef}...HEAD`,
66
+ { cwd: rootDir, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] },
67
+ );
68
+
69
+ return output
70
+ .split('\n')
71
+ .map((f) => f.trim())
72
+ .filter((f) => f && SCANNABLE_EXTENSIONS.has(f.slice(f.lastIndexOf('.'))))
73
+ .map((f) => resolve(rootDir, f));
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ function detectMergeBase(rootDir: string): string | null {
80
+ try {
81
+ const remote = execSync('git rev-parse --abbrev-ref origin/HEAD', {
82
+ cwd: rootDir, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
83
+ }).trim();
84
+ if (remote) return remote;
85
+ } catch { /* fallback */ }
86
+
87
+ for (const candidate of ['origin/main', 'origin/master']) {
88
+ try {
89
+ execSync(`git rev-parse --verify ${candidate}`, {
90
+ cwd: rootDir, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
91
+ });
92
+ return candidate;
93
+ } catch { /* try next */ }
94
+ }
95
+ return null;
96
+ }
97
+
43
98
  // ---------------------------------------------------------------------------
44
99
  // Scan defaults — applied when no config file exists
45
100
  // ---------------------------------------------------------------------------
@@ -138,7 +193,6 @@ export async function governScan(
138
193
  let registryMap: Record<string, unknown> | undefined;
139
194
  let hasRegistry = false;
140
195
  {
141
- const { readFileSync, existsSync } = await import('node:fs');
142
196
  const fragmentsJsonPath = resolve(rootDir, 'fragments.json');
143
197
  if (existsSync(fragmentsJsonPath)) {
144
198
  try {
@@ -177,14 +231,36 @@ export async function governScan(
177
231
  : undefined,
178
232
  );
179
233
 
180
- // 6. Scan codebase
181
- if (!quiet) {
234
+ // 6. Scan codebase (optionally scoped to changed files via --diff)
235
+ let diffFiles: string[] | undefined;
236
+ if (options.diff) {
237
+ const base = typeof options.diff === 'string' ? options.diff : undefined;
238
+ const changed = getChangedFiles(rootDir, base);
239
+ if (changed && changed.length > 0) {
240
+ diffFiles = changed;
241
+ if (!quiet) {
242
+ console.log(pc.dim(` Diff mode: scanning ${changed.length} changed file(s)...\n`));
243
+ }
244
+ } else if (changed && changed.length === 0) {
245
+ if (!quiet) {
246
+ console.log(pc.green(' No scannable files changed — all clear.\n'));
247
+ }
248
+ return { exitCode: 0 };
249
+ } else {
250
+ if (!quiet) {
251
+ console.log(pc.yellow(' Could not detect git diff — falling back to full scan.\n'));
252
+ }
253
+ }
254
+ }
255
+
256
+ if (!quiet && !diffFiles) {
182
257
  console.log(pc.dim(' Scanning files...\n'));
183
258
  }
184
259
 
185
260
  const analysis = await scanCodebase({
186
261
  rootDir,
187
262
  useCache: true,
263
+ files: diffFiles,
188
264
  onProgress: quiet
189
265
  ? undefined
190
266
  : (progress) => {
@@ -233,7 +309,7 @@ export async function governScan(
233
309
  let passedFiles = 0;
234
310
  let totalViolations = 0;
235
311
  const violationCounts = new Map<string, number>();
236
- const allVerdicts: Awaited<ReturnType<typeof engine.check>>[] = [];
312
+ const allVerdicts: GovernanceVerdict[] = [];
237
313
 
238
314
  for (const [filePath, usages] of grouped) {
239
315
  const spec = usagesToSpec(usages, filePath, rootDir);
@@ -335,9 +411,185 @@ export async function governScan(
335
411
  }
336
412
  }
337
413
 
414
+ // Report to Fragments Cloud
415
+ if (options.apiKey) {
416
+ await reportToCloud({
417
+ apiKey: options.apiKey,
418
+ cloudUrl: options.cloudUrl,
419
+ verdicts: allVerdicts,
420
+ rootDir,
421
+ quiet,
422
+ diffOnly: !!diffFiles,
423
+ });
424
+ }
425
+
338
426
  return { exitCode: passedFiles === totalFiles ? 0 : 1 };
339
427
  }
340
428
 
429
+ // ---------------------------------------------------------------------------
430
+ // Cloud reporting
431
+ // ---------------------------------------------------------------------------
432
+
433
+ const DEFAULT_CLOUD_URL = 'https://app.usefragments.com';
434
+
435
+ interface CloudReportOptions {
436
+ apiKey: string;
437
+ cloudUrl?: string;
438
+ verdicts: GovernanceVerdict[];
439
+ rootDir: string;
440
+ quiet: boolean;
441
+ diffOnly?: boolean;
442
+ }
443
+
444
+ function detectGitMetadata(): {
445
+ commitSha?: string;
446
+ branch?: string;
447
+ pr?: number;
448
+ repoFullName?: string;
449
+ } {
450
+ const env = process.env;
451
+ const meta: ReturnType<typeof detectGitMetadata> = {};
452
+
453
+ if (env.GITHUB_SHA) meta.commitSha = env.GITHUB_SHA;
454
+ if (env.GITHUB_REPOSITORY) meta.repoFullName = env.GITHUB_REPOSITORY;
455
+ meta.branch = env.GITHUB_HEAD_REF || env.GITHUB_REF_NAME;
456
+
457
+ if (env.GITHUB_EVENT_NAME === 'pull_request' && env.GITHUB_EVENT_PATH) {
458
+ try {
459
+ const event = JSON.parse(readFileSync(env.GITHUB_EVENT_PATH, 'utf-8'));
460
+ if (event?.pull_request?.number) {
461
+ meta.pr = event.pull_request.number;
462
+ }
463
+ if (event?.pull_request?.head?.sha) {
464
+ meta.commitSha = event.pull_request.head?.sha;
465
+ }
466
+ } catch {
467
+ // Event file unreadable — skip PR number
468
+ }
469
+ }
470
+
471
+ return meta;
472
+ }
473
+
474
+ async function reportToCloud(options: CloudReportOptions): Promise<void> {
475
+ const { apiKey, verdicts, rootDir, quiet, diffOnly } = options;
476
+ const baseUrl = (options.cloudUrl || DEFAULT_CLOUD_URL).replace(/\/+$/, '');
477
+
478
+ const findings: Array<{
479
+ ruleId: string;
480
+ severity: 'error' | 'warning' | 'info';
481
+ filePath?: string;
482
+ line?: number;
483
+ column?: number;
484
+ rawValue?: string;
485
+ suggestedToken?: string;
486
+ message: string;
487
+ category?: string;
488
+ fingerprint: string;
489
+ }> = [];
490
+
491
+ const { createHash } = await import('node:crypto');
492
+
493
+ for (const verdict of verdicts) {
494
+ for (const result of verdict.results) {
495
+ for (const v of result.violations) {
496
+ const severity = v.severity === 'critical' || v.severity === 'serious'
497
+ ? 'error'
498
+ : v.severity === 'moderate'
499
+ ? 'warning'
500
+ : 'info';
501
+
502
+ const fingerprint = createHash('sha256')
503
+ .update(`${v.rule}:${v.nodeType}:${v.nodeId}:${v.message}`)
504
+ .digest('hex')
505
+ .slice(0, 16);
506
+
507
+ let filePath: string | undefined;
508
+ let line: number | undefined;
509
+ let column: number | undefined;
510
+
511
+ if (v.filePath) {
512
+ filePath = v.filePath;
513
+ line = v.line;
514
+ column = v.column;
515
+ } else if (v.nodeId) {
516
+ const parts = v.nodeId.split(':');
517
+ if (parts.length >= 3) {
518
+ const col = parseInt(parts.pop()!, 10);
519
+ const ln = parseInt(parts.pop()!, 10);
520
+ const path = parts.join(':');
521
+ if (!isNaN(ln) && !isNaN(col) && path) {
522
+ filePath = path;
523
+ line = ln;
524
+ column = col;
525
+ }
526
+ }
527
+ if (!filePath) {
528
+ filePath = relative(rootDir, v.nodeId);
529
+ }
530
+ }
531
+
532
+ findings.push({
533
+ ruleId: v.rule,
534
+ severity,
535
+ filePath,
536
+ line,
537
+ column,
538
+ rawValue: v.rawValue,
539
+ message: `[${result.validator}] ${v.message}`,
540
+ category: result.validator,
541
+ fingerprint,
542
+ suggestedToken: v.suggestion,
543
+ });
544
+ }
545
+ }
546
+ }
547
+
548
+ if (!quiet) {
549
+ console.log(pc.dim(` Reporting ${findings.length} finding(s) to Fragments Cloud...`));
550
+ }
551
+
552
+ const gitMeta = detectGitMetadata();
553
+
554
+ try {
555
+ const response = await fetch(`${baseUrl}/api/govern/ingest`, {
556
+ method: 'POST',
557
+ headers: {
558
+ 'Content-Type': 'application/json',
559
+ 'Authorization': `Bearer ${apiKey}`,
560
+ },
561
+ body: JSON.stringify({
562
+ findings,
563
+ source: 'ci',
564
+ diffOnly: diffOnly ?? false,
565
+ ...gitMeta,
566
+ }),
567
+ });
568
+
569
+ if (!response.ok) {
570
+ const body = await response.json().catch(() => ({}));
571
+ const msg = (body as { error?: string }).error ?? `HTTP ${response.status}`;
572
+ console.error(pc.red(` ✗ Cloud report failed: ${msg}\n`));
573
+ return;
574
+ }
575
+
576
+ const body = await response.json() as { ingested?: number; orgSlug?: string };
577
+ if (!quiet) {
578
+ console.log(
579
+ pc.green(` ✓ Reported ${body.ingested ?? findings.length} finding(s) to Cloud`) +
580
+ (body.orgSlug ? pc.dim(` (${body.orgSlug})`) : '') +
581
+ '\n',
582
+ );
583
+ }
584
+ } catch (err) {
585
+ console.error(
586
+ pc.red(` ✗ Cloud report failed: `) +
587
+ pc.dim(err instanceof Error ? err.message : 'Network error') +
588
+ '\n',
589
+ );
590
+ }
591
+ }
592
+
341
593
  // ---------------------------------------------------------------------------
342
594
  // governWatch
343
595
  // ---------------------------------------------------------------------------
@@ -207,7 +207,6 @@ describe('CLI MCP server integration', () => {
207
207
  expect(names).toContain('inspect');
208
208
  expect(names).toContain('blocks');
209
209
  expect(names).toContain('tokens');
210
- expect(names).toContain('implement');
211
210
 
212
211
  const prefixedResult = await client.callTool({
213
212
  name: 'fragments_discover',
@@ -314,29 +313,19 @@ describe('CLI MCP server integration', () => {
314
313
  });
315
314
  });
316
315
 
317
- it('returns login-oriented implement results and respects component limits', async () => {
316
+ it('accepts discover depth:full without error', async () => {
318
317
  await withClient(async (client) => {
319
- const defaultResult = await client.callTool({
320
- name: 'implement',
321
- arguments: { useCase: 'login form' },
322
- });
323
- const defaultPayload = getTextPayload(defaultResult) as {
324
- components: Array<{ name: string }>;
325
- blocks?: Array<{ name: string }>;
326
- };
327
-
328
- expect(defaultPayload.components.length).toBeGreaterThan(0);
329
- expect(defaultPayload.blocks?.[0]?.name).toBe('Login Form');
330
-
331
- const limitedResult = await client.callTool({
332
- name: 'implement',
333
- arguments: { useCase: 'login form', limit: 1 },
318
+ const result = await client.callTool({
319
+ name: 'discover',
320
+ arguments: { useCase: 'button', depth: 'full' },
334
321
  });
335
- const limitedPayload = getTextPayload(limitedResult) as {
336
- components: Array<{ name: string }>;
322
+ const payload = getTextPayload(result) as {
323
+ useCase: string;
324
+ suggestions: Array<{ component: string }>;
337
325
  };
338
326
 
339
- expect(limitedPayload.components).toHaveLength(1);
327
+ expect(payload.useCase).toBe('button');
328
+ expect(payload.suggestions).toBeDefined();
340
329
  });
341
330
  });
342
331
  });