@fragments-sdk/cli 0.16.0 → 0.17.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.
- package/dist/bin.js +24 -21
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-77AAP6R6.js → chunk-ANTWP3UG.js} +2 -2
- package/dist/{chunk-G6UVWMFU.js → chunk-B4A4ZEGS.js} +5 -5
- package/dist/{chunk-SJFSG7QF.js → chunk-FFCI6OVZ.js} +3 -1
- package/dist/chunk-FFCI6OVZ.js.map +1 -0
- package/dist/{chunk-XRADMHMV.js → chunk-HNHE64CR.js} +2 -2
- package/dist/{chunk-ACX7YWZW.js → chunk-MN3B2EE6.js} +2 -2
- package/dist/{chunk-QCN35LJU.js → chunk-SAQW37L5.js} +3 -2
- package/dist/chunk-SAQW37L5.js.map +1 -0
- package/dist/{chunk-OZZ4SVZX.js → chunk-SNZXGHL2.js} +2 -2
- package/dist/{chunk-ACFVKMVZ.js → chunk-VT2J62ND.js} +4 -4
- package/dist/{codebase-scanner-MQHUZC2G.js → codebase-scanner-2T5QIDBA.js} +2 -2
- package/dist/core/index.js +1 -1
- package/dist/{create-3ZFYQB3T.js → create-D44QD7MV.js} +2 -2
- package/dist/{doctor-4IDUM7HI.js → doctor-7B5N4JYU.js} +2 -2
- package/dist/{generate-VNUUWVWQ.js → generate-T47JZRVU.js} +3 -3
- package/dist/{govern-scan-HTACKYPF.js → govern-scan-X6UEIOSV.js} +187 -7
- package/dist/govern-scan-X6UEIOSV.js.map +1 -0
- package/dist/index.js +6 -6
- package/dist/{init-PXFRAQ64.js → init-2RGAY4W6.js} +5 -5
- package/dist/mcp-bin.js +2 -2
- package/dist/{scan-L4GWGEZX.js → scan-A2WJM54L.js} +6 -6
- package/dist/{scan-generate-74EYSAGH.js → scan-generate-LUSOHT36.js} +3 -3
- package/dist/{service-VELQHEWV.js → service-ROCP7TKG.js} +4 -4
- package/dist/{snapshot-DT4B6DPR.js → snapshot-B3SAW74Y.js} +2 -2
- package/dist/{static-viewer-E4OJWFDJ.js → static-viewer-7L6UEYTJ.js} +3 -3
- package/dist/{test-QJY2QO4X.js → test-PQDVDURE.js} +3 -3
- package/dist/{token-normalizer-56H4242J.js → token-normalizer-7TFCVDZL.js} +2 -2
- package/dist/{tokens-K6URXFPK.js → tokens-64FG5FDP.js} +5 -5
- package/dist/{tokens-generate-EL6IN536.js → tokens-generate-CL4LBBQA.js} +2 -2
- package/package.json +4 -4
- package/src/bin.ts +6 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.contract.json +1 -1
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.contract.json +1 -1
- package/src/commands/govern-scan.ts +257 -5
- package/src/mcp/__tests__/server.integration.test.ts +9 -20
- package/src/service/enhance/codebase-scanner.ts +3 -2
- package/src/service/enhance/types.ts +3 -0
- package/dist/chunk-QCN35LJU.js.map +0 -1
- package/dist/chunk-SJFSG7QF.js.map +0 -1
- package/dist/govern-scan-HTACKYPF.js.map +0 -1
- /package/dist/{chunk-77AAP6R6.js.map → chunk-ANTWP3UG.js.map} +0 -0
- /package/dist/{chunk-G6UVWMFU.js.map → chunk-B4A4ZEGS.js.map} +0 -0
- /package/dist/{chunk-XRADMHMV.js.map → chunk-HNHE64CR.js.map} +0 -0
- /package/dist/{chunk-ACX7YWZW.js.map → chunk-MN3B2EE6.js.map} +0 -0
- /package/dist/{chunk-OZZ4SVZX.js.map → chunk-SNZXGHL2.js.map} +0 -0
- /package/dist/{chunk-ACFVKMVZ.js.map → chunk-VT2J62ND.js.map} +0 -0
- /package/dist/{codebase-scanner-MQHUZC2G.js.map → codebase-scanner-2T5QIDBA.js.map} +0 -0
- /package/dist/{create-3ZFYQB3T.js.map → create-D44QD7MV.js.map} +0 -0
- /package/dist/{doctor-4IDUM7HI.js.map → doctor-7B5N4JYU.js.map} +0 -0
- /package/dist/{generate-VNUUWVWQ.js.map → generate-T47JZRVU.js.map} +0 -0
- /package/dist/{init-PXFRAQ64.js.map → init-2RGAY4W6.js.map} +0 -0
- /package/dist/{scan-L4GWGEZX.js.map → scan-A2WJM54L.js.map} +0 -0
- /package/dist/{scan-generate-74EYSAGH.js.map → scan-generate-LUSOHT36.js.map} +0 -0
- /package/dist/{service-VELQHEWV.js.map → service-ROCP7TKG.js.map} +0 -0
- /package/dist/{snapshot-DT4B6DPR.js.map → snapshot-B3SAW74Y.js.map} +0 -0
- /package/dist/{static-viewer-E4OJWFDJ.js.map → static-viewer-7L6UEYTJ.js.map} +0 -0
- /package/dist/{test-QJY2QO4X.js.map → test-PQDVDURE.js.map} +0 -0
- /package/dist/{token-normalizer-56H4242J.js.map → token-normalizer-7TFCVDZL.js.map} +0 -0
- /package/dist/{tokens-K6URXFPK.js.map → tokens-64FG5FDP.js.map} +0 -0
- /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-
|
|
8
|
+
} from "./chunk-MN3B2EE6.js";
|
|
9
9
|
import "./chunk-D2CDBRNU.js";
|
|
10
10
|
import {
|
|
11
11
|
BRAND
|
|
12
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
5
|
-
import "./chunk-
|
|
6
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-B4A4ZEGS.js";
|
|
5
|
+
import "./chunk-HNHE64CR.js";
|
|
6
|
+
import "./chunk-ANTWP3UG.js";
|
|
7
7
|
import "./chunk-D2CDBRNU.js";
|
|
8
|
-
import "./chunk-
|
|
9
|
-
import "./chunk-
|
|
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-
|
|
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-
|
|
4
|
+
} from "./chunk-HNHE64CR.js";
|
|
5
5
|
import "./chunk-D2CDBRNU.js";
|
|
6
6
|
import {
|
|
7
7
|
BRAND
|
|
8
|
-
} from "./chunk-
|
|
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-
|
|
1403
|
+
//# sourceMappingURL=scan-generate-LUSOHT36.js.map
|
|
@@ -81,12 +81,12 @@ import {
|
|
|
81
81
|
sanitizeFilename,
|
|
82
82
|
shutdownSharedPool,
|
|
83
83
|
sleep
|
|
84
|
-
} from "./chunk-
|
|
84
|
+
} from "./chunk-ANTWP3UG.js";
|
|
85
85
|
import "./chunk-D2CDBRNU.js";
|
|
86
86
|
import {
|
|
87
87
|
BRAND,
|
|
88
88
|
DEFAULTS
|
|
89
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
5
|
+
} from "./chunk-SNZXGHL2.js";
|
|
6
6
|
import "./chunk-D2CDBRNU.js";
|
|
7
|
-
import "./chunk-
|
|
7
|
+
import "./chunk-FFCI6OVZ.js";
|
|
8
8
|
export {
|
|
9
9
|
generateStaticViewer,
|
|
10
10
|
generateViewerFromJson
|
|
11
11
|
};
|
|
12
|
-
//# sourceMappingURL=static-viewer-
|
|
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-
|
|
6
|
-
import "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
4
|
+
} from "./chunk-HNHE64CR.js";
|
|
5
5
|
import {
|
|
6
6
|
parseTokenFiles
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-ANTWP3UG.js";
|
|
8
8
|
import "./chunk-D2CDBRNU.js";
|
|
9
9
|
import {
|
|
10
10
|
BRAND
|
|
11
|
-
} from "./chunk-
|
|
12
|
-
import "./chunk-
|
|
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-
|
|
175
|
+
//# sourceMappingURL=tokens-64FG5FDP.js.map
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
generateFigmaVariables,
|
|
6
6
|
generateSCSSVariables,
|
|
7
7
|
generateTailwindConfig
|
|
8
|
-
} from "./chunk-
|
|
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-
|
|
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.
|
|
3
|
+
"version": "0.17.0",
|
|
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",
|
|
@@ -84,11 +84,11 @@
|
|
|
84
84
|
"zod": "^3.24.1",
|
|
85
85
|
"@fragments-sdk/compiler": "0.2.1",
|
|
86
86
|
"@fragments-sdk/extract": "0.1.1",
|
|
87
|
-
"@fragments-sdk/core": "2.1.0",
|
|
88
87
|
"@fragments-sdk/context": "0.6.1",
|
|
89
|
-
"@fragments-sdk/govern": "^0.3.
|
|
88
|
+
"@fragments-sdk/govern": "^0.3.2",
|
|
90
89
|
"@fragments-sdk/viewer": "0.2.10",
|
|
91
|
-
"@fragments-sdk/webmcp": "3.0.0"
|
|
90
|
+
"@fragments-sdk/webmcp": "3.0.0",
|
|
91
|
+
"@fragments-sdk/core": "2.1.0"
|
|
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,
|
|
1392
|
+
cloudUrl: options.cloudUrl ?? process.env.FRAGMENTS_CLOUD_URL,
|
|
1393
|
+
diff: options.diff,
|
|
1388
1394
|
});
|
|
1389
1395
|
process.exit(exitCode);
|
|
1390
1396
|
} catch (error) {
|
|
@@ -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
|
-
|
|
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:
|
|
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
|
+
if (env.GITHUB_REF_NAME) meta.branch = 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('
|
|
316
|
+
it('accepts discover depth:full without error', async () => {
|
|
318
317
|
await withClient(async (client) => {
|
|
319
|
-
const
|
|
320
|
-
name: '
|
|
321
|
-
arguments: { useCase: '
|
|
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
|
|
336
|
-
|
|
322
|
+
const payload = getTextPayload(result) as {
|
|
323
|
+
useCase: string;
|
|
324
|
+
suggestions: Array<{ component: string }>;
|
|
337
325
|
};
|
|
338
326
|
|
|
339
|
-
expect(
|
|
327
|
+
expect(payload.useCase).toBe('button');
|
|
328
|
+
expect(payload.suggestions).toBeDefined();
|
|
340
329
|
});
|
|
341
330
|
});
|
|
342
331
|
});
|