@fragments-sdk/cli 0.15.10 → 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 +901 -789
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-6SQPP47U.js → chunk-ANTWP3UG.js} +532 -31
- package/dist/chunk-ANTWP3UG.js.map +1 -0
- package/dist/{chunk-ONUP6Z4W.js → chunk-B4A4ZEGS.js} +9 -9
- package/dist/{chunk-32LIWN2P.js → chunk-FFCI6OVZ.js} +584 -261
- package/dist/chunk-FFCI6OVZ.js.map +1 -0
- package/dist/{chunk-HQ6A6DTV.js → chunk-HNHE64CR.js} +315 -1089
- package/dist/chunk-HNHE64CR.js.map +1 -0
- package/dist/{chunk-BJE3425I.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-2WXKALIG.js → chunk-SNZXGHL2.js} +2 -2
- package/dist/{chunk-5JF26E55.js → chunk-VT2J62ND.js} +11 -11
- package/dist/{codebase-scanner-MQHUZC2G.js → codebase-scanner-2T5QIDBA.js} +2 -2
- package/dist/core/index.js +53 -1
- package/dist/{create-EXURTBKK.js → create-D44QD7MV.js} +2 -2
- package/dist/{doctor-BDPMYYE6.js → doctor-7B5N4JYU.js} +2 -2
- package/dist/{generate-PVOLUAAC.js → generate-T47JZRVU.js} +4 -4
- package/dist/govern-scan-X6UEIOSV.js +632 -0
- package/dist/govern-scan-X6UEIOSV.js.map +1 -0
- package/dist/index.js +7 -8
- package/dist/index.js.map +1 -1
- package/dist/{init-SSGUSP7Z.js → init-2RGAY4W6.js} +5 -5
- package/dist/mcp-bin.js +2 -2
- package/dist/scan-A2WJM54L.js +14 -0
- package/dist/{scan-generate-VY27PIOX.js → scan-generate-LUSOHT36.js} +4 -4
- package/dist/{service-QJGWUIVL.js → service-ROCP7TKG.js} +13 -15
- package/dist/{snapshot-WIJMEIFT.js → snapshot-B3SAW74Y.js} +2 -2
- package/dist/{static-viewer-7QIBQZRC.js → static-viewer-7L6UEYTJ.js} +3 -3
- package/dist/{test-64Z5BKBA.js → test-PQDVDURE.js} +3 -3
- package/dist/{token-normalizer-TEPOVBPV.js → token-normalizer-7TFCVDZL.js} +2 -2
- package/dist/{tokens-NZWFQIAB.js → tokens-64FG5FDP.js} +8 -9
- package/dist/{tokens-NZWFQIAB.js.map → tokens-64FG5FDP.js.map} +1 -1
- package/dist/{tokens-generate-5JQSJ27E.js → tokens-generate-CL4LBBQA.js} +2 -2
- package/package.json +9 -8
- package/src/bin.ts +55 -88
- 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/__tests__/context-cloud.test.ts +291 -0
- package/src/commands/__tests__/govern-scan.test.ts +185 -0
- package/src/commands/__tests__/govern.test.ts +1 -0
- package/src/commands/context-cloud.ts +355 -0
- package/src/commands/govern-scan-report.ts +170 -0
- package/src/commands/govern-scan.ts +282 -135
- package/src/commands/govern.ts +0 -157
- 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-32LIWN2P.js.map +0 -1
- package/dist/chunk-6SQPP47U.js.map +0 -1
- package/dist/chunk-HQ6A6DTV.js.map +0 -1
- package/dist/chunk-MHIBEEW4.js +0 -511
- package/dist/chunk-MHIBEEW4.js.map +0 -1
- package/dist/chunk-QCN35LJU.js.map +0 -1
- package/dist/govern-scan-DW4QUAYD.js +0 -414
- package/dist/govern-scan-DW4QUAYD.js.map +0 -1
- package/dist/init-cloud-3DNKPWFB.js +0 -304
- package/dist/init-cloud-3DNKPWFB.js.map +0 -1
- package/dist/node-37AUE74M.js +0 -65
- package/dist/push-contracts-WY32TFP6.js +0 -84
- package/dist/push-contracts-WY32TFP6.js.map +0 -1
- package/dist/scan-PKSYSTRR.js +0 -15
- package/dist/static-viewer-7QIBQZRC.js.map +0 -1
- package/dist/token-parser-32KOIOFN.js +0 -22
- package/dist/token-parser-32KOIOFN.js.map +0 -1
- package/dist/tokens-push-HY3KO36V.js +0 -148
- package/dist/tokens-push-HY3KO36V.js.map +0 -1
- package/src/commands/init-cloud.ts +0 -382
- package/src/commands/push-contracts.ts +0 -112
- package/src/commands/tokens-push.ts +0 -199
- /package/dist/{chunk-ONUP6Z4W.js.map → chunk-B4A4ZEGS.js.map} +0 -0
- /package/dist/{chunk-BJE3425I.js.map → chunk-MN3B2EE6.js.map} +0 -0
- /package/dist/{chunk-2WXKALIG.js.map → chunk-SNZXGHL2.js.map} +0 -0
- /package/dist/{chunk-5JF26E55.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-EXURTBKK.js.map → create-D44QD7MV.js.map} +0 -0
- /package/dist/{doctor-BDPMYYE6.js.map → doctor-7B5N4JYU.js.map} +0 -0
- /package/dist/{generate-PVOLUAAC.js.map → generate-T47JZRVU.js.map} +0 -0
- /package/dist/{init-SSGUSP7Z.js.map → init-2RGAY4W6.js.map} +0 -0
- /package/dist/{node-37AUE74M.js.map → scan-A2WJM54L.js.map} +0 -0
- /package/dist/{scan-generate-VY27PIOX.js.map → scan-generate-LUSOHT36.js.map} +0 -0
- /package/dist/{scan-PKSYSTRR.js.map → service-ROCP7TKG.js.map} +0 -0
- /package/dist/{snapshot-WIJMEIFT.js.map → snapshot-B3SAW74Y.js.map} +0 -0
- /package/dist/{service-QJGWUIVL.js.map → static-viewer-7L6UEYTJ.js.map} +0 -0
- /package/dist/{test-64Z5BKBA.js.map → test-PQDVDURE.js.map} +0 -0
- /package/dist/{token-normalizer-TEPOVBPV.js.map → token-normalizer-7TFCVDZL.js.map} +0 -0
- /package/dist/{tokens-generate-5JQSJ27E.js.map → tokens-generate-CL4LBBQA.js.map} +0 -0
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
import { createRequire as __banner_createRequire } from 'module'; const require = __banner_createRequire(import.meta.url);
|
|
2
|
+
import "./chunk-D2CDBRNU.js";
|
|
3
|
+
import {
|
|
4
|
+
BRAND
|
|
5
|
+
} from "./chunk-FFCI6OVZ.js";
|
|
6
|
+
|
|
7
|
+
// src/commands/govern-scan.ts
|
|
8
|
+
import pc from "picocolors";
|
|
9
|
+
import { resolve as resolve2, relative as relative2 } from "path";
|
|
10
|
+
import { existsSync, readFileSync } from "fs";
|
|
11
|
+
import { execSync } from "child_process";
|
|
12
|
+
|
|
13
|
+
// src/commands/govern-scan-report.ts
|
|
14
|
+
import { resolve, dirname, relative } from "path";
|
|
15
|
+
var SEVERITY_RANK = {
|
|
16
|
+
critical: 4,
|
|
17
|
+
serious: 3,
|
|
18
|
+
moderate: 2,
|
|
19
|
+
minor: 1
|
|
20
|
+
};
|
|
21
|
+
function mergeSeverity(a, b) {
|
|
22
|
+
return SEVERITY_RANK[a] >= SEVERITY_RANK[b] ? a : b;
|
|
23
|
+
}
|
|
24
|
+
function aggregateVerdicts(verdicts, computeScore, runner = "cli") {
|
|
25
|
+
if (verdicts.length === 0) {
|
|
26
|
+
return {
|
|
27
|
+
passed: true,
|
|
28
|
+
score: 100,
|
|
29
|
+
results: [],
|
|
30
|
+
metadata: {
|
|
31
|
+
runner,
|
|
32
|
+
duration: 0,
|
|
33
|
+
nodeCount: 0,
|
|
34
|
+
componentTypes: []
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const byValidator = /* @__PURE__ */ new Map();
|
|
39
|
+
let duration = 0;
|
|
40
|
+
let nodeCount = 0;
|
|
41
|
+
const componentTypes = /* @__PURE__ */ new Set();
|
|
42
|
+
for (const verdict of verdicts) {
|
|
43
|
+
duration += verdict.metadata.duration;
|
|
44
|
+
nodeCount += verdict.metadata.nodeCount;
|
|
45
|
+
for (const type of verdict.metadata.componentTypes) {
|
|
46
|
+
componentTypes.add(type);
|
|
47
|
+
}
|
|
48
|
+
for (const result of verdict.results) {
|
|
49
|
+
const existing = byValidator.get(result.validator);
|
|
50
|
+
if (!existing) {
|
|
51
|
+
byValidator.set(result.validator, {
|
|
52
|
+
validator: result.validator,
|
|
53
|
+
severity: result.severity,
|
|
54
|
+
passed: result.passed,
|
|
55
|
+
violations: [...result.violations],
|
|
56
|
+
suggestions: result.suggestions ? [...result.suggestions] : void 0
|
|
57
|
+
});
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
existing.passed = existing.passed && result.passed;
|
|
61
|
+
existing.severity = mergeSeverity(existing.severity, result.severity);
|
|
62
|
+
existing.violations.push(...result.violations);
|
|
63
|
+
if (result.suggestions?.length) {
|
|
64
|
+
const merged = existing.suggestions ?? [];
|
|
65
|
+
merged.push(...result.suggestions);
|
|
66
|
+
existing.suggestions = merged;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const results = [...byValidator.values()].sort(
|
|
71
|
+
(a, b) => a.validator.localeCompare(b.validator)
|
|
72
|
+
);
|
|
73
|
+
const allViolations = results.flatMap((result) => result.violations);
|
|
74
|
+
return {
|
|
75
|
+
passed: verdicts.every((verdict) => verdict.passed),
|
|
76
|
+
score: computeScore(allViolations),
|
|
77
|
+
results,
|
|
78
|
+
metadata: {
|
|
79
|
+
runner,
|
|
80
|
+
duration,
|
|
81
|
+
nodeCount,
|
|
82
|
+
componentTypes: [...componentTypes].sort()
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function flattenComponentUsage(usages, rootDir) {
|
|
87
|
+
const counts = /* @__PURE__ */ new Map();
|
|
88
|
+
for (const usage of usages) {
|
|
89
|
+
const relPath = relative(rootDir, usage.filePath);
|
|
90
|
+
const key = `${relPath}\0${usage.componentName}`;
|
|
91
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
92
|
+
}
|
|
93
|
+
return [...counts.entries()].map(([key, occurrences]) => {
|
|
94
|
+
const separatorIndex = key.indexOf("\0");
|
|
95
|
+
const file = key.slice(0, separatorIndex);
|
|
96
|
+
const component = key.slice(separatorIndex + 1);
|
|
97
|
+
return { component, file, occurrences };
|
|
98
|
+
}).sort(
|
|
99
|
+
(a, b) => a.file === b.file ? a.component.localeCompare(b.component) : a.file.localeCompare(b.file)
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
function buildComplianceSummary(health) {
|
|
103
|
+
const usageTotals = Object.values(health.components).reduce(
|
|
104
|
+
(acc, component) => {
|
|
105
|
+
acc.passingUsages += component.passed;
|
|
106
|
+
acc.totalUsages += component.total;
|
|
107
|
+
return acc;
|
|
108
|
+
},
|
|
109
|
+
{ passingUsages: 0, totalUsages: 0 }
|
|
110
|
+
);
|
|
111
|
+
return {
|
|
112
|
+
complianceRate: health.overallCompliance,
|
|
113
|
+
passingUsages: usageTotals.passingUsages,
|
|
114
|
+
totalUsages: usageTotals.totalUsages,
|
|
115
|
+
contractedCount: health.contractedComponents,
|
|
116
|
+
detectedCount: health.totalComponents
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
async function writeGovernScanReport(path, report) {
|
|
120
|
+
const { mkdir, writeFile } = await import("fs/promises");
|
|
121
|
+
const absPath = resolve(path);
|
|
122
|
+
await mkdir(dirname(absPath), { recursive: true });
|
|
123
|
+
await writeFile(absPath, JSON.stringify(report, null, 2), "utf-8");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/commands/govern-scan.ts
|
|
127
|
+
var SCANNABLE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
128
|
+
".tsx",
|
|
129
|
+
".ts",
|
|
130
|
+
".jsx",
|
|
131
|
+
".js"
|
|
132
|
+
]);
|
|
133
|
+
function getChangedFiles(rootDir, base) {
|
|
134
|
+
try {
|
|
135
|
+
const baseRef = base || detectMergeBase(rootDir);
|
|
136
|
+
if (!baseRef) return null;
|
|
137
|
+
const output = execSync(
|
|
138
|
+
`git diff --name-only --diff-filter=ACMR ${baseRef}...HEAD`,
|
|
139
|
+
{ cwd: rootDir, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
140
|
+
);
|
|
141
|
+
return output.split("\n").map((f) => f.trim()).filter((f) => f && SCANNABLE_EXTENSIONS.has(f.slice(f.lastIndexOf(".")))).map((f) => resolve2(rootDir, f));
|
|
142
|
+
} catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function detectMergeBase(rootDir) {
|
|
147
|
+
try {
|
|
148
|
+
const remote = execSync("git rev-parse --abbrev-ref origin/HEAD", {
|
|
149
|
+
cwd: rootDir,
|
|
150
|
+
encoding: "utf-8",
|
|
151
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
152
|
+
}).trim();
|
|
153
|
+
if (remote) return remote;
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
for (const candidate of ["origin/main", "origin/master"]) {
|
|
157
|
+
try {
|
|
158
|
+
execSync(`git rev-parse --verify ${candidate}`, {
|
|
159
|
+
cwd: rootDir,
|
|
160
|
+
encoding: "utf-8",
|
|
161
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
162
|
+
});
|
|
163
|
+
return candidate;
|
|
164
|
+
} catch {
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
var SCAN_DEFAULT_RULES = {
|
|
170
|
+
"safety/block-event-handlers": true,
|
|
171
|
+
"safety/block-dangerous-props": true,
|
|
172
|
+
"safety/block-controlled-props": true,
|
|
173
|
+
"safety/block-function-props": true,
|
|
174
|
+
"safety/sanitize-hrefs": true,
|
|
175
|
+
"tokens/require-design-tokens": true
|
|
176
|
+
};
|
|
177
|
+
function detectRootDir(cwd) {
|
|
178
|
+
const candidates = ["src", "app", "pages", "components"];
|
|
179
|
+
for (const dir of candidates) {
|
|
180
|
+
if (existsSync(resolve2(cwd, dir))) {
|
|
181
|
+
return cwd;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return cwd;
|
|
185
|
+
}
|
|
186
|
+
function groupByFile(usages) {
|
|
187
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
188
|
+
for (const usage of usages) {
|
|
189
|
+
const existing = grouped.get(usage.filePath);
|
|
190
|
+
if (existing) {
|
|
191
|
+
existing.push(usage);
|
|
192
|
+
} else {
|
|
193
|
+
grouped.set(usage.filePath, [usage]);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return grouped;
|
|
197
|
+
}
|
|
198
|
+
async function governScan(options = {}) {
|
|
199
|
+
const {
|
|
200
|
+
loadPolicy,
|
|
201
|
+
createEngine,
|
|
202
|
+
buildAdaptersFromConfig,
|
|
203
|
+
formatVerdict,
|
|
204
|
+
computeComponentHealth,
|
|
205
|
+
computeScore
|
|
206
|
+
} = await import("@fragments-sdk/govern");
|
|
207
|
+
const { scanCodebase } = await import("./codebase-scanner-2T5QIDBA.js");
|
|
208
|
+
const { usagesToSpec } = await import("./converter-7XM3Y6NJ.js");
|
|
209
|
+
const format = options.format ?? "summary";
|
|
210
|
+
const quiet = options.quiet ?? false;
|
|
211
|
+
if (!quiet) {
|
|
212
|
+
console.log(pc.cyan(`
|
|
213
|
+
${BRAND.name} Governance Scan
|
|
214
|
+
`));
|
|
215
|
+
}
|
|
216
|
+
const rootDir = resolve2(options.dir ?? detectRootDir(process.cwd()));
|
|
217
|
+
if (!quiet) {
|
|
218
|
+
console.log(pc.dim(` Root: ${rootDir}
|
|
219
|
+
`));
|
|
220
|
+
}
|
|
221
|
+
let policy = await loadPolicy(options.config);
|
|
222
|
+
const hasRules = Object.keys(policy.rules).length > 0;
|
|
223
|
+
if (!hasRules) {
|
|
224
|
+
policy = { ...policy, rules: SCAN_DEFAULT_RULES };
|
|
225
|
+
if (!quiet) {
|
|
226
|
+
console.log(pc.dim(" No config found \u2014 using scan defaults (safety + tokens)\n"));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
let registryMap;
|
|
230
|
+
let hasRegistry = false;
|
|
231
|
+
{
|
|
232
|
+
const fragmentsJsonPath = resolve2(rootDir, "fragments.json");
|
|
233
|
+
if (existsSync(fragmentsJsonPath)) {
|
|
234
|
+
try {
|
|
235
|
+
const raw = readFileSync(fragmentsJsonPath, "utf-8");
|
|
236
|
+
const parsed = JSON.parse(raw);
|
|
237
|
+
if (parsed.fragments && Array.isArray(parsed.fragments)) {
|
|
238
|
+
const map = {};
|
|
239
|
+
for (const fragment of parsed.fragments) {
|
|
240
|
+
if (fragment.meta?.name) {
|
|
241
|
+
map[fragment.meta.name] = fragment;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
registryMap = map;
|
|
245
|
+
hasRegistry = true;
|
|
246
|
+
if (!quiet) {
|
|
247
|
+
console.log(
|
|
248
|
+
pc.dim(` Contract registry loaded (${parsed.fragments.length} components)
|
|
249
|
+
`)
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
} catch {
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const adapters = buildAdaptersFromConfig(policy.audit);
|
|
258
|
+
const engine = createEngine(
|
|
259
|
+
policy,
|
|
260
|
+
adapters,
|
|
261
|
+
registryMap ? { registry: { fragments: registryMap } } : void 0
|
|
262
|
+
);
|
|
263
|
+
let diffFiles;
|
|
264
|
+
if (options.diff) {
|
|
265
|
+
const base = typeof options.diff === "string" ? options.diff : void 0;
|
|
266
|
+
const changed = getChangedFiles(rootDir, base);
|
|
267
|
+
if (changed && changed.length > 0) {
|
|
268
|
+
diffFiles = changed;
|
|
269
|
+
if (!quiet) {
|
|
270
|
+
console.log(pc.dim(` Diff mode: scanning ${changed.length} changed file(s)...
|
|
271
|
+
`));
|
|
272
|
+
}
|
|
273
|
+
} else if (changed && changed.length === 0) {
|
|
274
|
+
if (!quiet) {
|
|
275
|
+
console.log(pc.green(" No scannable files changed \u2014 all clear.\n"));
|
|
276
|
+
}
|
|
277
|
+
return { exitCode: 0 };
|
|
278
|
+
} else {
|
|
279
|
+
if (!quiet) {
|
|
280
|
+
console.log(pc.yellow(" Could not detect git diff \u2014 falling back to full scan.\n"));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (!quiet && !diffFiles) {
|
|
285
|
+
console.log(pc.dim(" Scanning files...\n"));
|
|
286
|
+
}
|
|
287
|
+
const analysis = await scanCodebase({
|
|
288
|
+
rootDir,
|
|
289
|
+
useCache: true,
|
|
290
|
+
files: diffFiles,
|
|
291
|
+
onProgress: quiet ? void 0 : (progress) => {
|
|
292
|
+
if (progress.phase === "scanning") {
|
|
293
|
+
process.stdout.write(
|
|
294
|
+
`\r ${pc.dim(`[${progress.current}/${progress.total}]`)} ${pc.dim(relative2(rootDir, progress.currentFile))}`
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
if (!quiet) {
|
|
300
|
+
process.stdout.write("\r" + " ".repeat(80) + "\r");
|
|
301
|
+
console.log(
|
|
302
|
+
pc.dim(` Scanned ${analysis.totalFiles} files, found ${analysis.totalComponents} component types
|
|
303
|
+
`)
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
const allUsages = [];
|
|
307
|
+
for (const comp of Object.values(analysis.components)) {
|
|
308
|
+
allUsages.push(...comp.usages);
|
|
309
|
+
}
|
|
310
|
+
if (allUsages.length === 0) {
|
|
311
|
+
if (!quiet) {
|
|
312
|
+
console.log(pc.yellow(" No component usages found.\n"));
|
|
313
|
+
}
|
|
314
|
+
if (options.report) {
|
|
315
|
+
const report = {
|
|
316
|
+
verdict: aggregateVerdicts([], computeScore, "ci"),
|
|
317
|
+
componentUsage: []
|
|
318
|
+
};
|
|
319
|
+
await writeGovernScanReport(options.report, report);
|
|
320
|
+
if (!quiet) {
|
|
321
|
+
console.log(pc.dim(` Wrote governance report: ${resolve2(options.report)}
|
|
322
|
+
`));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return { exitCode: 0 };
|
|
326
|
+
}
|
|
327
|
+
const grouped = groupByFile(allUsages);
|
|
328
|
+
let totalFiles = 0;
|
|
329
|
+
let passedFiles = 0;
|
|
330
|
+
let totalViolations = 0;
|
|
331
|
+
const violationCounts = /* @__PURE__ */ new Map();
|
|
332
|
+
const allVerdicts = [];
|
|
333
|
+
for (const [filePath, usages] of grouped) {
|
|
334
|
+
const spec = usagesToSpec(usages, filePath, rootDir);
|
|
335
|
+
const relPath = relative2(rootDir, filePath);
|
|
336
|
+
const verdict = await engine.check(spec, {
|
|
337
|
+
runner: "cli",
|
|
338
|
+
input: relPath
|
|
339
|
+
});
|
|
340
|
+
allVerdicts.push(verdict);
|
|
341
|
+
totalFiles++;
|
|
342
|
+
if (verdict.passed) {
|
|
343
|
+
passedFiles++;
|
|
344
|
+
} else {
|
|
345
|
+
if (!quiet) {
|
|
346
|
+
console.log(pc.red(` \u2717 ${relPath}`));
|
|
347
|
+
if (format === "summary") {
|
|
348
|
+
for (const result of verdict.results) {
|
|
349
|
+
for (const v of result.violations) {
|
|
350
|
+
const count = violationCounts.get(v.rule) ?? 0;
|
|
351
|
+
violationCounts.set(v.rule, count + 1);
|
|
352
|
+
totalViolations++;
|
|
353
|
+
console.log(
|
|
354
|
+
pc.dim(` ${v.severity} `) + pc.yellow(v.rule) + pc.dim(` \u2014 ${v.message}`)
|
|
355
|
+
);
|
|
356
|
+
if (v.nodeId) {
|
|
357
|
+
console.log(pc.dim(` at ${v.nodeId}`));
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (verdict.passed && !quiet && format === "summary") {
|
|
365
|
+
console.log(pc.green(` \u2713 ${relPath}`) + pc.dim(` (${usages.length} components, score: ${verdict.score}/100)`));
|
|
366
|
+
}
|
|
367
|
+
if (format === "json" || format === "sarif") {
|
|
368
|
+
const output = formatVerdict(verdict, format);
|
|
369
|
+
console.log(output);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const health = computeComponentHealth(allVerdicts, registryMap ?? {});
|
|
373
|
+
if (!quiet && format === "summary") {
|
|
374
|
+
console.log(pc.dim("\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
375
|
+
console.log(` Files checked: ${totalFiles}`);
|
|
376
|
+
console.log(` Passed: ${passedFiles}/${totalFiles}`);
|
|
377
|
+
console.log(` Violations: ${totalViolations}`);
|
|
378
|
+
if (violationCounts.size > 0) {
|
|
379
|
+
console.log(pc.dim("\n Top violations:"));
|
|
380
|
+
const sorted = [...violationCounts.entries()].sort((a, b) => b[1] - a[1]);
|
|
381
|
+
for (const [rule, count] of sorted.slice(0, 5)) {
|
|
382
|
+
console.log(pc.dim(` ${count}\xD7 `) + pc.yellow(rule));
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
console.log(pc.dim("\n Component Health:"));
|
|
386
|
+
console.log(` Contract coverage: ${health.contractCoverage}% (${health.contractedComponents}/${health.totalComponents})`);
|
|
387
|
+
console.log(` Compliance rate: ${health.overallCompliance}%`);
|
|
388
|
+
if (health.uncontracted.length > 0) {
|
|
389
|
+
console.log(pc.dim(` Uncontracted: ${health.uncontracted.slice(0, 5).join(", ")}${health.uncontracted.length > 5 ? ` (+${health.uncontracted.length - 5} more)` : ""}`));
|
|
390
|
+
}
|
|
391
|
+
console.log();
|
|
392
|
+
if (passedFiles === totalFiles) {
|
|
393
|
+
console.log(pc.green(` \u2713 All files passed governance checks
|
|
394
|
+
`));
|
|
395
|
+
} else {
|
|
396
|
+
console.log(
|
|
397
|
+
pc.red(` \u2717 ${totalFiles - passedFiles} file(s) failed governance checks
|
|
398
|
+
`)
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (options.report) {
|
|
403
|
+
const report = {
|
|
404
|
+
verdict: aggregateVerdicts(allVerdicts, computeScore, "ci"),
|
|
405
|
+
componentUsage: flattenComponentUsage(allUsages, rootDir)
|
|
406
|
+
};
|
|
407
|
+
if (hasRegistry) {
|
|
408
|
+
report.complianceSummary = buildComplianceSummary(health);
|
|
409
|
+
}
|
|
410
|
+
await writeGovernScanReport(options.report, report);
|
|
411
|
+
if (!quiet) {
|
|
412
|
+
console.log(pc.dim(` Wrote governance report: ${resolve2(options.report)}
|
|
413
|
+
`));
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (options.apiKey) {
|
|
417
|
+
await reportToCloud({
|
|
418
|
+
apiKey: options.apiKey,
|
|
419
|
+
cloudUrl: options.cloudUrl,
|
|
420
|
+
verdicts: allVerdicts,
|
|
421
|
+
rootDir,
|
|
422
|
+
quiet,
|
|
423
|
+
diffOnly: !!diffFiles
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
return { exitCode: passedFiles === totalFiles ? 0 : 1 };
|
|
427
|
+
}
|
|
428
|
+
var DEFAULT_CLOUD_URL = "https://app.usefragments.com";
|
|
429
|
+
function detectGitMetadata() {
|
|
430
|
+
const env = process.env;
|
|
431
|
+
const meta = {};
|
|
432
|
+
if (env.GITHUB_SHA) meta.commitSha = env.GITHUB_SHA;
|
|
433
|
+
if (env.GITHUB_REPOSITORY) meta.repoFullName = env.GITHUB_REPOSITORY;
|
|
434
|
+
if (env.GITHUB_REF_NAME) meta.branch = env.GITHUB_REF_NAME;
|
|
435
|
+
if (env.GITHUB_EVENT_NAME === "pull_request" && env.GITHUB_EVENT_PATH) {
|
|
436
|
+
try {
|
|
437
|
+
const event = JSON.parse(readFileSync(env.GITHUB_EVENT_PATH, "utf-8"));
|
|
438
|
+
if (event?.pull_request?.number) {
|
|
439
|
+
meta.pr = event.pull_request.number;
|
|
440
|
+
}
|
|
441
|
+
if (event?.pull_request?.head?.sha) {
|
|
442
|
+
meta.commitSha = event.pull_request.head?.sha;
|
|
443
|
+
}
|
|
444
|
+
} catch {
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return meta;
|
|
448
|
+
}
|
|
449
|
+
async function reportToCloud(options) {
|
|
450
|
+
const { apiKey, verdicts, rootDir, quiet, diffOnly } = options;
|
|
451
|
+
const baseUrl = (options.cloudUrl ?? DEFAULT_CLOUD_URL).replace(/\/+$/, "");
|
|
452
|
+
const findings = [];
|
|
453
|
+
const { createHash } = await import("crypto");
|
|
454
|
+
for (const verdict of verdicts) {
|
|
455
|
+
for (const result of verdict.results) {
|
|
456
|
+
for (const v of result.violations) {
|
|
457
|
+
const severity = v.severity === "critical" || v.severity === "serious" ? "error" : v.severity === "moderate" ? "warning" : "info";
|
|
458
|
+
const fingerprint = createHash("sha256").update(`${v.rule}:${v.nodeType}:${v.nodeId}:${v.message}`).digest("hex").slice(0, 16);
|
|
459
|
+
let filePath;
|
|
460
|
+
let line;
|
|
461
|
+
let column;
|
|
462
|
+
if (v.filePath) {
|
|
463
|
+
filePath = v.filePath;
|
|
464
|
+
line = v.line;
|
|
465
|
+
column = v.column;
|
|
466
|
+
} else if (v.nodeId) {
|
|
467
|
+
const parts = v.nodeId.split(":");
|
|
468
|
+
if (parts.length >= 3) {
|
|
469
|
+
const col = parseInt(parts.pop(), 10);
|
|
470
|
+
const ln = parseInt(parts.pop(), 10);
|
|
471
|
+
const path = parts.join(":");
|
|
472
|
+
if (!isNaN(ln) && !isNaN(col) && path) {
|
|
473
|
+
filePath = path;
|
|
474
|
+
line = ln;
|
|
475
|
+
column = col;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (!filePath) {
|
|
479
|
+
filePath = relative2(rootDir, v.nodeId);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
findings.push({
|
|
483
|
+
ruleId: v.rule,
|
|
484
|
+
severity,
|
|
485
|
+
filePath,
|
|
486
|
+
line,
|
|
487
|
+
column,
|
|
488
|
+
rawValue: v.rawValue,
|
|
489
|
+
message: `[${result.validator}] ${v.message}`,
|
|
490
|
+
category: result.validator,
|
|
491
|
+
fingerprint,
|
|
492
|
+
suggestedToken: v.suggestion
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (!quiet) {
|
|
498
|
+
console.log(pc.dim(` Reporting ${findings.length} finding(s) to Fragments Cloud...`));
|
|
499
|
+
}
|
|
500
|
+
const gitMeta = detectGitMetadata();
|
|
501
|
+
try {
|
|
502
|
+
const response = await fetch(`${baseUrl}/api/govern/ingest`, {
|
|
503
|
+
method: "POST",
|
|
504
|
+
headers: {
|
|
505
|
+
"Content-Type": "application/json",
|
|
506
|
+
"Authorization": `Bearer ${apiKey}`
|
|
507
|
+
},
|
|
508
|
+
body: JSON.stringify({
|
|
509
|
+
findings,
|
|
510
|
+
source: "ci",
|
|
511
|
+
diffOnly: diffOnly ?? false,
|
|
512
|
+
...gitMeta
|
|
513
|
+
})
|
|
514
|
+
});
|
|
515
|
+
if (!response.ok) {
|
|
516
|
+
const body2 = await response.json().catch(() => ({}));
|
|
517
|
+
const msg = body2.error ?? `HTTP ${response.status}`;
|
|
518
|
+
console.error(pc.red(` \u2717 Cloud report failed: ${msg}
|
|
519
|
+
`));
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
const body = await response.json();
|
|
523
|
+
if (!quiet) {
|
|
524
|
+
console.log(
|
|
525
|
+
pc.green(` \u2713 Reported ${body.ingested ?? findings.length} finding(s) to Cloud`) + (body.orgSlug ? pc.dim(` (${body.orgSlug})`) : "") + "\n"
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
} catch (err) {
|
|
529
|
+
console.error(
|
|
530
|
+
pc.red(` \u2717 Cloud report failed: `) + pc.dim(err instanceof Error ? err.message : "Network error") + "\n"
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
async function governWatch(options = {}) {
|
|
535
|
+
const {
|
|
536
|
+
loadPolicy,
|
|
537
|
+
createEngine,
|
|
538
|
+
buildAdaptersFromConfig,
|
|
539
|
+
formatVerdict
|
|
540
|
+
} = await import("@fragments-sdk/govern");
|
|
541
|
+
const { scanFile } = await import("./scanner-4KZNOXAK.js");
|
|
542
|
+
const { usagesToSpec } = await import("./converter-7XM3Y6NJ.js");
|
|
543
|
+
const quiet = options.quiet ?? false;
|
|
544
|
+
const debounceMs = options.debounce ?? 300;
|
|
545
|
+
const format = options.format ?? "summary";
|
|
546
|
+
console.log(pc.cyan(`
|
|
547
|
+
${BRAND.name} Governance Watch
|
|
548
|
+
`));
|
|
549
|
+
const { exitCode } = await governScan(options);
|
|
550
|
+
if (!quiet) {
|
|
551
|
+
console.log(
|
|
552
|
+
pc.dim(` Initial scan ${exitCode === 0 ? "passed" : "completed with violations"}
|
|
553
|
+
`)
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
const rootDir = resolve2(options.dir ?? detectRootDir(process.cwd()));
|
|
557
|
+
let policy = await loadPolicy(options.config);
|
|
558
|
+
if (Object.keys(policy.rules).length === 0) {
|
|
559
|
+
policy = { ...policy, rules: SCAN_DEFAULT_RULES };
|
|
560
|
+
}
|
|
561
|
+
const adapters = buildAdaptersFromConfig(policy.audit);
|
|
562
|
+
const engine = createEngine(policy, adapters);
|
|
563
|
+
console.log(pc.dim(" Watching for changes... (Ctrl+C to stop)\n"));
|
|
564
|
+
const chokidar = await import("chokidar");
|
|
565
|
+
const watcher = chokidar.watch(
|
|
566
|
+
["**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"],
|
|
567
|
+
{
|
|
568
|
+
cwd: rootDir,
|
|
569
|
+
ignoreInitial: true,
|
|
570
|
+
ignored: [
|
|
571
|
+
"**/node_modules/**",
|
|
572
|
+
"**/dist/**",
|
|
573
|
+
"**/build/**",
|
|
574
|
+
"**/.next/**",
|
|
575
|
+
"**/*.test.*",
|
|
576
|
+
"**/*.spec.*",
|
|
577
|
+
"**/*.stories.*"
|
|
578
|
+
],
|
|
579
|
+
awaitWriteFinish: { stabilityThreshold: debounceMs }
|
|
580
|
+
}
|
|
581
|
+
);
|
|
582
|
+
const handleChange = async (changedRelPath) => {
|
|
583
|
+
const absolutePath = resolve2(rootDir, changedRelPath);
|
|
584
|
+
try {
|
|
585
|
+
const { usages } = await scanFile(absolutePath);
|
|
586
|
+
if (usages.length === 0) {
|
|
587
|
+
if (!quiet) {
|
|
588
|
+
console.log(pc.dim(` \u25CB ${changedRelPath} \u2014 no component usages`));
|
|
589
|
+
}
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const spec = usagesToSpec(usages, absolutePath, rootDir);
|
|
593
|
+
const verdict = await engine.check(spec, {
|
|
594
|
+
runner: "cli",
|
|
595
|
+
input: changedRelPath
|
|
596
|
+
});
|
|
597
|
+
if (verdict.passed) {
|
|
598
|
+
console.log(
|
|
599
|
+
pc.green(` \u2713 ${changedRelPath}`) + pc.dim(` (${usages.length} components, score: ${verdict.score}/100)`)
|
|
600
|
+
);
|
|
601
|
+
} else {
|
|
602
|
+
console.log(pc.red(` \u2717 ${changedRelPath}`));
|
|
603
|
+
if (format === "summary") {
|
|
604
|
+
for (const result of verdict.results) {
|
|
605
|
+
for (const v of result.violations) {
|
|
606
|
+
console.log(
|
|
607
|
+
pc.dim(` ${v.severity} `) + pc.yellow(v.rule) + pc.dim(` \u2014 ${v.message}`)
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
} else {
|
|
612
|
+
console.log(formatVerdict(verdict, format));
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
} catch (error) {
|
|
616
|
+
if (!quiet) {
|
|
617
|
+
console.log(
|
|
618
|
+
pc.dim(` \u26A0 ${changedRelPath} \u2014 `) + pc.yellow(error instanceof Error ? error.message : "parse error")
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
watcher.on("change", handleChange);
|
|
624
|
+
watcher.on("add", handleChange);
|
|
625
|
+
await new Promise(() => {
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
export {
|
|
629
|
+
governScan,
|
|
630
|
+
governWatch
|
|
631
|
+
};
|
|
632
|
+
//# sourceMappingURL=govern-scan-X6UEIOSV.js.map
|