@vibgrate/cli 2026.4.30 → 2026.6.5153336
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/baseline-MJNJICDN.js +12 -0
- package/dist/chunk-734OP6JR.js +100 -0
- package/dist/chunk-74ZJFYEM.js +1936 -0
- package/dist/{chunk-JQHUH6A3.js → chunk-HAT4W7NZ.js} +2 -8
- package/dist/chunk-JSBRDJBE.js +30 -0
- package/dist/{chunk-XCIPC2J7.js → chunk-K2D6JXLA.js} +8688 -7297
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1676 -301
- package/dist/{fs-D24ONFXR.js → fs-D24ONFXR-SJOEVGDJ.js} +2 -1
- package/dist/index.d.ts +1 -617
- package/dist/index.js +4 -2
- package/dist/semver-JBJZTHUX.js +5 -0
- package/package.json +8 -9
- package/dist/baseline-QZZXBT74.js +0 -10
- package/dist/chunk-2VJCLUTR.js +0 -31
package/dist/cli.js
CHANGED
|
@@ -1,29 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
baselineCommand
|
|
4
|
-
} from "./chunk-2VJCLUTR.js";
|
|
5
|
-
import {
|
|
6
|
-
VERSION,
|
|
7
|
-
computeHmac,
|
|
8
|
-
dsnCommand,
|
|
9
|
-
formatMarkdown,
|
|
10
|
-
formatText,
|
|
11
|
-
parseDsn,
|
|
12
|
-
pushCommand,
|
|
13
|
-
scanCommand,
|
|
14
|
-
writeDefaultConfig
|
|
15
|
-
} from "./chunk-XCIPC2J7.js";
|
|
16
2
|
import {
|
|
17
3
|
Semaphore,
|
|
4
|
+
baselineCommand,
|
|
18
5
|
ensureDir,
|
|
19
6
|
pathExists,
|
|
20
7
|
readJsonFile,
|
|
21
8
|
writeTextFile
|
|
22
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-734OP6JR.js";
|
|
10
|
+
import {
|
|
11
|
+
computeRepoFingerprint,
|
|
12
|
+
detectVcs,
|
|
13
|
+
fetchScanPreflight,
|
|
14
|
+
parseDsn,
|
|
15
|
+
prepareCompressedUpload,
|
|
16
|
+
resolveRepositoryName,
|
|
17
|
+
runScan,
|
|
18
|
+
writeDefaultConfig
|
|
19
|
+
} from "./chunk-K2D6JXLA.js";
|
|
20
|
+
import {
|
|
21
|
+
require_semver
|
|
22
|
+
} from "./chunk-74ZJFYEM.js";
|
|
23
|
+
import {
|
|
24
|
+
pathExists as pathExists2
|
|
25
|
+
} from "./chunk-HAT4W7NZ.js";
|
|
26
|
+
import {
|
|
27
|
+
__toESM
|
|
28
|
+
} from "./chunk-JSBRDJBE.js";
|
|
23
29
|
|
|
24
30
|
// src/cli.ts
|
|
25
|
-
import { Command as
|
|
26
|
-
import
|
|
31
|
+
import { Command as Command11 } from "commander";
|
|
32
|
+
import chalk12 from "chalk";
|
|
27
33
|
|
|
28
34
|
// src/commands/init.ts
|
|
29
35
|
import * as path from "path";
|
|
@@ -42,7 +48,7 @@ var initCommand = new Command("init").description("Initialize vibgrate in a proj
|
|
|
42
48
|
console.log(chalk.green("\u2714") + ` Created ${chalk.bold("vibgrate.config.ts")}`);
|
|
43
49
|
}
|
|
44
50
|
if (opts.baseline) {
|
|
45
|
-
const { runBaseline } = await import("./baseline-
|
|
51
|
+
const { runBaseline } = await import("./baseline-MJNJICDN.js");
|
|
46
52
|
await runBaseline(rootDir);
|
|
47
53
|
}
|
|
48
54
|
console.log("");
|
|
@@ -52,15 +58,1034 @@ var initCommand = new Command("init").description("Initialize vibgrate in a proj
|
|
|
52
58
|
console.log("");
|
|
53
59
|
});
|
|
54
60
|
|
|
55
|
-
// src/commands/
|
|
61
|
+
// src/commands/scan.ts
|
|
62
|
+
import * as path3 from "path";
|
|
63
|
+
import { Command as Command3 } from "commander";
|
|
64
|
+
import chalk3 from "chalk";
|
|
65
|
+
|
|
66
|
+
// src/version.ts
|
|
67
|
+
import { createRequire } from "module";
|
|
68
|
+
var require2 = createRequire(import.meta.url);
|
|
69
|
+
var pkg = require2("../package.json");
|
|
70
|
+
var VERSION = pkg.version;
|
|
71
|
+
|
|
72
|
+
// src/commands/dsn.ts
|
|
73
|
+
import * as crypto from "crypto";
|
|
56
74
|
import * as path2 from "path";
|
|
57
75
|
import { Command as Command2 } from "commander";
|
|
58
76
|
import chalk2 from "chalk";
|
|
59
|
-
var
|
|
60
|
-
|
|
77
|
+
var REGION_HOSTS = {
|
|
78
|
+
us: "us.ingest.vibgrate.com",
|
|
79
|
+
eu: "eu.ingest.vibgrate.com"
|
|
80
|
+
};
|
|
81
|
+
function resolveIngestHost(region, ingest) {
|
|
82
|
+
if (ingest) {
|
|
83
|
+
try {
|
|
84
|
+
return new URL(ingest).host;
|
|
85
|
+
} catch {
|
|
86
|
+
throw new Error(`Invalid ingest URL: ${ingest}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const r = (region ?? "us").toLowerCase();
|
|
90
|
+
const host = REGION_HOSTS[r];
|
|
91
|
+
if (!host) {
|
|
92
|
+
throw new Error(`Unknown region "${r}". Supported: ${Object.keys(REGION_HOSTS).join(", ")}`);
|
|
93
|
+
}
|
|
94
|
+
return host;
|
|
95
|
+
}
|
|
96
|
+
async function provisionDsn(keyId, secret, workspaceId, ingestHost) {
|
|
97
|
+
const url = `https://${ingestHost}/v1/provision`;
|
|
98
|
+
try {
|
|
99
|
+
const response = await fetch(url, {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers: {
|
|
102
|
+
"Content-Type": "application/json",
|
|
103
|
+
"Connection": "close"
|
|
104
|
+
// Prevent keep-alive delays on exit
|
|
105
|
+
},
|
|
106
|
+
body: JSON.stringify({ keyId, secret, workspaceId })
|
|
107
|
+
});
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
const result = await response.json();
|
|
110
|
+
return { success: false, error: result.error || `HTTP ${response.status}` };
|
|
111
|
+
}
|
|
112
|
+
return { success: true };
|
|
113
|
+
} catch (e) {
|
|
114
|
+
return { success: false, error: e instanceof Error ? e.message : String(e) };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
var dsnCommand = new Command2("dsn").description("Manage DSN tokens");
|
|
118
|
+
dsnCommand.command("create").description("Create a new DSN token").option("--ingest <url>", "Ingest API URL (overrides --region)").option("--region <region>", "Data residency region (us, eu)", "us").requiredOption("--workspace <id>", 'Workspace ID (use "new" to auto-generate)').option("--write <path>", "Write DSN to file").action(async (opts) => {
|
|
119
|
+
const keyId = crypto.randomBytes(12).toString("hex");
|
|
120
|
+
const secret = crypto.randomBytes(32).toString("hex");
|
|
121
|
+
let ingestHost;
|
|
122
|
+
try {
|
|
123
|
+
ingestHost = resolveIngestHost(opts.region, opts.ingest);
|
|
124
|
+
} catch (e) {
|
|
125
|
+
console.error(chalk2.red(e instanceof Error ? e.message : String(e)));
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
let workspaceId = opts.workspace;
|
|
129
|
+
const isNewWorkspace = opts.workspace.toLowerCase() === "new";
|
|
130
|
+
if (isNewWorkspace) {
|
|
131
|
+
workspaceId = crypto.randomBytes(8).toString("hex");
|
|
132
|
+
console.log(chalk2.dim(`Provisioning new workspace ${workspaceId}...`));
|
|
133
|
+
}
|
|
134
|
+
if (isNewWorkspace) {
|
|
135
|
+
const result = await provisionDsn(keyId, secret, workspaceId, ingestHost);
|
|
136
|
+
if (!result.success) {
|
|
137
|
+
console.error(chalk2.red(`Failed to provision DSN: ${result.error}`));
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const dsn = `vibgrate+https://${keyId}:${secret}@${ingestHost}/${workspaceId}`;
|
|
142
|
+
console.log(chalk2.green("\u2714") + " DSN created");
|
|
143
|
+
console.log("");
|
|
144
|
+
console.log(chalk2.bold("DSN:"));
|
|
145
|
+
console.log(` ${dsn}`);
|
|
146
|
+
console.log("");
|
|
147
|
+
console.log(chalk2.bold("Key ID:"));
|
|
148
|
+
console.log(` ${keyId}`);
|
|
149
|
+
if (isNewWorkspace) {
|
|
150
|
+
console.log("");
|
|
151
|
+
console.log(chalk2.bold("Workspace ID:"));
|
|
152
|
+
console.log(` ${workspaceId}`);
|
|
153
|
+
}
|
|
154
|
+
console.log("");
|
|
155
|
+
console.log(chalk2.dim("Set this as VIBGRATE_DSN in your CI environment."));
|
|
156
|
+
if (!isNewWorkspace) {
|
|
157
|
+
console.log(chalk2.dim("The secret must be registered on your Vibgrate ingest API."));
|
|
158
|
+
}
|
|
159
|
+
if (opts.write) {
|
|
160
|
+
const writePath = path2.resolve(opts.write);
|
|
161
|
+
await writeTextFile(writePath, dsn + "\n");
|
|
162
|
+
console.log("");
|
|
163
|
+
console.log(chalk2.green("\u2714") + ` DSN written to ${opts.write}`);
|
|
164
|
+
console.log(chalk2.yellow("\u26A0") + " Add this file to .gitignore!");
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// src/utils/ingest-id-output.ts
|
|
169
|
+
function emitIngestIdLine(ingestId, options) {
|
|
170
|
+
console.log(`VIBGRATE_INGEST_ID=${ingestId}`);
|
|
171
|
+
if (options?.unchanged) {
|
|
172
|
+
console.log("VIBGRATE_INGEST_UNCHANGED=1");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/commands/scan.ts
|
|
177
|
+
async function autoPush(artifact, rootDir, opts) {
|
|
178
|
+
const dsn = opts.dsn || process.env.VIBGRATE_DSN;
|
|
179
|
+
if (!dsn) {
|
|
180
|
+
console.error(chalk3.red("No DSN provided for push."));
|
|
181
|
+
console.error(chalk3.dim("Set VIBGRATE_DSN environment variable or use --dsn flag."));
|
|
182
|
+
if (opts.strict) process.exit(1);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const parsed = parseDsn(dsn);
|
|
186
|
+
if (!parsed) {
|
|
187
|
+
console.error(chalk3.red("Invalid DSN format."));
|
|
188
|
+
if (opts.strict) process.exit(1);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const { body, contentEncoding } = await prepareCompressedUpload(artifact);
|
|
192
|
+
const timestamp = String(Date.now());
|
|
193
|
+
let host = parsed.host;
|
|
194
|
+
if (opts.region) {
|
|
195
|
+
try {
|
|
196
|
+
host = resolveIngestHost(opts.region);
|
|
197
|
+
} catch (e) {
|
|
198
|
+
console.error(chalk3.red(e instanceof Error ? e.message : String(e)));
|
|
199
|
+
if (opts.strict) process.exit(1);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const url = `${parsed.scheme}://${host}/v1/ingest/scan`;
|
|
204
|
+
const originalSize = JSON.stringify(artifact).length;
|
|
205
|
+
const compressedSize = body.length;
|
|
206
|
+
const ratio = ((1 - compressedSize / originalSize) * 100).toFixed(0);
|
|
207
|
+
console.log(chalk3.dim(`Uploading to ${host}... (${(compressedSize / 1024).toFixed(0)} KB, ${ratio}% smaller)`));
|
|
208
|
+
try {
|
|
209
|
+
const response = await fetch(url, {
|
|
210
|
+
method: "POST",
|
|
211
|
+
headers: {
|
|
212
|
+
"Content-Type": "application/json",
|
|
213
|
+
"Content-Encoding": contentEncoding,
|
|
214
|
+
"X-Vibgrate-Timestamp": timestamp,
|
|
215
|
+
"Authorization": `VibgrateDSN ${parsed.keyId}:${parsed.secret}`,
|
|
216
|
+
"Connection": "close"
|
|
217
|
+
// Prevent keep-alive delays on exit
|
|
218
|
+
},
|
|
219
|
+
body
|
|
220
|
+
});
|
|
221
|
+
if (!response.ok) {
|
|
222
|
+
const text = await response.text();
|
|
223
|
+
throw new Error(`HTTP ${response.status}: ${text}`);
|
|
224
|
+
}
|
|
225
|
+
const result = await response.json();
|
|
226
|
+
if (result.unchanged) {
|
|
227
|
+
console.log(
|
|
228
|
+
chalk3.green("\u2714") + ` Repository unchanged since ${result.lastScannedAt ?? "last scan"} \u2014 skipped upload (no credit used).`
|
|
229
|
+
);
|
|
230
|
+
if (result.previousIngestId) {
|
|
231
|
+
emitIngestIdLine(result.previousIngestId, { unchanged: true });
|
|
232
|
+
const dashUrl = `https://dash.vibgrate.com/${parsed.workspaceId}/scan/${result.previousIngestId}`;
|
|
233
|
+
console.log(chalk3.dim(` Previous report: ${dashUrl}`));
|
|
234
|
+
} else if (opts.strict) {
|
|
235
|
+
console.error(chalk3.red("Repository unchanged but no previous ingest id returned."));
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
console.log(chalk3.green("\u2714") + ` Scan queued for processing (${result.ingestId ?? "ok"})`);
|
|
241
|
+
if (result.ingestId) {
|
|
242
|
+
emitIngestIdLine(result.ingestId);
|
|
243
|
+
const dashUrl = `https://dash.vibgrate.com/${parsed.workspaceId}/scan/${result.ingestId}`;
|
|
244
|
+
const CLEAR_LINE = process.platform === "win32" ? "\x1B[0G\x1B[2K" : "";
|
|
245
|
+
console.log("");
|
|
246
|
+
console.log(CLEAR_LINE + chalk3.dim(" Processing continues in the background. Results available shortly."));
|
|
247
|
+
console.log("");
|
|
248
|
+
console.log(CLEAR_LINE + chalk3.cyan("\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\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\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
249
|
+
console.log(CLEAR_LINE + chalk3.bold(" \u{1F4CA} View Scan Report"));
|
|
250
|
+
console.log(CLEAR_LINE + " " + chalk3.underline.cyan(dashUrl));
|
|
251
|
+
console.log(CLEAR_LINE + chalk3.cyan("\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\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\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
252
|
+
console.log("");
|
|
253
|
+
console.log("");
|
|
254
|
+
}
|
|
255
|
+
} catch (e) {
|
|
256
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
257
|
+
console.error(chalk3.red(`Upload failed: ${msg}`));
|
|
258
|
+
if (opts.strict) process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function parseNonNegativeNumber(value, label) {
|
|
262
|
+
if (value === void 0) return void 0;
|
|
263
|
+
const parsed = Number(value);
|
|
264
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
265
|
+
throw new Error(`${label} must be a non-negative number.`);
|
|
266
|
+
}
|
|
267
|
+
return parsed;
|
|
268
|
+
}
|
|
269
|
+
var scanCommand = new Command3("scan").description("Scan a project for upgrade drift").argument("[path]", "Path to scan", ".").option("--out <file>", "Output file path").option("--format <format>", "Output format (text|json|sarif|md)", "text").option("--fail-on <level>", "Fail on warn or error").option("--baseline <file>", "Compare against baseline").option("--changed-only", "Only scan changed files").option("--concurrency <n>", "Max concurrent npm calls", "8").option("--push", "Auto-push results to Vibgrate API after scan").option("--dsn <dsn>", "DSN token for push (or use VIBGRATE_DSN env)").option("--region <region>", "Override data residency region for push (us, eu)").option("--strict", "Fail on push errors").option("--ui-purpose", "Enable optional UI purpose evidence extraction (slower)").option("--no-local-artifacts", "Do not write .vibgrate JSON artifacts to disk").option("--max-privacy", "Enable strongest privacy mode (minimal scanners, no local artifacts)").option("--offline", "Run without network calls; do not upload results").option("--package-manifest <file>", "Use local package-version manifest JSON/ZIP (for offline mode)").option("--project-scan-timeout <seconds>", "Per-project scan timeout in seconds (default: 180)").option("--drift-budget <score>", "Fail if drift score is above budget (0-100)").option("--drift-worsening <percent>", "Fail if drift worsens by more than % since baseline").action(async (targetPath, opts) => {
|
|
270
|
+
const rootDir = path3.resolve(targetPath);
|
|
271
|
+
if (!await pathExists2(rootDir)) {
|
|
272
|
+
console.error(chalk3.red(`Path does not exist: ${rootDir}`));
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
const hasDsn = !!(opts.dsn || process.env.VIBGRATE_DSN);
|
|
276
|
+
const willPush = !opts.offline && (opts.push || hasDsn);
|
|
277
|
+
if (willPush && hasDsn) {
|
|
278
|
+
const dsn = opts.dsn || process.env.VIBGRATE_DSN;
|
|
279
|
+
const parsed = parseDsn(dsn);
|
|
280
|
+
if (parsed) {
|
|
281
|
+
const ingestHost = opts.region ? resolveIngestHost(opts.region) : parsed.host;
|
|
282
|
+
const vcs = await detectVcs(rootDir);
|
|
283
|
+
const fingerprint = await computeRepoFingerprint(rootDir, vcs);
|
|
284
|
+
const repositoryName = await resolveRepositoryName(rootDir);
|
|
285
|
+
try {
|
|
286
|
+
const preflight = await fetchScanPreflight(parsed, ingestHost, {
|
|
287
|
+
repositoryName,
|
|
288
|
+
vcsSha: fingerprint.vcsSha
|
|
289
|
+
});
|
|
290
|
+
if (preflight.status === "error" || !preflight.scans.allowed) {
|
|
291
|
+
console.error(chalk3.red(preflight.error ?? "Scan ingestion not allowed for this workspace."));
|
|
292
|
+
console.error(
|
|
293
|
+
chalk3.dim(
|
|
294
|
+
`Credits: ${preflight.scans.used}/${preflight.scans.limit} (${preflight.plan.label} plan)`
|
|
295
|
+
)
|
|
296
|
+
);
|
|
297
|
+
if (opts.strict) process.exit(1);
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
console.log(
|
|
301
|
+
chalk3.dim(
|
|
302
|
+
`Plan: ${preflight.plan.label} \u2014 scan credits ${preflight.scans.used}/${preflight.scans.limit} this month`
|
|
303
|
+
)
|
|
304
|
+
);
|
|
305
|
+
if (preflight.repository?.unchanged) {
|
|
306
|
+
console.log(
|
|
307
|
+
chalk3.green("\u2714") + ` Repository unchanged at ${preflight.repository.lastVcsSha?.slice(0, 7) ?? "same revision"} \u2014 skipping scan.`
|
|
308
|
+
);
|
|
309
|
+
if (preflight.repository.lastIngestId) {
|
|
310
|
+
emitIngestIdLine(preflight.repository.lastIngestId, { unchanged: true });
|
|
311
|
+
const dashUrl = `https://dash.vibgrate.com/${parsed.workspaceId}/scan/${preflight.repository.lastIngestId}`;
|
|
312
|
+
console.log(chalk3.dim(` Latest report: ${dashUrl}`));
|
|
313
|
+
} else if (opts.strict) {
|
|
314
|
+
console.error(chalk3.red("Repository unchanged but no previous ingest id available."));
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
} catch (e) {
|
|
320
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
321
|
+
console.error(chalk3.yellow(`Preflight check failed: ${msg}`));
|
|
322
|
+
if (opts.strict) process.exit(1);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const scanOpts = {
|
|
327
|
+
vibgrateVersion: VERSION,
|
|
328
|
+
out: opts.out,
|
|
329
|
+
format: opts.format || "text",
|
|
330
|
+
failOn: opts.failOn,
|
|
331
|
+
baseline: opts.baseline,
|
|
332
|
+
changedOnly: opts.changedOnly,
|
|
333
|
+
concurrency: parseInt(opts.concurrency, 10) || 8,
|
|
334
|
+
push: opts.push,
|
|
335
|
+
dsn: opts.dsn,
|
|
336
|
+
region: opts.region,
|
|
337
|
+
strict: opts.strict,
|
|
338
|
+
uiPurpose: opts.uiPurpose,
|
|
339
|
+
noLocalArtifacts: opts.noLocalArtifacts,
|
|
340
|
+
maxPrivacy: opts.maxPrivacy,
|
|
341
|
+
offline: opts.offline,
|
|
342
|
+
packageManifest: opts.packageManifest,
|
|
343
|
+
driftBudget: parseNonNegativeNumber(opts.driftBudget, "--drift-budget"),
|
|
344
|
+
driftWorseningPercent: parseNonNegativeNumber(opts.driftWorsening, "--drift-worsening"),
|
|
345
|
+
projectScanTimeout: opts.projectScanTimeout ? parseInt(opts.projectScanTimeout, 10) || void 0 : void 0
|
|
346
|
+
};
|
|
347
|
+
const artifact = await runScan(rootDir, scanOpts);
|
|
348
|
+
if (opts.failOn) {
|
|
349
|
+
const hasErrors = artifact.findings.some((f) => f.level === "error");
|
|
350
|
+
const hasWarnings = artifact.findings.some((f) => f.level === "warning");
|
|
351
|
+
if (opts.failOn === "error" && hasErrors) {
|
|
352
|
+
console.error(chalk3.red(`
|
|
353
|
+
Failing: ${artifact.findings.filter((f) => f.level === "error").length} error finding(s) detected.`));
|
|
354
|
+
process.exit(2);
|
|
355
|
+
}
|
|
356
|
+
if (opts.failOn === "warn" && (hasErrors || hasWarnings)) {
|
|
357
|
+
console.error(chalk3.red(`
|
|
358
|
+
Failing: findings detected at warn level or above.`));
|
|
359
|
+
process.exit(2);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (scanOpts.driftBudget !== void 0 && artifact.drift.score > scanOpts.driftBudget) {
|
|
363
|
+
console.error(chalk3.red(`
|
|
364
|
+
Failing fitness function: drift score ${artifact.drift.score}/100 exceeds budget ${scanOpts.driftBudget}.`));
|
|
365
|
+
process.exit(2);
|
|
366
|
+
}
|
|
367
|
+
if (scanOpts.driftWorseningPercent !== void 0) {
|
|
368
|
+
if (artifact.delta === void 0) {
|
|
369
|
+
console.error(chalk3.red("\nFailing fitness function: --drift-worsening requires --baseline to compare against previous drift."));
|
|
370
|
+
process.exit(2);
|
|
371
|
+
}
|
|
372
|
+
if (artifact.delta > 0) {
|
|
373
|
+
const baselineScore = artifact.drift.score - artifact.delta;
|
|
374
|
+
const denominator = Math.max(Math.abs(baselineScore), 1e-4);
|
|
375
|
+
const worseningPercent = artifact.delta / denominator * 100;
|
|
376
|
+
if (worseningPercent > scanOpts.driftWorseningPercent) {
|
|
377
|
+
console.error(chalk3.red(`
|
|
378
|
+
Failing fitness function: drift worsened by ${worseningPercent.toFixed(2)}% (threshold ${scanOpts.driftWorseningPercent}%).`));
|
|
379
|
+
process.exit(2);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (willPush) {
|
|
384
|
+
await autoPush(artifact, rootDir, scanOpts);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// src/commands/report.ts
|
|
389
|
+
import * as path4 from "path";
|
|
390
|
+
import { Command as Command4 } from "commander";
|
|
391
|
+
import chalk5 from "chalk";
|
|
392
|
+
|
|
393
|
+
// src/formatters/text.ts
|
|
394
|
+
import chalk4 from "chalk";
|
|
395
|
+
function formatText(artifact) {
|
|
396
|
+
const lines = [];
|
|
397
|
+
lines.push("");
|
|
398
|
+
lines.push(chalk4.cyan(" \u256D\u2500\u2500\u2500\u256E") + chalk4.greenBright("\u279C"));
|
|
399
|
+
lines.push(chalk4.cyan(" \u256D\u2524") + chalk4.greenBright("\u25C9 \u25C9") + chalk4.cyan("\u251C\u256E") + " " + chalk4.bold.white("V I B G R A T E"));
|
|
400
|
+
lines.push(chalk4.cyan(" \u2570\u2524") + chalk4.dim("\u2500\u2500\u2500") + chalk4.cyan("\u251C\u256F") + " " + chalk4.dim(`Drift Intelligence Engine v${VERSION}`));
|
|
401
|
+
lines.push(chalk4.cyan(" \u2570\u2500\u2500\u2500\u256F"));
|
|
402
|
+
lines.push("");
|
|
403
|
+
lines.push(chalk4.bold.cyan("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
|
|
404
|
+
lines.push(chalk4.bold.cyan("\u2551 Vibgrate Drift Report \u2551"));
|
|
405
|
+
lines.push(chalk4.bold.cyan("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
|
|
406
|
+
lines.push("");
|
|
407
|
+
for (const project of artifact.projects) {
|
|
408
|
+
lines.push(chalk4.bold(` \u2500\u2500 ${project.name} `) + chalk4.dim(`(${project.type}) ${project.path}`));
|
|
409
|
+
if (project.runtime) {
|
|
410
|
+
const behindStr = project.runtimeMajorsBehind !== void 0 && project.runtimeMajorsBehind > 0 ? chalk4.yellow(` (${project.runtimeMajorsBehind} major${project.runtimeMajorsBehind > 1 ? "s" : ""} behind)`) : chalk4.green(" (current)");
|
|
411
|
+
lines.push(` Runtime: ${project.runtime}${behindStr}`);
|
|
412
|
+
}
|
|
413
|
+
if (project.targetFramework) {
|
|
414
|
+
lines.push(` Target: ${project.targetFramework}`);
|
|
415
|
+
}
|
|
416
|
+
if (project.frameworks.length > 0) {
|
|
417
|
+
lines.push(" Frameworks:");
|
|
418
|
+
for (const fw of project.frameworks) {
|
|
419
|
+
const lag = fw.majorsBehind !== null ? fw.majorsBehind === 0 ? chalk4.green("current") : chalk4.yellow(`${fw.majorsBehind} behind`) : chalk4.dim("unknown");
|
|
420
|
+
lines.push(` ${fw.name}: ${fw.currentVersion ?? "?"} \u2192 ${fw.latestVersion ?? "?"} (${lag})`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const b = project.dependencyAgeBuckets;
|
|
424
|
+
const total = b.current + b.oneBehind + b.twoPlusBehind + b.unknown;
|
|
425
|
+
if (total > 0) {
|
|
426
|
+
lines.push(" Dependencies:");
|
|
427
|
+
lines.push(` ${chalk4.green(`${b.current} current`)} ${chalk4.yellow(`${b.oneBehind} 1-behind`)} ${chalk4.red(`${b.twoPlusBehind} 2+ behind`)} ${chalk4.dim(`${b.unknown} unknown`)}`);
|
|
428
|
+
}
|
|
429
|
+
lines.push("");
|
|
430
|
+
}
|
|
431
|
+
if (artifact.delta !== void 0) {
|
|
432
|
+
const deltaStr = artifact.delta > 0 ? chalk4.green(`+${artifact.delta}`) : artifact.delta < 0 ? chalk4.red(`${artifact.delta}`) : chalk4.dim("0");
|
|
433
|
+
lines.push(chalk4.bold(" Drift Delta: ") + deltaStr + " (vs baseline)");
|
|
434
|
+
lines.push("");
|
|
435
|
+
}
|
|
436
|
+
if (artifact.extended) {
|
|
437
|
+
lines.push(...formatExtended(artifact.extended));
|
|
438
|
+
}
|
|
439
|
+
if (artifact.findings.length > 0) {
|
|
440
|
+
const errors = artifact.findings.filter((f) => f.level === "error");
|
|
441
|
+
const warnings = artifact.findings.filter((f) => f.level === "warning");
|
|
442
|
+
const notes = artifact.findings.filter((f) => f.level === "note");
|
|
443
|
+
const summary = [
|
|
444
|
+
errors.length > 0 ? chalk4.red(`${errors.length} error${errors.length !== 1 ? "s" : ""}`) : "",
|
|
445
|
+
warnings.length > 0 ? chalk4.yellow(`${warnings.length} warning${warnings.length !== 1 ? "s" : ""}`) : "",
|
|
446
|
+
notes.length > 0 ? chalk4.blue(`${notes.length} note${notes.length !== 1 ? "s" : ""}`) : ""
|
|
447
|
+
].filter(Boolean).join(chalk4.dim(", "));
|
|
448
|
+
lines.push(chalk4.bold.underline(` Findings`) + chalk4.dim(` (${summary})`));
|
|
449
|
+
for (const f of artifact.findings) {
|
|
450
|
+
const icon = f.level === "error" ? chalk4.red("\u2716") : f.level === "warning" ? chalk4.yellow("\u26A0") : chalk4.blue("\u2139");
|
|
451
|
+
lines.push(` ${icon} ${f.message}`);
|
|
452
|
+
lines.push(chalk4.dim(` ${f.ruleId} in ${f.location}`));
|
|
453
|
+
}
|
|
454
|
+
lines.push("");
|
|
455
|
+
}
|
|
456
|
+
const actions = generatePriorityActions(artifact);
|
|
457
|
+
if (actions.length > 0) {
|
|
458
|
+
lines.push(chalk4.bold.cyan("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
|
|
459
|
+
lines.push(chalk4.bold.cyan("\u2551 Top Priority Actions \u2551"));
|
|
460
|
+
lines.push(chalk4.bold.cyan("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
|
|
461
|
+
lines.push("");
|
|
462
|
+
for (let i = 0; i < actions.length; i++) {
|
|
463
|
+
const a = actions[i];
|
|
464
|
+
const num = chalk4.bold.cyan(` ${i + 1}.`);
|
|
465
|
+
lines.push(`${num} ${chalk4.bold(a.title)}`);
|
|
466
|
+
lines.push(chalk4.dim(` ${a.explanation}`));
|
|
467
|
+
if (a.impact) lines.push(` Impact: ${chalk4.green(a.impact)}`);
|
|
468
|
+
lines.push("");
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (artifact.extended?.architecture) {
|
|
472
|
+
lines.push(...formatArchitectureDiagram(artifact.extended.architecture));
|
|
473
|
+
}
|
|
474
|
+
if (artifact.solutions && artifact.solutions.length > 0) {
|
|
475
|
+
lines.push(chalk4.bold.cyan("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
|
|
476
|
+
lines.push(chalk4.bold.cyan("\u2551 Solution Drift Summary \u2551"));
|
|
477
|
+
lines.push(chalk4.bold.cyan("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
|
|
478
|
+
lines.push("");
|
|
479
|
+
for (const solution of artifact.solutions) {
|
|
480
|
+
const solScore = solution.drift?.score;
|
|
481
|
+
const color = typeof solScore === "number" ? solScore >= 70 ? chalk4.green : solScore >= 40 ? chalk4.yellow : chalk4.red : chalk4.dim;
|
|
482
|
+
lines.push(` \u2022 ${solution.name} (${solution.projectPaths.length} projects) \u2014 ${typeof solScore === "number" ? color(`${solScore}/100`) : chalk4.dim("n/a")}`);
|
|
483
|
+
}
|
|
484
|
+
lines.push("");
|
|
485
|
+
}
|
|
486
|
+
const scoreColor = artifact.drift.score >= 70 ? chalk4.green : artifact.drift.score >= 40 ? chalk4.yellow : chalk4.red;
|
|
487
|
+
lines.push(chalk4.bold.cyan("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
|
|
488
|
+
lines.push(chalk4.bold.cyan("\u2551 Drift Score Summary \u2551"));
|
|
489
|
+
lines.push(chalk4.bold.cyan("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
|
|
490
|
+
lines.push("");
|
|
491
|
+
lines.push(chalk4.bold(" Drift Score: ") + scoreColor.bold(`${artifact.drift.score}/100`));
|
|
492
|
+
lines.push(chalk4.bold(" Risk Level: ") + riskBadge(artifact.drift.riskLevel));
|
|
493
|
+
lines.push(chalk4.bold(" Projects: ") + `${artifact.projects.length}`);
|
|
494
|
+
if (artifact.vcs) {
|
|
495
|
+
const vcsParts = [artifact.vcs.type];
|
|
496
|
+
if (artifact.vcs.branch) vcsParts.push(artifact.vcs.branch);
|
|
497
|
+
if (artifact.vcs.shortSha) vcsParts.push(chalk4.dim(artifact.vcs.shortSha));
|
|
498
|
+
lines.push(chalk4.bold(" VCS: ") + vcsParts.join(" "));
|
|
499
|
+
}
|
|
500
|
+
lines.push("");
|
|
501
|
+
const m = new Set(artifact.drift.measured ?? ["runtime", "framework", "dependency", "eol"]);
|
|
502
|
+
lines.push(" " + chalk4.bold.underline("Score Breakdown"));
|
|
503
|
+
lines.push(` Runtime: ${m.has("runtime") ? scoreBar(artifact.drift.components.runtimeScore) : chalk4.dim("n/a")}`);
|
|
504
|
+
lines.push(` Frameworks: ${m.has("framework") ? scoreBar(artifact.drift.components.frameworkScore) : chalk4.dim("n/a")}`);
|
|
505
|
+
lines.push(` Dependencies: ${m.has("dependency") ? scoreBar(artifact.drift.components.dependencyScore) : chalk4.dim("n/a")}`);
|
|
506
|
+
lines.push(` EOL Risk: ${m.has("eol") ? scoreBar(artifact.drift.components.eolScore) : chalk4.dim("n/a")}`);
|
|
507
|
+
lines.push("");
|
|
508
|
+
const scannedParts = [`Scanned at ${artifact.timestamp}`];
|
|
509
|
+
if (artifact.durationMs !== void 0) {
|
|
510
|
+
const secs = (artifact.durationMs / 1e3).toFixed(1);
|
|
511
|
+
scannedParts.push(`${secs}s`);
|
|
512
|
+
}
|
|
513
|
+
if (artifact.filesScanned !== void 0) {
|
|
514
|
+
scannedParts.push(`${artifact.filesScanned} file${artifact.filesScanned !== 1 ? "s" : ""} scanned`);
|
|
515
|
+
}
|
|
516
|
+
if (artifact.treeSummary) {
|
|
517
|
+
scannedParts.push(`${artifact.treeSummary.totalFiles.toLocaleString()} workspace files`);
|
|
518
|
+
scannedParts.push(`${artifact.treeSummary.totalDirs.toLocaleString()} dirs`);
|
|
519
|
+
}
|
|
520
|
+
lines.push(chalk4.dim(` ${scannedParts.join(" \xB7 ")}`));
|
|
521
|
+
lines.push("");
|
|
522
|
+
return lines.join("\n");
|
|
523
|
+
}
|
|
524
|
+
function riskBadge(level) {
|
|
525
|
+
switch (level) {
|
|
526
|
+
case "low":
|
|
527
|
+
return chalk4.bgGreen.black(" LOW ");
|
|
528
|
+
case "moderate":
|
|
529
|
+
return chalk4.bgYellow.black(" MODERATE ");
|
|
530
|
+
case "high":
|
|
531
|
+
return chalk4.bgRed.white(" HIGH ");
|
|
532
|
+
default:
|
|
533
|
+
return level;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
function scoreBar(score) {
|
|
537
|
+
const width = 20;
|
|
538
|
+
const filled = Math.round(score / 100 * width);
|
|
539
|
+
const empty = width - filled;
|
|
540
|
+
const color = score >= 70 ? chalk4.green : score >= 40 ? chalk4.yellow : chalk4.red;
|
|
541
|
+
return color("\u2588".repeat(filled)) + chalk4.dim("\u2591".repeat(empty)) + ` ${Math.round(score)}`;
|
|
542
|
+
}
|
|
543
|
+
var CATEGORY_LABELS = {
|
|
544
|
+
frontend: "Frontend",
|
|
545
|
+
metaFrameworks: "Meta-frameworks",
|
|
546
|
+
bundlers: "Bundlers",
|
|
547
|
+
css: "CSS / UI",
|
|
548
|
+
backend: "Backend",
|
|
549
|
+
orm: "ORM / Database",
|
|
550
|
+
testing: "Testing",
|
|
551
|
+
lintFormat: "Lint & Format",
|
|
552
|
+
apiMessaging: "API & Messaging",
|
|
553
|
+
observability: "Observability",
|
|
554
|
+
payment: "Payment",
|
|
555
|
+
auth: "Auth",
|
|
556
|
+
email: "Email",
|
|
557
|
+
cloud: "Cloud",
|
|
558
|
+
databases: "Databases",
|
|
559
|
+
messaging: "Messaging",
|
|
560
|
+
crm: "CRM & Comms",
|
|
561
|
+
storage: "Storage",
|
|
562
|
+
search: "Search & AI"
|
|
563
|
+
};
|
|
564
|
+
function formatExtended(ext) {
|
|
565
|
+
const lines = [];
|
|
566
|
+
if (ext.toolingInventory) {
|
|
567
|
+
const inv = ext.toolingInventory;
|
|
568
|
+
const categories = Object.entries(inv).filter(([, items]) => items.length > 0);
|
|
569
|
+
if (categories.length > 0) {
|
|
570
|
+
lines.push(chalk4.bold.underline(" Tech Stack"));
|
|
571
|
+
for (const [cat, items] of categories) {
|
|
572
|
+
const label = CATEGORY_LABELS[cat] ?? cat;
|
|
573
|
+
const names = items.map((i) => chalk4.white(i.name)).join(chalk4.dim(", "));
|
|
574
|
+
lines.push(` ${chalk4.cyan(label)}: ${names}`);
|
|
575
|
+
}
|
|
576
|
+
lines.push("");
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (ext.serviceDependencies) {
|
|
580
|
+
const svc = ext.serviceDependencies;
|
|
581
|
+
const categories = Object.entries(svc).filter(([, items]) => items.length > 0);
|
|
582
|
+
if (categories.length > 0) {
|
|
583
|
+
lines.push(chalk4.bold.underline(" Services & Integrations"));
|
|
584
|
+
for (const [cat, items] of categories) {
|
|
585
|
+
const label = CATEGORY_LABELS[cat] ?? cat;
|
|
586
|
+
const names = items.map((i) => {
|
|
587
|
+
const ver = i.version ? chalk4.dim(` ${i.version}`) : "";
|
|
588
|
+
return chalk4.white(i.name) + ver;
|
|
589
|
+
}).join(chalk4.dim(", "));
|
|
590
|
+
lines.push(` ${chalk4.cyan(label)}: ${names}`);
|
|
591
|
+
}
|
|
592
|
+
lines.push("");
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
if (ext.breakingChangeExposure) {
|
|
596
|
+
const bc = ext.breakingChangeExposure;
|
|
597
|
+
if (bc.deprecatedPackages.length > 0 || bc.legacyPolyfills.length > 0) {
|
|
598
|
+
lines.push(chalk4.bold.underline(" Breaking Change Exposure"));
|
|
599
|
+
const exposureColor = bc.exposureScore >= 40 ? chalk4.red : bc.exposureScore >= 20 ? chalk4.yellow : chalk4.green;
|
|
600
|
+
lines.push(` Exposure Score: ${exposureColor.bold(`${bc.exposureScore}/100`)}`);
|
|
601
|
+
if (bc.deprecatedPackages.length > 0) {
|
|
602
|
+
lines.push(` ${chalk4.red("Deprecated")}: ${bc.deprecatedPackages.map((p) => chalk4.dim(p)).join(", ")}`);
|
|
603
|
+
}
|
|
604
|
+
if (bc.legacyPolyfills.length > 0) {
|
|
605
|
+
lines.push(` ${chalk4.yellow("Polyfills")}: ${bc.legacyPolyfills.map((p) => chalk4.dim(p)).join(", ")}`);
|
|
606
|
+
}
|
|
607
|
+
if (bc.peerConflictsDetected) {
|
|
608
|
+
lines.push(` ${chalk4.red("\u26A0")} Peer dependency conflicts detected`);
|
|
609
|
+
}
|
|
610
|
+
lines.push(` Recommendation: ${chalk4.bold(bc.overallRecommendation)}`);
|
|
611
|
+
const projectsWithPlans = bc.projectIntelligence.filter((p) => p.packages.length > 0).slice(0, 3);
|
|
612
|
+
if (projectsWithPlans.length > 0) {
|
|
613
|
+
lines.push(" Major Upgrade Intelligence:");
|
|
614
|
+
for (const p of projectsWithPlans) {
|
|
615
|
+
lines.push(` - ${p.project} (${p.recommendation})`);
|
|
616
|
+
for (const pkg2 of p.packages.slice(0, 2)) {
|
|
617
|
+
lines.push(` \xB7 ${pkg2.package} ${pkg2.currentVersion ?? "?"} \u2192 ${pkg2.targetVersion ?? "?"} | touched ~${pkg2.usage.touchedPercent}% | ${pkg2.automatable}`);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
lines.push("");
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (ext.tsModernity && ext.tsModernity.typescriptVersion) {
|
|
625
|
+
const ts = ext.tsModernity;
|
|
626
|
+
lines.push(chalk4.bold.underline(" TypeScript"));
|
|
627
|
+
const parts = [];
|
|
628
|
+
parts.push(`v${ts.typescriptVersion}`);
|
|
629
|
+
if (ts.strict === true) parts.push(chalk4.green("strict \u2714"));
|
|
630
|
+
else if (ts.strict === false) parts.push(chalk4.yellow("strict \u2716"));
|
|
631
|
+
if (ts.moduleType) parts.push(ts.moduleType.toUpperCase());
|
|
632
|
+
if (ts.target) parts.push(`target: ${ts.target}`);
|
|
633
|
+
lines.push(` ${parts.join(chalk4.dim(" \xB7 "))}`);
|
|
634
|
+
lines.push("");
|
|
635
|
+
}
|
|
636
|
+
if (ext.buildDeploy) {
|
|
637
|
+
const bd = ext.buildDeploy;
|
|
638
|
+
const hasSomething = bd.ci.length > 0 || bd.docker.dockerfileCount > 0 || bd.packageManagers.length > 0;
|
|
639
|
+
if (hasSomething) {
|
|
640
|
+
lines.push(chalk4.bold.underline(" Build & Deploy"));
|
|
641
|
+
if (bd.ci.length > 0) lines.push(` CI: ${bd.ci.join(", ")}`);
|
|
642
|
+
if (bd.docker.dockerfileCount > 0) {
|
|
643
|
+
lines.push(` Docker: ${bd.docker.dockerfileCount} Dockerfile${bd.docker.dockerfileCount !== 1 ? "s" : ""} (${bd.docker.baseImages.join(", ")})`);
|
|
644
|
+
}
|
|
645
|
+
if (bd.packageManagers.length > 0) lines.push(` Package Managers: ${bd.packageManagers.join(", ")}`);
|
|
646
|
+
if (bd.monorepoTools.length > 0) lines.push(` Monorepo: ${bd.monorepoTools.join(", ")}`);
|
|
647
|
+
if (bd.iac.length > 0) lines.push(` IaC: ${bd.iac.join(", ")}`);
|
|
648
|
+
lines.push("");
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
if (ext.uiPurpose) {
|
|
652
|
+
const up = ext.uiPurpose;
|
|
653
|
+
lines.push(chalk4.bold.underline(" Product Purpose Signals"));
|
|
654
|
+
lines.push(` Frameworks: ${up.detectedFrameworks.length > 0 ? up.detectedFrameworks.join(", ") : chalk4.dim("unknown")}`);
|
|
655
|
+
lines.push(` Evidence: ${up.topEvidence.length}${up.capped ? chalk4.dim(` of ${up.evidenceCount} (capped)`) : ""}`);
|
|
656
|
+
const top = up.topEvidence.slice(0, 8);
|
|
657
|
+
if (top.length > 0) {
|
|
658
|
+
lines.push(" Top Signals:");
|
|
659
|
+
for (const item of top) {
|
|
660
|
+
lines.push(` - [${item.kind}] ${item.value} ${chalk4.dim(`(${item.file})`)}`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
if (up.unknownSignals.length > 0) {
|
|
664
|
+
lines.push(" Unknowns:");
|
|
665
|
+
for (const u of up.unknownSignals.slice(0, 4)) {
|
|
666
|
+
lines.push(` - ${chalk4.yellow(u)}`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
lines.push("");
|
|
670
|
+
}
|
|
671
|
+
if (ext.securityPosture) {
|
|
672
|
+
const sec = ext.securityPosture;
|
|
673
|
+
lines.push(chalk4.bold.underline(" Security Posture"));
|
|
674
|
+
const checks = [];
|
|
675
|
+
checks.push(sec.lockfilePresent ? chalk4.green("Lockfile \u2714") : chalk4.red("Lockfile \u2716"));
|
|
676
|
+
checks.push(sec.gitignoreCoversEnv ? chalk4.green(".env \u2714") : chalk4.red(".env \u2716"));
|
|
677
|
+
checks.push(sec.gitignoreCoversNodeModules ? chalk4.green("node_modules \u2714") : chalk4.yellow("node_modules \u2716"));
|
|
678
|
+
if (sec.multipleLockfileTypes) checks.push(chalk4.yellow("Multiple lockfiles \u26A0"));
|
|
679
|
+
if (sec.envFilesTracked) checks.push(chalk4.red("Env files tracked \u2716"));
|
|
680
|
+
lines.push(` ${checks.join(chalk4.dim(" \xB7 "))}`);
|
|
681
|
+
lines.push("");
|
|
682
|
+
}
|
|
683
|
+
if (ext.platformMatrix) {
|
|
684
|
+
const pm = ext.platformMatrix;
|
|
685
|
+
if (pm.nativeModules.length > 0 || pm.dockerBaseImages.length > 0) {
|
|
686
|
+
lines.push(chalk4.bold.underline(" Platform"));
|
|
687
|
+
if (pm.nativeModules.length > 0) {
|
|
688
|
+
lines.push(` Native modules: ${pm.nativeModules.map((m) => chalk4.dim(m)).join(", ")}`);
|
|
689
|
+
}
|
|
690
|
+
if (pm.osAssumptions.length > 0) {
|
|
691
|
+
lines.push(` OS assumptions: ${pm.osAssumptions.join(", ")}`);
|
|
692
|
+
}
|
|
693
|
+
lines.push("");
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
if (ext.codeQuality) {
|
|
697
|
+
const cq = ext.codeQuality;
|
|
698
|
+
lines.push(chalk4.bold.underline(" Code Quality"));
|
|
699
|
+
lines.push(` Files: ${chalk4.white(`${cq.filesAnalyzed}`)} \xB7 Functions: ${chalk4.white(`${cq.functionsAnalyzed}`)} \xB7 Avg complexity: ${chalk4.white(`${cq.avgCyclomaticComplexity}`)} \xB7 Avg length: ${chalk4.white(`${cq.avgFunctionLength}`)} lines`);
|
|
700
|
+
lines.push(` Max nesting: ${cq.maxNestingDepth} \xB7 Circular deps: ${cq.circularDependencies} \xB7 Dead code: ${cq.deadCodePercent}%`);
|
|
701
|
+
if (cq.godFiles.length > 0) {
|
|
702
|
+
const preview = cq.godFiles.slice(0, 3).map((f) => `${f.path} (${f.lines} lines)`).join(", ");
|
|
703
|
+
lines.push(` ${chalk4.yellow("God files")}: ${preview}`);
|
|
704
|
+
}
|
|
705
|
+
lines.push("");
|
|
706
|
+
}
|
|
707
|
+
if (ext.dependencyGraph) {
|
|
708
|
+
const dg = ext.dependencyGraph;
|
|
709
|
+
if (dg.lockfileType) {
|
|
710
|
+
lines.push(chalk4.bold.underline(" Dependency Graph"));
|
|
711
|
+
lines.push(` ${dg.lockfileType}: ${chalk4.white(`${dg.totalUnique}`)} unique, ${chalk4.white(`${dg.totalInstalled}`)} installed`);
|
|
712
|
+
if (dg.duplicatedPackages.length > 0) {
|
|
713
|
+
lines.push(` ${chalk4.yellow(`${dg.duplicatedPackages.length} duplicated`)} packages`);
|
|
714
|
+
}
|
|
715
|
+
if (dg.phantomDependencies.length > 0) {
|
|
716
|
+
lines.push(` ${chalk4.red(`${dg.phantomDependencies.length} phantom`)} dependencies`);
|
|
717
|
+
}
|
|
718
|
+
lines.push("");
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return lines;
|
|
722
|
+
}
|
|
723
|
+
function formatArchitectureDiagram(arch) {
|
|
724
|
+
const lines = [];
|
|
725
|
+
lines.push(chalk4.bold.cyan("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
|
|
726
|
+
lines.push(chalk4.bold.cyan("\u2551 Architecture Layers \u2551"));
|
|
727
|
+
lines.push(chalk4.bold.cyan("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
|
|
728
|
+
lines.push("");
|
|
729
|
+
lines.push(chalk4.bold(" Archetype: ") + `${arch.archetype}` + chalk4.dim(` (${Math.round(arch.archetypeConfidence * 100)}% confidence)`));
|
|
730
|
+
lines.push(` Files classified: ${arch.totalClassified}` + (arch.unclassified > 0 ? chalk4.dim(` (${arch.unclassified} unclassified)`) : ""));
|
|
731
|
+
lines.push("");
|
|
732
|
+
if (arch.layers.length > 0) {
|
|
733
|
+
for (const layer of arch.layers) {
|
|
734
|
+
const risk = layer.riskLevel === "none" ? chalk4.dim("none") : layer.riskLevel === "low" ? chalk4.green("low") : layer.riskLevel === "moderate" ? chalk4.yellow("moderate") : chalk4.red("high");
|
|
735
|
+
lines.push(` ${chalk4.bold(layer.layer)} ${layer.fileCount} file${layer.fileCount !== 1 ? "s" : ""} drift ${scoreBar(layer.driftScore)} risk ${risk}`);
|
|
736
|
+
}
|
|
737
|
+
lines.push("");
|
|
738
|
+
}
|
|
739
|
+
return lines;
|
|
740
|
+
}
|
|
741
|
+
function generatePriorityActions(artifact) {
|
|
742
|
+
const actions = [];
|
|
743
|
+
const eolProjects = artifact.projects.filter(
|
|
744
|
+
(p) => p.runtimeMajorsBehind !== void 0 && p.runtimeMajorsBehind >= 3
|
|
745
|
+
);
|
|
746
|
+
if (eolProjects.length > 0) {
|
|
747
|
+
const names = eolProjects.map((p) => p.name).join(", ");
|
|
748
|
+
let detail = `End-of-life runtimes no longer receive security patches and block ecosystem upgrades.`;
|
|
749
|
+
const fileLines = [];
|
|
750
|
+
for (const p of eolProjects) {
|
|
751
|
+
fileLines.push(`
|
|
752
|
+
./${p.path}`);
|
|
753
|
+
fileLines.push(`
|
|
754
|
+
${p.runtime} \u2192 ${p.runtimeLatest} (${p.runtimeMajorsBehind} major${p.runtimeMajorsBehind > 1 ? "s" : ""} behind)`);
|
|
755
|
+
}
|
|
756
|
+
detail += fileLines.join("");
|
|
757
|
+
actions.push({
|
|
758
|
+
title: `Upgrade EOL runtime${eolProjects.length > 1 ? "s" : ""} in ${names}`,
|
|
759
|
+
explanation: detail,
|
|
760
|
+
impact: `+${Math.min(eolProjects.length * 10, 30)} points (runtime & EOL scores)`,
|
|
761
|
+
severity: 100
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
const severeFrameworks = [];
|
|
765
|
+
for (const p of artifact.projects) {
|
|
766
|
+
for (const fw of p.frameworks) {
|
|
767
|
+
if (fw.majorsBehind !== null && fw.majorsBehind >= 3) {
|
|
768
|
+
severeFrameworks.push({ name: fw.name, fw: `${fw.currentVersion} \u2192 ${fw.latestVersion}`, behind: fw.majorsBehind, project: p.name, projectPath: p.path });
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
if (severeFrameworks.length > 0) {
|
|
773
|
+
const worst = severeFrameworks.sort((a, b) => b.behind - a.behind)[0];
|
|
774
|
+
const others = severeFrameworks.length > 1 ? ` (+${severeFrameworks.length - 1} more)` : "";
|
|
775
|
+
let detail = `${worst.behind} major versions behind. Major framework drift increases breaking change risk and blocks access to security fixes and performance improvements.`;
|
|
776
|
+
const fileLines = [];
|
|
777
|
+
let shown = 0;
|
|
778
|
+
for (const sf of severeFrameworks) {
|
|
779
|
+
if (shown >= 8) break;
|
|
780
|
+
fileLines.push(`
|
|
781
|
+
./${sf.projectPath}`);
|
|
782
|
+
fileLines.push(`
|
|
783
|
+
${sf.name}: ${sf.fw} (${sf.behind} major${sf.behind > 1 ? "s" : ""} behind)`);
|
|
784
|
+
shown++;
|
|
785
|
+
}
|
|
786
|
+
const remaining = severeFrameworks.length - shown;
|
|
787
|
+
detail += fileLines.join("");
|
|
788
|
+
if (remaining > 0) detail += `
|
|
789
|
+
... and ${remaining} more`;
|
|
790
|
+
actions.push({
|
|
791
|
+
title: `Upgrade ${worst.name} ${worst.fw} in ${worst.project}${others}`,
|
|
792
|
+
explanation: detail,
|
|
793
|
+
impact: `+5\u201315 points`,
|
|
794
|
+
severity: 90
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
for (const p of artifact.projects) {
|
|
798
|
+
const b = p.dependencyAgeBuckets;
|
|
799
|
+
const total = b.current + b.oneBehind + b.twoPlusBehind;
|
|
800
|
+
if (total === 0) continue;
|
|
801
|
+
const twoPlusPct = Math.round(b.twoPlusBehind / total * 100);
|
|
802
|
+
if (twoPlusPct >= 40) {
|
|
803
|
+
let detail = `${b.twoPlusBehind} of ${total} dependencies are 2+ majors behind. Run \`npm outdated\` and prioritise packages with known CVEs or breaking API changes.`;
|
|
804
|
+
const worstDeps = p.dependencies.filter((d) => d.majorsBehind !== null && d.majorsBehind >= 2).sort((a, b2) => (b2.majorsBehind ?? 0) - (a.majorsBehind ?? 0));
|
|
805
|
+
if (worstDeps.length > 0) {
|
|
806
|
+
const depLines = [];
|
|
807
|
+
let shown = 0;
|
|
808
|
+
depLines.push(`
|
|
809
|
+
./${p.path}`);
|
|
810
|
+
for (const dep of worstDeps) {
|
|
811
|
+
if (shown >= 8) break;
|
|
812
|
+
const current = dep.resolvedVersion ?? dep.currentSpec;
|
|
813
|
+
const latest = dep.latestStable ?? "?";
|
|
814
|
+
depLines.push(`
|
|
815
|
+
${dep.package}: ${current} \u2192 ${latest} (${dep.majorsBehind} major${dep.majorsBehind > 1 ? "s" : ""} behind)`);
|
|
816
|
+
shown++;
|
|
817
|
+
}
|
|
818
|
+
const remaining = worstDeps.length - shown;
|
|
819
|
+
detail += depLines.join("");
|
|
820
|
+
if (remaining > 0) detail += `
|
|
821
|
+
... and ${remaining} more`;
|
|
822
|
+
}
|
|
823
|
+
actions.push({
|
|
824
|
+
title: `Reduce dependency rot in ${p.name} (${twoPlusPct}% severely outdated)`,
|
|
825
|
+
explanation: detail,
|
|
826
|
+
impact: `+5\u201310 points`,
|
|
827
|
+
severity: 80 + twoPlusPct / 10
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
const twoMajorFrameworks = [];
|
|
832
|
+
for (const p of artifact.projects) {
|
|
833
|
+
for (const fw of p.frameworks) {
|
|
834
|
+
if (fw.majorsBehind === 2) {
|
|
835
|
+
twoMajorFrameworks.push({ name: fw.name, project: p.name, projectPath: p.path, fw: `${fw.currentVersion} \u2192 ${fw.latestVersion}` });
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
const uniqueTwo = [...new Map(twoMajorFrameworks.map((f) => [f.name, f])).values()];
|
|
840
|
+
if (uniqueTwo.length > 0) {
|
|
841
|
+
const list = uniqueTwo.slice(0, 3).map((f) => `${f.name} (${f.fw})`).join(", ");
|
|
842
|
+
const moreCount = uniqueTwo.length > 3 ? ` +${uniqueTwo.length - 3} more` : "";
|
|
843
|
+
let detail = `These frameworks are 2 major versions behind. Create upgrade tickets and check migration guides \u2014 the gap will widen with each new release.`;
|
|
844
|
+
const fileLines = [];
|
|
845
|
+
let shown = 0;
|
|
846
|
+
for (const tf of twoMajorFrameworks) {
|
|
847
|
+
if (shown >= 8) break;
|
|
848
|
+
fileLines.push(`
|
|
849
|
+
./${tf.projectPath}`);
|
|
850
|
+
fileLines.push(`
|
|
851
|
+
${tf.name}: ${tf.fw}`);
|
|
852
|
+
shown++;
|
|
853
|
+
}
|
|
854
|
+
const remaining = twoMajorFrameworks.length - shown;
|
|
855
|
+
detail += fileLines.join("");
|
|
856
|
+
if (remaining > 0) detail += `
|
|
857
|
+
... and ${remaining} more`;
|
|
858
|
+
actions.push({
|
|
859
|
+
title: `Plan major framework upgrades: ${list}${moreCount}`,
|
|
860
|
+
explanation: detail,
|
|
861
|
+
impact: `+5\u201310 points`,
|
|
862
|
+
severity: 60
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
if (artifact.extended?.breakingChangeExposure) {
|
|
866
|
+
const bc = artifact.extended.breakingChangeExposure;
|
|
867
|
+
const total = bc.deprecatedPackages.length + bc.legacyPolyfills.length;
|
|
868
|
+
if (total > 0) {
|
|
869
|
+
const items = [...bc.deprecatedPackages, ...bc.legacyPolyfills].slice(0, 5).join(", ");
|
|
870
|
+
const moreCount = total > 5 ? ` +${total - 5} more` : "";
|
|
871
|
+
let detail = `${total} package${total !== 1 ? "s" : ""} are deprecated or legacy polyfills. These receive no updates and may have known vulnerabilities.`;
|
|
872
|
+
const allPkgNames = /* @__PURE__ */ new Set([...bc.deprecatedPackages, ...bc.legacyPolyfills]);
|
|
873
|
+
const fileLines = [];
|
|
874
|
+
let shown = 0;
|
|
875
|
+
for (const p of artifact.projects) {
|
|
876
|
+
const matches = p.dependencies.filter((d) => allPkgNames.has(d.package));
|
|
877
|
+
if (matches.length === 0) continue;
|
|
878
|
+
if (shown >= 10) break;
|
|
879
|
+
fileLines.push(`
|
|
880
|
+
./${p.path}`);
|
|
881
|
+
for (const dep of matches) {
|
|
882
|
+
if (shown >= 10) break;
|
|
883
|
+
const ver = dep.resolvedVersion ?? dep.currentSpec;
|
|
884
|
+
const label = bc.deprecatedPackages.includes(dep.package) ? "deprecated" : "polyfill";
|
|
885
|
+
fileLines.push(`
|
|
886
|
+
${dep.package}: ${ver} (${label})`);
|
|
887
|
+
shown++;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
const remaining = total - shown;
|
|
891
|
+
detail += fileLines.join("");
|
|
892
|
+
if (remaining > 0) detail += `
|
|
893
|
+
... and ${remaining} more`;
|
|
894
|
+
actions.push({
|
|
895
|
+
title: `Replace deprecated/legacy packages: ${items}${moreCount}`,
|
|
896
|
+
explanation: detail,
|
|
897
|
+
severity: 55
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
if (artifact.extended?.dependencyGraph) {
|
|
902
|
+
const dg = artifact.extended.dependencyGraph;
|
|
903
|
+
const phantomCount = dg.phantomDependencies.length;
|
|
904
|
+
if (phantomCount >= 10) {
|
|
905
|
+
let detail = `Packages used in code but not declared in package.json. These rely on transitive installs and can break unpredictably when other packages update.`;
|
|
906
|
+
const details = dg.phantomDependencyDetails;
|
|
907
|
+
if (details && details.length > 0) {
|
|
908
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
909
|
+
for (const d of details) {
|
|
910
|
+
if (!byPath.has(d.sourcePath)) byPath.set(d.sourcePath, []);
|
|
911
|
+
byPath.get(d.sourcePath).push({ package: d.package, spec: d.spec });
|
|
912
|
+
}
|
|
913
|
+
const pathLines = [];
|
|
914
|
+
let shown = 0;
|
|
915
|
+
for (const [srcPath, pkgs] of byPath) {
|
|
916
|
+
if (shown >= 10) break;
|
|
917
|
+
pathLines.push(`
|
|
918
|
+
./${srcPath}`);
|
|
919
|
+
for (const pkg2 of pkgs) {
|
|
920
|
+
if (shown >= 10) break;
|
|
921
|
+
pathLines.push(`
|
|
922
|
+
${pkg2.package}: ${pkg2.spec}`);
|
|
923
|
+
shown++;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
const remaining = phantomCount - shown;
|
|
927
|
+
detail += pathLines.join("");
|
|
928
|
+
if (remaining > 0) detail += `
|
|
929
|
+
... and ${remaining} more`;
|
|
930
|
+
}
|
|
931
|
+
actions.push({
|
|
932
|
+
title: `Fix ${phantomCount} phantom dependencies`,
|
|
933
|
+
explanation: detail,
|
|
934
|
+
severity: 45
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
if (artifact.extended?.securityPosture) {
|
|
939
|
+
const sec = artifact.extended.securityPosture;
|
|
940
|
+
if (sec.envFilesTracked || !sec.lockfilePresent) {
|
|
941
|
+
const issues = [];
|
|
942
|
+
if (sec.envFilesTracked) issues.push(".env files are tracked in git");
|
|
943
|
+
if (!sec.lockfilePresent) issues.push("no lockfile found");
|
|
944
|
+
let detail;
|
|
945
|
+
if (sec.envFilesTracked) {
|
|
946
|
+
detail = "Environment files may contain secrets. Add them to .gitignore and rotate any exposed credentials immediately.";
|
|
947
|
+
detail += "\n ./.gitignore";
|
|
948
|
+
detail += "\n Add: .env, .env.*, .env.local";
|
|
949
|
+
} else {
|
|
950
|
+
detail = "Without a lockfile, installs are non-deterministic. Run the install command to generate one and commit it.";
|
|
951
|
+
detail += "\n ./";
|
|
952
|
+
detail += `
|
|
953
|
+
Missing: ${sec.lockfileTypes.length > 0 ? sec.lockfileTypes.join(", ") + " (multiple types detected)" : "package-lock.json, pnpm-lock.yaml, or yarn.lock"}`;
|
|
954
|
+
}
|
|
955
|
+
actions.push({
|
|
956
|
+
title: `Fix security posture: ${issues.join(", ")}`,
|
|
957
|
+
explanation: detail,
|
|
958
|
+
severity: 95
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
if (artifact.extended?.dependencyGraph) {
|
|
963
|
+
const dupes = artifact.extended.dependencyGraph.duplicatedPackages;
|
|
964
|
+
const highImpactDupes = dupes.filter((d) => d.versions.length >= 3);
|
|
965
|
+
if (highImpactDupes.length >= 3) {
|
|
966
|
+
const names = highImpactDupes.slice(0, 4).map((d) => `${d.name} (${d.versions.length}v)`).join(", ");
|
|
967
|
+
let detail = `${highImpactDupes.length} packages have 3+ versions installed. Run \`npm dedupe\` to reduce bundle size and install time.`;
|
|
968
|
+
const dupeLines = [];
|
|
969
|
+
let shown = 0;
|
|
970
|
+
for (const d of highImpactDupes) {
|
|
971
|
+
if (shown >= 8) break;
|
|
972
|
+
dupeLines.push(`
|
|
973
|
+
${d.name}: ${d.versions.join(", ")} (${d.consumers} consumer${d.consumers !== 1 ? "s" : ""})`);
|
|
974
|
+
shown++;
|
|
975
|
+
}
|
|
976
|
+
const remaining = highImpactDupes.length - shown;
|
|
977
|
+
detail += dupeLines.join("");
|
|
978
|
+
if (remaining > 0) detail += `
|
|
979
|
+
... and ${remaining} more`;
|
|
980
|
+
actions.push({
|
|
981
|
+
title: `Deduplicate heavily-versioned packages`,
|
|
982
|
+
explanation: detail,
|
|
983
|
+
severity: 35
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
actions.sort((a, b) => b.severity - a.severity);
|
|
988
|
+
return actions.slice(0, 5);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// src/formatters/markdown.ts
|
|
992
|
+
function formatMarkdown(artifact) {
|
|
993
|
+
const lines = [];
|
|
994
|
+
lines.push("# Vibgrate Drift Report");
|
|
995
|
+
lines.push("");
|
|
996
|
+
lines.push(`| Metric | Value |`);
|
|
997
|
+
lines.push(`|--------|-------|`);
|
|
998
|
+
lines.push(`| **Drift Score** | ${artifact.drift.score}/100 |`);
|
|
999
|
+
lines.push(`| **Risk Level** | ${artifact.drift.riskLevel.toUpperCase()} |`);
|
|
1000
|
+
lines.push(`| **Projects** | ${artifact.projects.length} |`);
|
|
1001
|
+
const scannedMeta = [artifact.timestamp];
|
|
1002
|
+
if (artifact.durationMs !== void 0) scannedMeta.push(`${(artifact.durationMs / 1e3).toFixed(1)}s`);
|
|
1003
|
+
if (artifact.filesScanned !== void 0) scannedMeta.push(`${artifact.filesScanned} files`);
|
|
1004
|
+
if (artifact.treeSummary) scannedMeta.push(`${artifact.treeSummary.totalFiles.toLocaleString()} workspace files \xB7 ${artifact.treeSummary.totalDirs.toLocaleString()} dirs`);
|
|
1005
|
+
lines.push(`| **Scanned** | ${scannedMeta.join(" \xB7 ")} |`);
|
|
1006
|
+
if (artifact.vcs) {
|
|
1007
|
+
lines.push(`| **VCS** | ${artifact.vcs.type} |`);
|
|
1008
|
+
if (artifact.vcs.branch) lines.push(`| **Branch** | ${artifact.vcs.branch} |`);
|
|
1009
|
+
if (artifact.vcs.sha) lines.push(`| **Commit** | \`${artifact.vcs.shortSha}\` |`);
|
|
1010
|
+
}
|
|
1011
|
+
lines.push("");
|
|
1012
|
+
lines.push("## Score Breakdown");
|
|
1013
|
+
lines.push("");
|
|
1014
|
+
lines.push(`| Component | Score |`);
|
|
1015
|
+
lines.push(`|-----------|-------|`);
|
|
1016
|
+
lines.push(`| Runtime | ${artifact.drift.components.runtimeScore} |`);
|
|
1017
|
+
lines.push(`| Frameworks | ${artifact.drift.components.frameworkScore} |`);
|
|
1018
|
+
lines.push(`| Dependencies | ${artifact.drift.components.dependencyScore} |`);
|
|
1019
|
+
lines.push(`| EOL Risk | ${artifact.drift.components.eolScore} |`);
|
|
1020
|
+
lines.push("");
|
|
1021
|
+
lines.push("## Projects");
|
|
1022
|
+
lines.push("");
|
|
1023
|
+
for (const project of artifact.projects) {
|
|
1024
|
+
lines.push(`### ${project.name} (${project.type})`);
|
|
1025
|
+
lines.push("");
|
|
1026
|
+
if (project.runtime) {
|
|
1027
|
+
const lag = project.runtimeMajorsBehind !== void 0 && project.runtimeMajorsBehind > 0 ? ` \u2014 ${project.runtimeMajorsBehind} major(s) behind` : " \u2014 current";
|
|
1028
|
+
lines.push(`- **Runtime:** ${project.runtime}${lag}`);
|
|
1029
|
+
}
|
|
1030
|
+
if (project.frameworks.length > 0) {
|
|
1031
|
+
lines.push("- **Frameworks:**");
|
|
1032
|
+
for (const fw of project.frameworks) {
|
|
1033
|
+
const lag = fw.majorsBehind !== null ? fw.majorsBehind === 0 ? "current" : `${fw.majorsBehind} behind` : "unknown";
|
|
1034
|
+
lines.push(` - ${fw.name}: ${fw.currentVersion ?? "?"} \u2192 ${fw.latestVersion ?? "?"} (${lag})`);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
const b = project.dependencyAgeBuckets;
|
|
1038
|
+
const total = b.current + b.oneBehind + b.twoPlusBehind + b.unknown;
|
|
1039
|
+
if (total > 0) {
|
|
1040
|
+
lines.push(`- **Dependencies:** ${b.current} current, ${b.oneBehind} 1-behind, ${b.twoPlusBehind} 2+ behind, ${b.unknown} unknown`);
|
|
1041
|
+
}
|
|
1042
|
+
lines.push("");
|
|
1043
|
+
}
|
|
1044
|
+
if (artifact.extended?.uiPurpose) {
|
|
1045
|
+
const up = artifact.extended.uiPurpose;
|
|
1046
|
+
lines.push("## Product Purpose Signals");
|
|
1047
|
+
lines.push("");
|
|
1048
|
+
lines.push(`- **Frameworks:** ${up.detectedFrameworks.length > 0 ? up.detectedFrameworks.join(", ") : "unknown"}`);
|
|
1049
|
+
lines.push(`- **Evidence Items:** ${up.topEvidence.length}${up.capped ? ` (capped from ${up.evidenceCount})` : ""}`);
|
|
1050
|
+
if (up.topEvidence.length > 0) {
|
|
1051
|
+
lines.push("- **Top Evidence:**");
|
|
1052
|
+
for (const item of up.topEvidence.slice(0, 10)) {
|
|
1053
|
+
lines.push(` - [${item.kind}] ${item.value} (${item.file})`);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
if (up.unknownSignals.length > 0) {
|
|
1057
|
+
lines.push("- **Unknowns:**");
|
|
1058
|
+
for (const u of up.unknownSignals.slice(0, 5)) {
|
|
1059
|
+
lines.push(` - ${u}`);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
lines.push("");
|
|
1063
|
+
}
|
|
1064
|
+
if (artifact.findings.length > 0) {
|
|
1065
|
+
lines.push("## Findings");
|
|
1066
|
+
lines.push("");
|
|
1067
|
+
lines.push(`| Level | Rule | Message | Location |`);
|
|
1068
|
+
lines.push(`|-------|------|---------|----------|`);
|
|
1069
|
+
for (const f of artifact.findings) {
|
|
1070
|
+
const emoji = f.level === "error" ? "\u{1F534}" : f.level === "warning" ? "\u{1F7E1}" : "\u{1F535}";
|
|
1071
|
+
lines.push(`| ${emoji} ${f.level} | ${f.ruleId} | ${f.message} | ${f.location} |`);
|
|
1072
|
+
}
|
|
1073
|
+
lines.push("");
|
|
1074
|
+
}
|
|
1075
|
+
if (artifact.delta !== void 0) {
|
|
1076
|
+
const dir = artifact.delta > 0 ? "\u{1F4C8}" : artifact.delta < 0 ? "\u{1F4C9}" : "\u27A1\uFE0F";
|
|
1077
|
+
lines.push(`## Drift Delta: ${dir} ${artifact.delta > 0 ? "+" : ""}${artifact.delta} vs baseline`);
|
|
1078
|
+
lines.push("");
|
|
1079
|
+
}
|
|
1080
|
+
return lines.join("\n");
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// src/commands/report.ts
|
|
1084
|
+
var reportCommand = new Command4("report").description("Generate a drift report from a scan artifact").option("--in <file>", "Input artifact file", ".vibgrate/scan_result.json").option("--format <format>", "Output format (md|text|json)", "text").action(async (opts) => {
|
|
1085
|
+
const artifactPath = path4.resolve(opts.in);
|
|
61
1086
|
if (!await pathExists(artifactPath)) {
|
|
62
|
-
console.error(
|
|
63
|
-
console.error(
|
|
1087
|
+
console.error(chalk5.red(`Artifact not found: ${artifactPath}`));
|
|
1088
|
+
console.error(chalk5.dim('Run "vibgrate scan" first to generate a scan artifact.'));
|
|
64
1089
|
process.exit(1);
|
|
65
1090
|
}
|
|
66
1091
|
const artifact = await readJsonFile(artifactPath);
|
|
@@ -78,20 +1103,370 @@ var reportCommand = new Command2("report").description("Generate a drift report
|
|
|
78
1103
|
}
|
|
79
1104
|
});
|
|
80
1105
|
|
|
1106
|
+
// src/commands/push.ts
|
|
1107
|
+
import * as crypto2 from "crypto";
|
|
1108
|
+
import * as path5 from "path";
|
|
1109
|
+
import { Command as Command5 } from "commander";
|
|
1110
|
+
import chalk6 from "chalk";
|
|
1111
|
+
|
|
1112
|
+
// src/utils/compact-artifact.ts
|
|
1113
|
+
import * as zlib from "zlib";
|
|
1114
|
+
import { promisify } from "util";
|
|
1115
|
+
|
|
1116
|
+
// src/utils/compact-evidence.ts
|
|
1117
|
+
var CATEGORY_PATTERNS = [
|
|
1118
|
+
{ category: "pricing", pattern: /price|pricing|billing|subscri|trial|credit|plan|tier|upgrade|premium|pro|enterprise/i },
|
|
1119
|
+
{ category: "auth", pattern: /sign[- ]?in|sign[- ]?up|log[- ]?in|log[- ]?out|auth|sso|oauth|password|register|invite|onboard/i },
|
|
1120
|
+
{ category: "dashboard", pattern: /dashboard|overview|home|main|summary|stats/i },
|
|
1121
|
+
{ category: "settings", pattern: /setting|config|preference|option|profile|account/i },
|
|
1122
|
+
{ category: "users", pattern: /user|member|team|role|permission|access|admin|owner/i },
|
|
1123
|
+
{ category: "integrations", pattern: /integrat|connect|webhook|api[- ]?key|sync|import|export/i },
|
|
1124
|
+
{ category: "reports", pattern: /report|analy|metric|chart|graph|insight|track/i },
|
|
1125
|
+
{ category: "workflows", pattern: /workflow|automat|schedule|trigger|action|job|task|pipeline/i },
|
|
1126
|
+
{ category: "projects", pattern: /project|workspace|organization|folder|repo/i },
|
|
1127
|
+
{ category: "navigation", pattern: /menu|nav|sidebar|header|footer|breadcrumb/i }
|
|
1128
|
+
];
|
|
1129
|
+
function compactUiPurpose(result, maxSamplesPerCategory = 3) {
|
|
1130
|
+
const evidence = result.topEvidence;
|
|
1131
|
+
const dependencies = evidence.filter((e) => e.kind === "dependency").map((e) => e.value).slice(0, 10);
|
|
1132
|
+
const routes = dedupeRoutes(
|
|
1133
|
+
evidence.filter((e) => e.kind === "route").map((e) => e.value)
|
|
1134
|
+
).slice(0, 15);
|
|
1135
|
+
const textEvidence = evidence.filter(
|
|
1136
|
+
(e) => e.kind !== "dependency" && e.kind !== "route" && e.kind !== "feature_flag"
|
|
1137
|
+
);
|
|
1138
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
1139
|
+
const categoryCounts = {};
|
|
1140
|
+
for (const item of textEvidence) {
|
|
1141
|
+
const category = categorize(item.value);
|
|
1142
|
+
if (!byCategory.has(category)) {
|
|
1143
|
+
byCategory.set(category, /* @__PURE__ */ new Set());
|
|
1144
|
+
}
|
|
1145
|
+
const normalized = normalizeValue(item.value);
|
|
1146
|
+
if (normalized.length >= 3) {
|
|
1147
|
+
byCategory.get(category).add(normalized);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
const samples = [];
|
|
1151
|
+
for (const [category, values] of byCategory) {
|
|
1152
|
+
const deduped = dedupeStrings([...values]);
|
|
1153
|
+
categoryCounts[category] = deduped.length;
|
|
1154
|
+
for (const value of deduped.slice(0, maxSamplesPerCategory)) {
|
|
1155
|
+
samples.push({ kind: "text", value, category });
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
const featureFlags = evidence.filter((e) => e.kind === "feature_flag");
|
|
1159
|
+
if (featureFlags.length > 0) {
|
|
1160
|
+
categoryCounts["feature_flags"] = featureFlags.length;
|
|
1161
|
+
samples.push({ kind: "feature_flag", value: "feature flags detected", category: "feature_flags" });
|
|
1162
|
+
}
|
|
1163
|
+
return {
|
|
1164
|
+
samples,
|
|
1165
|
+
categoryCounts,
|
|
1166
|
+
originalCount: evidence.length,
|
|
1167
|
+
dependencies,
|
|
1168
|
+
routes,
|
|
1169
|
+
detectedFrameworks: result.detectedFrameworks
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
function categorize(value) {
|
|
1173
|
+
for (const { category, pattern } of CATEGORY_PATTERNS) {
|
|
1174
|
+
if (pattern.test(value)) return category;
|
|
1175
|
+
}
|
|
1176
|
+
return "general";
|
|
1177
|
+
}
|
|
1178
|
+
function normalizeValue(value) {
|
|
1179
|
+
return value.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").replace(/\s+/g, " ").trim().slice(0, 60);
|
|
1180
|
+
}
|
|
1181
|
+
function dedupeStrings(values) {
|
|
1182
|
+
const sorted = values.sort((a, b) => b.length - a.length);
|
|
1183
|
+
const kept = [];
|
|
1184
|
+
for (const value of sorted) {
|
|
1185
|
+
const isDupe = kept.some((k) => {
|
|
1186
|
+
const stem = value.slice(0, 6);
|
|
1187
|
+
return k.startsWith(stem) || k.includes(value) || value.includes(k);
|
|
1188
|
+
});
|
|
1189
|
+
if (!isDupe) {
|
|
1190
|
+
kept.push(value);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
return kept;
|
|
1194
|
+
}
|
|
1195
|
+
function dedupeRoutes(routes) {
|
|
1196
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1197
|
+
const result = [];
|
|
1198
|
+
for (const route of routes) {
|
|
1199
|
+
const normalized = route.replace(/:[a-z_]+/gi, ":param").replace(/\[\[*\.*\.*[a-z_]+\]*\]/gi, ":param").replace(/\/+$/, "").toLowerCase();
|
|
1200
|
+
if (!seen.has(normalized)) {
|
|
1201
|
+
seen.add(normalized);
|
|
1202
|
+
result.push(route);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
return result;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// src/utils/compact-artifact.ts
|
|
1209
|
+
var gzip2 = promisify(zlib.gzip);
|
|
1210
|
+
var MAX_ITEMS = 50;
|
|
1211
|
+
function extractName(entry) {
|
|
1212
|
+
const match = entry.match(/^(.+?)\s*\(/);
|
|
1213
|
+
return match ? match[1].trim() : entry.trim();
|
|
1214
|
+
}
|
|
1215
|
+
function compactDataStores(result) {
|
|
1216
|
+
return {
|
|
1217
|
+
databaseTechnologies: result.databaseTechnologies.slice(0, 10),
|
|
1218
|
+
connectionStrings: [],
|
|
1219
|
+
// Don't include connection strings in upload
|
|
1220
|
+
connectionPoolSettings: result.connectionPoolSettings.slice(0, MAX_ITEMS),
|
|
1221
|
+
replicationSettings: result.replicationSettings.slice(0, 20),
|
|
1222
|
+
readReplicaSettings: result.readReplicaSettings.slice(0, 20),
|
|
1223
|
+
failoverSettings: result.failoverSettings.slice(0, 20),
|
|
1224
|
+
collationAndEncoding: result.collationAndEncoding.slice(0, 20),
|
|
1225
|
+
queryTimeoutDefaults: result.queryTimeoutDefaults.slice(0, 20),
|
|
1226
|
+
manualIndexes: result.manualIndexes.map(extractName).slice(0, MAX_ITEMS),
|
|
1227
|
+
tables: result.tables.map(extractName).slice(0, MAX_ITEMS),
|
|
1228
|
+
views: result.views.map(extractName).slice(0, MAX_ITEMS),
|
|
1229
|
+
storedProcedures: result.storedProcedures.map(extractName).slice(0, MAX_ITEMS),
|
|
1230
|
+
triggers: result.triggers.map(extractName).slice(0, MAX_ITEMS),
|
|
1231
|
+
rowLevelSecurityPolicies: result.rowLevelSecurityPolicies.slice(0, 20),
|
|
1232
|
+
otherServices: result.otherServices.slice(0, 20)
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
function compactApiSurface(result) {
|
|
1236
|
+
const seenProviders = /* @__PURE__ */ new Set();
|
|
1237
|
+
const uniqueIntegrations = result.integrations.filter((i) => {
|
|
1238
|
+
const domain = i.provider.split(":")[0];
|
|
1239
|
+
if (seenProviders.has(domain)) return false;
|
|
1240
|
+
seenProviders.add(domain);
|
|
1241
|
+
return true;
|
|
1242
|
+
}).slice(0, MAX_ITEMS).map((i) => ({
|
|
1243
|
+
provider: i.provider,
|
|
1244
|
+
endpoint: "",
|
|
1245
|
+
// Don't include full endpoints
|
|
1246
|
+
version: i.version,
|
|
1247
|
+
parameters: [],
|
|
1248
|
+
// Don't include params
|
|
1249
|
+
configOptions: [],
|
|
1250
|
+
authHints: [],
|
|
1251
|
+
files: []
|
|
1252
|
+
// Don't include file paths
|
|
1253
|
+
}));
|
|
1254
|
+
return {
|
|
1255
|
+
integrations: uniqueIntegrations,
|
|
1256
|
+
openApiSpecifications: result.openApiSpecifications.slice(0, 10),
|
|
1257
|
+
webhookUrls: result.webhookUrls.slice(0, 20),
|
|
1258
|
+
callbackEndpoints: result.callbackEndpoints.slice(0, 20),
|
|
1259
|
+
apiVersionPins: result.apiVersionPins.slice(0, 20),
|
|
1260
|
+
tokenExpirationPolicies: result.tokenExpirationPolicies.slice(0, 20),
|
|
1261
|
+
rateLimitOverrides: result.rateLimitOverrides.slice(0, 20),
|
|
1262
|
+
customHeaders: result.customHeaders.slice(0, 20),
|
|
1263
|
+
corsPolicies: result.corsPolicies.slice(0, 20),
|
|
1264
|
+
oauthScopes: result.oauthScopes.slice(0, 20),
|
|
1265
|
+
apiTokens: []
|
|
1266
|
+
// Don't include token references
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
function compactAssetBranding(result) {
|
|
1270
|
+
return {
|
|
1271
|
+
faviconFiles: result.faviconFiles.slice(0, 1),
|
|
1272
|
+
productLogos: []
|
|
1273
|
+
// Don't include logos
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
function prepareArtifactForUpload(artifact) {
|
|
1277
|
+
const compacted = { ...artifact };
|
|
1278
|
+
if (compacted.extended) {
|
|
1279
|
+
const ext = { ...compacted.extended };
|
|
1280
|
+
if (ext.dataStores) {
|
|
1281
|
+
ext.dataStores = compactDataStores(ext.dataStores);
|
|
1282
|
+
}
|
|
1283
|
+
if (ext.apiSurface) {
|
|
1284
|
+
ext.apiSurface = compactApiSurface(ext.apiSurface);
|
|
1285
|
+
}
|
|
1286
|
+
if (ext.assetBranding) {
|
|
1287
|
+
ext.assetBranding = compactAssetBranding(ext.assetBranding);
|
|
1288
|
+
}
|
|
1289
|
+
if (ext.uiPurpose) {
|
|
1290
|
+
const compactedUi = compactUiPurpose(ext.uiPurpose);
|
|
1291
|
+
ext.uiPurpose = {
|
|
1292
|
+
enabled: ext.uiPurpose.enabled,
|
|
1293
|
+
detectedFrameworks: compactedUi.detectedFrameworks,
|
|
1294
|
+
evidenceCount: compactedUi.originalCount,
|
|
1295
|
+
capped: ext.uiPurpose.capped,
|
|
1296
|
+
topEvidence: [],
|
|
1297
|
+
// Clear full evidence
|
|
1298
|
+
unknownSignals: [],
|
|
1299
|
+
// Add compacted data under extended properties
|
|
1300
|
+
...{ compacted: compactedUi }
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
if (ext.runtimeConfiguration) {
|
|
1304
|
+
ext.runtimeConfiguration = {
|
|
1305
|
+
...ext.runtimeConfiguration,
|
|
1306
|
+
environmentVariables: ext.runtimeConfiguration.environmentVariables.slice(0, 100),
|
|
1307
|
+
hiddenConfigFiles: ext.runtimeConfiguration.hiddenConfigFiles.slice(0, MAX_ITEMS),
|
|
1308
|
+
startupArguments: ext.runtimeConfiguration.startupArguments.slice(0, 100)
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
if (ext.operationalResilience) {
|
|
1312
|
+
const ops = ext.operationalResilience;
|
|
1313
|
+
ext.operationalResilience = {
|
|
1314
|
+
implicitTimeouts: ops.implicitTimeouts.slice(0, 30),
|
|
1315
|
+
defaultPaginationSize: ops.defaultPaginationSize.slice(0, 30),
|
|
1316
|
+
implicitRetryLogic: ops.implicitRetryLogic.slice(0, 30),
|
|
1317
|
+
defaultLocale: ops.defaultLocale.slice(0, 20),
|
|
1318
|
+
defaultCurrency: ops.defaultCurrency.slice(0, 20),
|
|
1319
|
+
implicitTimezone: ops.implicitTimezone.slice(0, 20),
|
|
1320
|
+
defaultCharacterEncoding: ops.defaultCharacterEncoding.slice(0, 20),
|
|
1321
|
+
sessionStores: ops.sessionStores.slice(0, 20),
|
|
1322
|
+
distributedLocks: ops.distributedLocks.slice(0, 20),
|
|
1323
|
+
jobSchedulers: ops.jobSchedulers.slice(0, 30),
|
|
1324
|
+
idempotencyKeys: ops.idempotencyKeys.slice(0, 20),
|
|
1325
|
+
rateLimitingCounters: ops.rateLimitingCounters.slice(0, 20),
|
|
1326
|
+
circuitBreakerState: ops.circuitBreakerState.slice(0, 20),
|
|
1327
|
+
abTestToggles: ops.abTestToggles.slice(0, 20),
|
|
1328
|
+
regionalEnablementRules: ops.regionalEnablementRules.slice(0, 20),
|
|
1329
|
+
betaAccessGroups: ops.betaAccessGroups.slice(0, 20),
|
|
1330
|
+
licensingEnforcementLogic: ops.licensingEnforcementLogic.slice(0, 20),
|
|
1331
|
+
killSwitches: ops.killSwitches.slice(0, 20),
|
|
1332
|
+
connectorRetryLogic: ops.connectorRetryLogic.slice(0, 20),
|
|
1333
|
+
apiPollingIntervals: ops.apiPollingIntervals.slice(0, 20),
|
|
1334
|
+
fieldMappings: ops.fieldMappings.slice(0, 20),
|
|
1335
|
+
schemaRegistryRules: ops.schemaRegistryRules.slice(0, 20),
|
|
1336
|
+
deadLetterQueueBehavior: ops.deadLetterQueueBehavior.slice(0, 20),
|
|
1337
|
+
dataMaskingRules: ops.dataMaskingRules.slice(0, 20),
|
|
1338
|
+
transformationLogic: ops.transformationLogic.slice(0, 20),
|
|
1339
|
+
timezoneHandling: ops.timezoneHandling.slice(0, 20),
|
|
1340
|
+
encryptionSettings: ops.encryptionSettings.slice(0, 30),
|
|
1341
|
+
hardcodedSecretSignals: ops.hardcodedSecretSignals.slice(0, 20)
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
if (ext.dependencyGraph) {
|
|
1345
|
+
ext.dependencyGraph = {
|
|
1346
|
+
...ext.dependencyGraph,
|
|
1347
|
+
phantomDependencies: ext.dependencyGraph.phantomDependencies.slice(0, MAX_ITEMS),
|
|
1348
|
+
phantomDependencyDetails: ext.dependencyGraph.phantomDependencyDetails?.slice(0, MAX_ITEMS),
|
|
1349
|
+
duplicatedPackages: ext.dependencyGraph.duplicatedPackages.slice(0, MAX_ITEMS)
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
compacted.extended = ext;
|
|
1353
|
+
}
|
|
1354
|
+
return compacted;
|
|
1355
|
+
}
|
|
1356
|
+
async function compressArtifact(artifact) {
|
|
1357
|
+
const json = JSON.stringify(artifact);
|
|
1358
|
+
return gzip2(json, { level: 9 });
|
|
1359
|
+
}
|
|
1360
|
+
async function prepareCompressedUpload2(artifact) {
|
|
1361
|
+
const compacted = prepareArtifactForUpload(artifact);
|
|
1362
|
+
const compressed = await compressArtifact(compacted);
|
|
1363
|
+
return { body: compressed, contentEncoding: "gzip" };
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// src/commands/push.ts
|
|
1367
|
+
function parseDsn2(dsn) {
|
|
1368
|
+
const cleaned = dsn.replace(/[\x00-\x1F\x7F\uFEFF\u200B-\u200D\u2060]/g, "").trim();
|
|
1369
|
+
const match = cleaned.match(/^vibgrate\+(https?):?\/\/([^:]+):([^@]+)@([^/]+)\/(.+)$/);
|
|
1370
|
+
if (!match) return null;
|
|
1371
|
+
return {
|
|
1372
|
+
scheme: match[1],
|
|
1373
|
+
keyId: match[2],
|
|
1374
|
+
secret: match[3],
|
|
1375
|
+
host: match[4],
|
|
1376
|
+
workspaceId: match[5]
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
function computeHmac(body, secret) {
|
|
1380
|
+
return crypto2.createHmac("sha256", secret).update(body).digest("base64");
|
|
1381
|
+
}
|
|
1382
|
+
var pushCommand = new Command5("push").description("Push scan results to Vibgrate API").option("--dsn <dsn>", "DSN token (or use VIBGRATE_DSN env)").option("--region <region>", "Override data residency region (us, eu)").option("--file <file>", "Scan artifact file", ".vibgrate/scan_result.json").option("--strict", "Fail on upload errors").action(async (opts) => {
|
|
1383
|
+
const dsn = opts.dsn || process.env.VIBGRATE_DSN;
|
|
1384
|
+
if (!dsn) {
|
|
1385
|
+
console.error(chalk6.red("No DSN provided."));
|
|
1386
|
+
console.error(chalk6.dim("Set VIBGRATE_DSN environment variable or use --dsn flag."));
|
|
1387
|
+
if (opts.strict) process.exit(1);
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
const parsed = parseDsn2(dsn);
|
|
1391
|
+
if (!parsed) {
|
|
1392
|
+
console.error(chalk6.red("Invalid DSN format."));
|
|
1393
|
+
console.error(chalk6.dim("Expected: vibgrate+https://<key_id>:<secret>@<host>/<workspace_id>"));
|
|
1394
|
+
if (opts.strict) process.exit(1);
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
const filePath = path5.resolve(opts.file);
|
|
1398
|
+
if (!await pathExists(filePath)) {
|
|
1399
|
+
console.error(chalk6.red(`Scan artifact not found: ${filePath}`));
|
|
1400
|
+
console.error(chalk6.dim('Run "vibgrate scan" first.'));
|
|
1401
|
+
if (opts.strict) process.exit(1);
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
const artifact = await readJsonFile(filePath);
|
|
1405
|
+
const { body, contentEncoding } = await prepareCompressedUpload2(artifact);
|
|
1406
|
+
const timestamp = String(Date.now());
|
|
1407
|
+
let host = parsed.host;
|
|
1408
|
+
if (opts.region) {
|
|
1409
|
+
try {
|
|
1410
|
+
host = resolveIngestHost(opts.region);
|
|
1411
|
+
} catch (e) {
|
|
1412
|
+
console.error(chalk6.red(e instanceof Error ? e.message : String(e)));
|
|
1413
|
+
if (opts.strict) process.exit(1);
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
const url = `${parsed.scheme}://${host}/v1/ingest/scan`;
|
|
1418
|
+
const originalSize = JSON.stringify(artifact).length;
|
|
1419
|
+
const compressedSize = body.length;
|
|
1420
|
+
const ratio = ((1 - compressedSize / originalSize) * 100).toFixed(0);
|
|
1421
|
+
console.log(chalk6.dim(`Uploading to ${host}... (${(compressedSize / 1024).toFixed(0)} KB, ${ratio}% smaller)`));
|
|
1422
|
+
try {
|
|
1423
|
+
const response = await fetch(url, {
|
|
1424
|
+
method: "POST",
|
|
1425
|
+
headers: {
|
|
1426
|
+
"Content-Type": "application/json",
|
|
1427
|
+
"Content-Encoding": contentEncoding,
|
|
1428
|
+
"X-Vibgrate-Timestamp": timestamp,
|
|
1429
|
+
"Authorization": `VibgrateDSN ${parsed.keyId}:${parsed.secret}`,
|
|
1430
|
+
"Connection": "close"
|
|
1431
|
+
// Prevent keep-alive delays on exit
|
|
1432
|
+
},
|
|
1433
|
+
body
|
|
1434
|
+
});
|
|
1435
|
+
if (!response.ok) {
|
|
1436
|
+
const text = await response.text();
|
|
1437
|
+
throw new Error(`HTTP ${response.status}: ${text}`);
|
|
1438
|
+
}
|
|
1439
|
+
const result = await response.json();
|
|
1440
|
+
console.log(chalk6.green("\u2714") + ` Scan queued for processing (${result.ingestId ?? "ok"})`);
|
|
1441
|
+
console.log();
|
|
1442
|
+
console.log(chalk6.dim("Processing continues in the background. Results available shortly."));
|
|
1443
|
+
console.log();
|
|
1444
|
+
if (result.ingestId) {
|
|
1445
|
+
const dashHost = host.includes("eu.") ? "dash.vibgrate.eu" : "dash.vibgrate.com";
|
|
1446
|
+
const reportUrl = `https://${dashHost}/${parsed.workspaceId}/scan/${result.ingestId}`;
|
|
1447
|
+
console.log(chalk6.dim("View report: ") + chalk6.underline(reportUrl));
|
|
1448
|
+
}
|
|
1449
|
+
} catch (e) {
|
|
1450
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1451
|
+
console.error(chalk6.red(`Upload failed: ${msg}`));
|
|
1452
|
+
if (opts.strict) process.exit(1);
|
|
1453
|
+
}
|
|
1454
|
+
});
|
|
1455
|
+
|
|
81
1456
|
// src/commands/update.ts
|
|
82
1457
|
import { execSync } from "child_process";
|
|
83
|
-
import * as
|
|
84
|
-
import { Command as
|
|
85
|
-
import
|
|
1458
|
+
import * as path7 from "path";
|
|
1459
|
+
import { Command as Command6 } from "commander";
|
|
1460
|
+
import chalk7 from "chalk";
|
|
86
1461
|
|
|
87
1462
|
// src/utils/update-check.ts
|
|
1463
|
+
var import_semver = __toESM(require_semver(), 1);
|
|
88
1464
|
import * as fs from "fs/promises";
|
|
89
|
-
import * as
|
|
1465
|
+
import * as path6 from "path";
|
|
90
1466
|
import * as os from "os";
|
|
91
|
-
import semver from "semver";
|
|
92
1467
|
var REGISTRY_URL = "https://registry.npmjs.org/@vibgrate%2fcli/latest";
|
|
93
|
-
var CACHE_DIR =
|
|
94
|
-
var CACHE_FILE =
|
|
1468
|
+
var CACHE_DIR = path6.join(os.homedir(), ".vibgrate");
|
|
1469
|
+
var CACHE_FILE = path6.join(CACHE_DIR, "update-check.json");
|
|
95
1470
|
var CHECK_INTERVAL_MS = 12 * 60 * 60 * 1e3;
|
|
96
1471
|
async function checkForUpdate() {
|
|
97
1472
|
try {
|
|
@@ -100,7 +1475,7 @@ async function checkForUpdate() {
|
|
|
100
1475
|
return {
|
|
101
1476
|
current: VERSION,
|
|
102
1477
|
latest: cached.latest,
|
|
103
|
-
updateAvailable:
|
|
1478
|
+
updateAvailable: import_semver.default.gt(cached.latest, VERSION)
|
|
104
1479
|
};
|
|
105
1480
|
}
|
|
106
1481
|
const controller = new AbortController();
|
|
@@ -118,12 +1493,12 @@ async function checkForUpdate() {
|
|
|
118
1493
|
if (!response.ok) return null;
|
|
119
1494
|
const data = await response.json();
|
|
120
1495
|
const latest = data.version;
|
|
121
|
-
if (!latest || !
|
|
1496
|
+
if (!latest || !import_semver.default.valid(latest)) return null;
|
|
122
1497
|
await writeCache({ latest, checkedAt: Date.now() });
|
|
123
1498
|
return {
|
|
124
1499
|
current: VERSION,
|
|
125
1500
|
latest,
|
|
126
|
-
updateAvailable:
|
|
1501
|
+
updateAvailable: import_semver.default.gt(latest, VERSION)
|
|
127
1502
|
};
|
|
128
1503
|
} catch {
|
|
129
1504
|
return null;
|
|
@@ -146,7 +1521,7 @@ async function fetchLatestVersion() {
|
|
|
146
1521
|
if (!response.ok) return null;
|
|
147
1522
|
const data = await response.json();
|
|
148
1523
|
const latest = data.version;
|
|
149
|
-
if (!latest || !
|
|
1524
|
+
if (!latest || !import_semver.default.valid(latest)) return null;
|
|
150
1525
|
await writeCache({ latest, checkedAt: Date.now() });
|
|
151
1526
|
return latest;
|
|
152
1527
|
} catch {
|
|
@@ -184,8 +1559,8 @@ function detectGlobalInstall() {
|
|
|
184
1559
|
}
|
|
185
1560
|
return null;
|
|
186
1561
|
}
|
|
187
|
-
function getGlobalUpdateCommand(pm,
|
|
188
|
-
const spec = `${
|
|
1562
|
+
function getGlobalUpdateCommand(pm, pkg2, version) {
|
|
1563
|
+
const spec = `${pkg2}@${version}`;
|
|
189
1564
|
switch (pm) {
|
|
190
1565
|
case "pnpm":
|
|
191
1566
|
return `pnpm add -g ${spec}`;
|
|
@@ -199,13 +1574,13 @@ function getGlobalUpdateCommand(pm, pkg, version) {
|
|
|
199
1574
|
}
|
|
200
1575
|
}
|
|
201
1576
|
async function detectPackageManager(cwd) {
|
|
202
|
-
if (await pathExists(
|
|
203
|
-
if (await pathExists(
|
|
204
|
-
if (await pathExists(
|
|
1577
|
+
if (await pathExists(path7.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
1578
|
+
if (await pathExists(path7.join(cwd, "bun.lockb"))) return "bun";
|
|
1579
|
+
if (await pathExists(path7.join(cwd, "yarn.lock"))) return "yarn";
|
|
205
1580
|
return "npm";
|
|
206
1581
|
}
|
|
207
|
-
function getInstallCommand(pm,
|
|
208
|
-
const spec = `${
|
|
1582
|
+
function getInstallCommand(pm, pkg2, version, isDev) {
|
|
1583
|
+
const spec = `${pkg2}@${version}`;
|
|
209
1584
|
switch (pm) {
|
|
210
1585
|
case "pnpm":
|
|
211
1586
|
return isDev ? `pnpm add -D ${spec}` : `pnpm add ${spec}`;
|
|
@@ -220,30 +1595,30 @@ function getInstallCommand(pm, pkg, version, isDev) {
|
|
|
220
1595
|
}
|
|
221
1596
|
async function isDevDependency(cwd) {
|
|
222
1597
|
try {
|
|
223
|
-
const pkgPath =
|
|
1598
|
+
const pkgPath = path7.join(cwd, "package.json");
|
|
224
1599
|
const raw = await (await import("fs/promises")).readFile(pkgPath, "utf-8");
|
|
225
|
-
const
|
|
226
|
-
return Boolean(
|
|
1600
|
+
const pkg2 = JSON.parse(raw);
|
|
1601
|
+
return Boolean(pkg2.devDependencies?.["@vibgrate/cli"]);
|
|
227
1602
|
} catch {
|
|
228
1603
|
return true;
|
|
229
1604
|
}
|
|
230
1605
|
}
|
|
231
|
-
var updateCommand = new
|
|
232
|
-
console.log(
|
|
233
|
-
console.log(
|
|
1606
|
+
var updateCommand = new Command6("update").description("Update vibgrate to the latest version").option("--check", "Only check for updates, do not install").option("--pm <manager>", "Package manager to use (npm, pnpm, yarn, bun)").option("--global", "Update global installation").action(async (opts) => {
|
|
1607
|
+
console.log(chalk7.dim(`Current version: ${VERSION}`));
|
|
1608
|
+
console.log(chalk7.dim("Checking npm registry..."));
|
|
234
1609
|
const latest = await fetchLatestVersion();
|
|
235
1610
|
if (!latest) {
|
|
236
|
-
console.error(
|
|
1611
|
+
console.error(chalk7.red("Could not reach the npm registry. Check your network connection."));
|
|
237
1612
|
process.exit(1);
|
|
238
1613
|
}
|
|
239
|
-
const semver2 = await import("semver");
|
|
1614
|
+
const semver2 = await import("./semver-JBJZTHUX.js");
|
|
240
1615
|
if (!semver2.gt(latest, VERSION)) {
|
|
241
|
-
console.log(
|
|
1616
|
+
console.log(chalk7.green("\u2714") + ` You are on the latest version (${VERSION}).`);
|
|
242
1617
|
return;
|
|
243
1618
|
}
|
|
244
|
-
console.log(
|
|
1619
|
+
console.log(chalk7.yellow(`Update available: ${VERSION} \u2192 ${latest}`));
|
|
245
1620
|
if (opts.check) {
|
|
246
|
-
console.log(
|
|
1621
|
+
console.log(chalk7.dim('Run "vibgrate update" to install.'));
|
|
247
1622
|
return;
|
|
248
1623
|
}
|
|
249
1624
|
const cwd = process.cwd();
|
|
@@ -253,26 +1628,26 @@ var updateCommand = new Command3("update").description("Update vibgrate to the l
|
|
|
253
1628
|
let cmd;
|
|
254
1629
|
if (isGlobal) {
|
|
255
1630
|
cmd = getGlobalUpdateCommand(pm, "@vibgrate/cli", latest);
|
|
256
|
-
console.log(
|
|
1631
|
+
console.log(chalk7.dim(`Updating global installation with ${pm}: ${cmd}`));
|
|
257
1632
|
} else {
|
|
258
1633
|
const isDev = await isDevDependency(cwd);
|
|
259
1634
|
cmd = getInstallCommand(pm, "@vibgrate/cli", latest, isDev);
|
|
260
|
-
console.log(
|
|
1635
|
+
console.log(chalk7.dim(`Using ${pm}: ${cmd}`));
|
|
261
1636
|
}
|
|
262
1637
|
try {
|
|
263
1638
|
execSync(cmd, { cwd, stdio: "inherit" });
|
|
264
|
-
console.log(
|
|
1639
|
+
console.log(chalk7.green("\u2714") + ` Updated to @vibgrate/cli@${latest}`);
|
|
265
1640
|
} catch {
|
|
266
|
-
console.error(
|
|
1641
|
+
console.error(chalk7.red(`Update failed. Run manually: ${cmd}`));
|
|
267
1642
|
process.exit(1);
|
|
268
1643
|
}
|
|
269
1644
|
});
|
|
270
1645
|
|
|
271
1646
|
// src/commands/sbom.ts
|
|
272
|
-
import * as
|
|
1647
|
+
import * as path8 from "path";
|
|
273
1648
|
import { randomUUID } from "crypto";
|
|
274
|
-
import { Command as
|
|
275
|
-
import
|
|
1649
|
+
import { Command as Command7 } from "commander";
|
|
1650
|
+
import chalk8 from "chalk";
|
|
276
1651
|
function flattenDependencies(artifact) {
|
|
277
1652
|
const rows = [];
|
|
278
1653
|
for (const project of artifact.projects) {
|
|
@@ -410,50 +1785,50 @@ function formatDeltaText(base, current) {
|
|
|
410
1785
|
return lines.join("\n");
|
|
411
1786
|
}
|
|
412
1787
|
async function readArtifactOrExit(filePath) {
|
|
413
|
-
const absolutePath =
|
|
1788
|
+
const absolutePath = path8.resolve(filePath);
|
|
414
1789
|
if (!await pathExists(absolutePath)) {
|
|
415
|
-
console.error(
|
|
1790
|
+
console.error(chalk8.red(`Artifact not found: ${absolutePath}`));
|
|
416
1791
|
process.exit(1);
|
|
417
1792
|
}
|
|
418
1793
|
return readJsonFile(absolutePath);
|
|
419
1794
|
}
|
|
420
|
-
var exportCommand = new
|
|
1795
|
+
var exportCommand = new Command7("export").description("Export scan artifact as SBOM").option("--in <file>", "Input artifact file", ".vibgrate/scan_result.json").option("--out <file>", "Output SBOM file").option("--format <format>", "SBOM format (cyclonedx|spdx)", "cyclonedx").action(async (opts) => {
|
|
421
1796
|
const artifact = await readArtifactOrExit(opts.in);
|
|
422
1797
|
const format = opts.format.toLowerCase();
|
|
423
1798
|
if (format !== "cyclonedx" && format !== "spdx") {
|
|
424
|
-
console.error(
|
|
1799
|
+
console.error(chalk8.red("Invalid SBOM format. Use cyclonedx or spdx."));
|
|
425
1800
|
process.exit(1);
|
|
426
1801
|
}
|
|
427
1802
|
const sbom = format === "cyclonedx" ? toCycloneDx(artifact) : toSpdx(artifact);
|
|
428
1803
|
const body = JSON.stringify(sbom, null, 2);
|
|
429
1804
|
if (opts.out) {
|
|
430
|
-
await writeTextFile(
|
|
431
|
-
console.log(
|
|
1805
|
+
await writeTextFile(path8.resolve(opts.out), body);
|
|
1806
|
+
console.log(chalk8.green("\u2714") + ` SBOM written to ${opts.out}`);
|
|
432
1807
|
} else {
|
|
433
1808
|
console.log(body);
|
|
434
1809
|
}
|
|
435
1810
|
});
|
|
436
|
-
var deltaCommand = new
|
|
1811
|
+
var deltaCommand = new Command7("delta").description("Show SBOM delta between two scan artifacts").requiredOption("--from <file>", "Baseline scan artifact path").requiredOption("--to <file>", "Current scan artifact path").option("--out <file>", "Write report to file").action(async (opts) => {
|
|
437
1812
|
const base = await readArtifactOrExit(opts.from);
|
|
438
1813
|
const current = await readArtifactOrExit(opts.to);
|
|
439
1814
|
const report = formatDeltaText(base, current);
|
|
440
1815
|
if (opts.out) {
|
|
441
|
-
await writeTextFile(
|
|
442
|
-
console.log(
|
|
1816
|
+
await writeTextFile(path8.resolve(opts.out), report);
|
|
1817
|
+
console.log(chalk8.green("\u2714") + ` SBOM delta report written to ${opts.out}`);
|
|
443
1818
|
} else {
|
|
444
1819
|
console.log(report);
|
|
445
1820
|
}
|
|
446
1821
|
});
|
|
447
|
-
var sbomCommand = new
|
|
1822
|
+
var sbomCommand = new Command7("sbom").description("SBOM export and delta reports for dependency drift tracking").addCommand(exportCommand).addCommand(deltaCommand);
|
|
448
1823
|
|
|
449
1824
|
// src/commands/extract.ts
|
|
450
|
-
import * as
|
|
1825
|
+
import * as path9 from "path";
|
|
451
1826
|
import * as os2 from "os";
|
|
452
1827
|
import * as fs2 from "fs/promises";
|
|
453
1828
|
import { existsSync } from "fs";
|
|
454
1829
|
import { spawn, spawnSync } from "child_process";
|
|
455
|
-
import { Command as
|
|
456
|
-
import
|
|
1830
|
+
import { Command as Command8 } from "commander";
|
|
1831
|
+
import chalk9 from "chalk";
|
|
457
1832
|
var EXIT_SUCCESS = 0;
|
|
458
1833
|
var EXIT_SCHEMA_FAILURE = 1;
|
|
459
1834
|
var EXIT_PARSE_FAILURE = 2;
|
|
@@ -543,8 +1918,8 @@ async function detectLanguages(rootDir, includeTests) {
|
|
|
543
1918
|
}
|
|
544
1919
|
for (const entry of entries) {
|
|
545
1920
|
if (entry.name.startsWith(".")) continue;
|
|
546
|
-
const absPath =
|
|
547
|
-
const relPath =
|
|
1921
|
+
const absPath = path9.join(dir, entry.name);
|
|
1922
|
+
const relPath = path9.join(relBase, entry.name);
|
|
548
1923
|
if (entry.isDirectory()) {
|
|
549
1924
|
if (!SKIP_DIRS.has(entry.name)) {
|
|
550
1925
|
await walk(absPath, relPath);
|
|
@@ -553,7 +1928,7 @@ async function detectLanguages(rootDir, includeTests) {
|
|
|
553
1928
|
}
|
|
554
1929
|
if (!entry.isFile()) continue;
|
|
555
1930
|
if (!includeTests && isTestPath(relPath)) continue;
|
|
556
|
-
const ext =
|
|
1931
|
+
const ext = path9.extname(entry.name).toLowerCase();
|
|
557
1932
|
for (const [lang, exts] of Object.entries(LANGUAGE_EXTENSIONS)) {
|
|
558
1933
|
if (exts.has(ext)) {
|
|
559
1934
|
counts.set(lang, (counts.get(lang) ?? 0) + 1);
|
|
@@ -629,10 +2004,10 @@ function splitHcsOutput(lines) {
|
|
|
629
2004
|
return { preamble, facts };
|
|
630
2005
|
}
|
|
631
2006
|
function resolveHcsWorkerBin() {
|
|
632
|
-
const base = import.meta.dirname ??
|
|
633
|
-
const bundledPath =
|
|
2007
|
+
const base = import.meta.dirname ?? path9.dirname(new URL(import.meta.url).pathname);
|
|
2008
|
+
const bundledPath = path9.resolve(base, "..", "dist", "hcs-worker.js");
|
|
634
2009
|
if (existsSync(bundledPath)) return bundledPath;
|
|
635
|
-
const siblingPath =
|
|
2010
|
+
const siblingPath = path9.resolve(base, "hcs-worker.js");
|
|
636
2011
|
if (existsSync(siblingPath)) return siblingPath;
|
|
637
2012
|
try {
|
|
638
2013
|
const resolved = import.meta.resolve("@vibgrate/hcs-node-worker");
|
|
@@ -640,9 +2015,9 @@ function resolveHcsWorkerBin() {
|
|
|
640
2015
|
if (existsSync(resolvedPath)) return resolvedPath;
|
|
641
2016
|
} catch {
|
|
642
2017
|
}
|
|
643
|
-
const monorepoPath =
|
|
2018
|
+
const monorepoPath = path9.resolve(base, "..", "..", "..", "vibgrate-hcs", "node", "dist", "main.js");
|
|
644
2019
|
if (existsSync(monorepoPath)) return monorepoPath;
|
|
645
|
-
const devPath =
|
|
2020
|
+
const devPath = path9.resolve(base, "..", "..", "..", "vibgrate-hcs", "node", "src", "main.ts");
|
|
646
2021
|
if (existsSync(devPath)) return devPath;
|
|
647
2022
|
throw new Error(
|
|
648
2023
|
'Cannot locate HCS worker. Run "pnpm build" in the CLI package to bundle the worker into dist/.'
|
|
@@ -745,7 +2120,7 @@ async function runNodeWorker(rootDir, language, opts) {
|
|
|
745
2120
|
args.push("--cobol-copybook-paths", opts.copybookPaths);
|
|
746
2121
|
}
|
|
747
2122
|
}
|
|
748
|
-
return new Promise((
|
|
2123
|
+
return new Promise((resolve9) => {
|
|
749
2124
|
const facts = [];
|
|
750
2125
|
const errors = [];
|
|
751
2126
|
let stdoutBuf = "";
|
|
@@ -782,7 +2157,7 @@ async function runNodeWorker(rootDir, language, opts) {
|
|
|
782
2157
|
continue;
|
|
783
2158
|
}
|
|
784
2159
|
if (opts.verbose) {
|
|
785
|
-
process.stderr.write(
|
|
2160
|
+
process.stderr.write(chalk9.dim(`[${language}] ${trimmed}
|
|
786
2161
|
`));
|
|
787
2162
|
}
|
|
788
2163
|
if (trimmed.startsWith("[error]")) {
|
|
@@ -796,15 +2171,15 @@ async function runNodeWorker(rootDir, language, opts) {
|
|
|
796
2171
|
facts.push(stdoutBuf.trim());
|
|
797
2172
|
}
|
|
798
2173
|
if (killed) {
|
|
799
|
-
|
|
2174
|
+
resolve9({ language, facts, errors: ["Worker killed: timeout exceeded"], exitCode: EXIT_TIMEOUT });
|
|
800
2175
|
} else {
|
|
801
|
-
|
|
2176
|
+
resolve9({ language, facts, errors, exitCode: code ?? 0 });
|
|
802
2177
|
}
|
|
803
2178
|
});
|
|
804
2179
|
child.on("error", (err) => {
|
|
805
2180
|
clearTimeout(timer);
|
|
806
2181
|
errors.push(`Failed to spawn worker: ${err.message}`);
|
|
807
|
-
|
|
2182
|
+
resolve9({ language, facts, errors, exitCode: EXIT_PARSE_FAILURE });
|
|
808
2183
|
});
|
|
809
2184
|
});
|
|
810
2185
|
}
|
|
@@ -824,7 +2199,7 @@ function resolveDotnetPublishedWorker(workersDir) {
|
|
|
824
2199
|
};
|
|
825
2200
|
const candidates = candidatesByPlatform[process.platform] ?? [];
|
|
826
2201
|
for (const filename of candidates) {
|
|
827
|
-
const fullPath =
|
|
2202
|
+
const fullPath = path9.join(workersDir, filename);
|
|
828
2203
|
if (existsSync(fullPath)) {
|
|
829
2204
|
return fullPath;
|
|
830
2205
|
}
|
|
@@ -832,30 +2207,30 @@ function resolveDotnetPublishedWorker(workersDir) {
|
|
|
832
2207
|
return null;
|
|
833
2208
|
}
|
|
834
2209
|
function resolveNativeWorker(language, projectDir) {
|
|
835
|
-
const base = import.meta.dirname ??
|
|
836
|
-
const hcsFromBundle =
|
|
837
|
-
const hcsFromSrc =
|
|
2210
|
+
const base = import.meta.dirname ?? path9.dirname(new URL(import.meta.url).pathname);
|
|
2211
|
+
const hcsFromBundle = path9.resolve(base, "..", "..", "vibgrate-hcs");
|
|
2212
|
+
const hcsFromSrc = path9.resolve(base, "..", "..", "..", "vibgrate-hcs");
|
|
838
2213
|
const hcsRoot = existsSync(hcsFromBundle) ? hcsFromBundle : hcsFromSrc;
|
|
839
|
-
const workersDir =
|
|
2214
|
+
const workersDir = path9.resolve(base, "workers");
|
|
840
2215
|
switch (language) {
|
|
841
2216
|
case "go": {
|
|
842
|
-
const bin =
|
|
2217
|
+
const bin = path9.join(workersDir, process.platform === "win32" ? "vibgrate-hcs-go.exe" : "vibgrate-hcs-go");
|
|
843
2218
|
if (existsSync(bin)) {
|
|
844
2219
|
return { cmd: bin, args: ["--project", projectDir, "--output", "ndjson"] };
|
|
845
2220
|
}
|
|
846
|
-
const src =
|
|
847
|
-
if (existsSync(
|
|
2221
|
+
const src = path9.join(hcsRoot, "go");
|
|
2222
|
+
if (existsSync(path9.join(src, "main.go"))) {
|
|
848
2223
|
return { cmd: "go", args: ["run", ".", "--project", projectDir, "--output", "ndjson"], cwd: src };
|
|
849
2224
|
}
|
|
850
2225
|
return null;
|
|
851
2226
|
}
|
|
852
2227
|
case "python": {
|
|
853
|
-
const bin =
|
|
2228
|
+
const bin = path9.join(workersDir, "vibgrate-hcs-python");
|
|
854
2229
|
if (existsSync(bin)) {
|
|
855
2230
|
return { cmd: bin, args: ["--project", projectDir, "--output", "ndjson"] };
|
|
856
2231
|
}
|
|
857
|
-
const src =
|
|
858
|
-
if (existsSync(
|
|
2232
|
+
const src = path9.join(hcsRoot, "python");
|
|
2233
|
+
if (existsSync(path9.join(src, "pyproject.toml"))) {
|
|
859
2234
|
return {
|
|
860
2235
|
cmd: "python3",
|
|
861
2236
|
args: ["-m", "vibgrate_hcs_python.main", "--project", projectDir, "--output", "ndjson"],
|
|
@@ -865,13 +2240,13 @@ function resolveNativeWorker(language, projectDir) {
|
|
|
865
2240
|
return null;
|
|
866
2241
|
}
|
|
867
2242
|
case "java": {
|
|
868
|
-
const jar =
|
|
2243
|
+
const jar = path9.join(workersDir, "vibgrate-hcs-jvm.jar");
|
|
869
2244
|
if (existsSync(jar)) {
|
|
870
2245
|
return { cmd: "java", args: ["-jar", jar, "--project", projectDir, "--output", "ndjson"] };
|
|
871
2246
|
}
|
|
872
|
-
const src =
|
|
873
|
-
if (existsSync(
|
|
874
|
-
const gradlew =
|
|
2247
|
+
const src = path9.join(hcsRoot, "jvm");
|
|
2248
|
+
if (existsSync(path9.join(src, "build.gradle.kts"))) {
|
|
2249
|
+
const gradlew = path9.join(src, process.platform === "win32" ? "gradlew.bat" : "gradlew");
|
|
875
2250
|
const launcher = existsSync(gradlew) ? gradlew : "gradle";
|
|
876
2251
|
return {
|
|
877
2252
|
cmd: launcher,
|
|
@@ -887,11 +2262,11 @@ function resolveNativeWorker(language, projectDir) {
|
|
|
887
2262
|
if (publishedWorker) {
|
|
888
2263
|
return { cmd: publishedWorker, args: ["--project", projectDir, "--output", "ndjson"] };
|
|
889
2264
|
}
|
|
890
|
-
const dll =
|
|
2265
|
+
const dll = path9.join(workersDir, "VibgrateHcsWorker.dll");
|
|
891
2266
|
if (existsSync(dll)) {
|
|
892
2267
|
return { cmd: "dotnet", args: [dll, "--project", projectDir, "--output", "ndjson"] };
|
|
893
2268
|
}
|
|
894
|
-
const csproj =
|
|
2269
|
+
const csproj = path9.join(hcsRoot, "dotnet", "src", "VibgrateHcsWorker", "VibgrateHcsWorker.csproj");
|
|
895
2270
|
if (existsSync(csproj)) {
|
|
896
2271
|
return {
|
|
897
2272
|
cmd: "dotnet",
|
|
@@ -917,19 +2292,19 @@ async function runNativeWorker(rootDir, language, opts) {
|
|
|
917
2292
|
exitCode: EXIT_PARSE_FAILURE
|
|
918
2293
|
};
|
|
919
2294
|
}
|
|
920
|
-
return new Promise((
|
|
2295
|
+
return new Promise((resolve9) => {
|
|
921
2296
|
const facts = [];
|
|
922
2297
|
const errors = [];
|
|
923
2298
|
let stdoutBuf = "";
|
|
924
2299
|
let killed = false;
|
|
925
2300
|
if (opts.verbose) {
|
|
926
|
-
process.stderr.write(
|
|
2301
|
+
process.stderr.write(chalk9.dim(`[${language}] Spawning: ${spec.cmd} ${spec.args.join(" ")}
|
|
927
2302
|
`));
|
|
928
2303
|
}
|
|
929
2304
|
const runtimeReq = NATIVE_RUNTIME_REQUIREMENTS[language];
|
|
930
2305
|
const usesPathCommand = runtimeReq && spec.cmd === runtimeReq.command;
|
|
931
2306
|
if (usesPathCommand && !commandExistsOnPath(spec.cmd)) {
|
|
932
|
-
|
|
2307
|
+
resolve9({
|
|
933
2308
|
language,
|
|
934
2309
|
facts: [],
|
|
935
2310
|
errors: [
|
|
@@ -964,7 +2339,7 @@ async function runNativeWorker(rootDir, language, opts) {
|
|
|
964
2339
|
const trimmed = line.trim();
|
|
965
2340
|
if (!trimmed) continue;
|
|
966
2341
|
if (opts.verbose) {
|
|
967
|
-
process.stderr.write(
|
|
2342
|
+
process.stderr.write(chalk9.dim(`[${language}] ${trimmed}
|
|
968
2343
|
`));
|
|
969
2344
|
}
|
|
970
2345
|
if (trimmed.startsWith("[error]")) {
|
|
@@ -976,9 +2351,9 @@ async function runNativeWorker(rootDir, language, opts) {
|
|
|
976
2351
|
clearTimeout(timer);
|
|
977
2352
|
if (stdoutBuf.trim()) facts.push(stdoutBuf.trim());
|
|
978
2353
|
if (killed) {
|
|
979
|
-
|
|
2354
|
+
resolve9({ language, facts, errors: ["Worker killed: timeout exceeded"], exitCode: EXIT_TIMEOUT });
|
|
980
2355
|
} else {
|
|
981
|
-
|
|
2356
|
+
resolve9({ language, facts, errors, exitCode: code ?? 0 });
|
|
982
2357
|
}
|
|
983
2358
|
});
|
|
984
2359
|
child.on("error", (err) => {
|
|
@@ -987,21 +2362,21 @@ async function runNativeWorker(rootDir, language, opts) {
|
|
|
987
2362
|
errors.push(
|
|
988
2363
|
isNotFound ? `[error] '${spec.cmd}' not found. ${buildNativeInstallHint(language)}` : `[error] Failed to spawn native worker: ${err.message}`
|
|
989
2364
|
);
|
|
990
|
-
|
|
2365
|
+
resolve9({ language, facts, errors, exitCode: EXIT_PARSE_FAILURE });
|
|
991
2366
|
});
|
|
992
2367
|
});
|
|
993
2368
|
}
|
|
994
2369
|
async function pushFacts(facts, dsn, verbose) {
|
|
995
|
-
const parsed =
|
|
2370
|
+
const parsed = parseDsn2(dsn);
|
|
996
2371
|
if (!parsed) {
|
|
997
|
-
process.stderr.write(
|
|
998
|
-
process.stderr.write(
|
|
2372
|
+
process.stderr.write(chalk9.red("Invalid DSN format.\n"));
|
|
2373
|
+
process.stderr.write(chalk9.dim("Expected: vibgrate+https://<key_id>:<secret>@<host>/<workspace_id>\n"));
|
|
999
2374
|
return false;
|
|
1000
2375
|
}
|
|
1001
2376
|
const body = facts.join("\n") + "\n";
|
|
1002
2377
|
const url = `${parsed.scheme}://${parsed.host}/v1/ingest/hcs`;
|
|
1003
2378
|
if (verbose) {
|
|
1004
|
-
process.stderr.write(
|
|
2379
|
+
process.stderr.write(chalk9.dim(`Pushing ${facts.length} facts to ${parsed.host}...
|
|
1005
2380
|
`));
|
|
1006
2381
|
}
|
|
1007
2382
|
try {
|
|
@@ -1019,17 +2394,17 @@ async function pushFacts(facts, dsn, verbose) {
|
|
|
1019
2394
|
});
|
|
1020
2395
|
if (!response.ok) {
|
|
1021
2396
|
const text = await response.text().catch(() => "");
|
|
1022
|
-
process.stderr.write(
|
|
2397
|
+
process.stderr.write(chalk9.red(`Push failed: HTTP ${response.status} ${text}
|
|
1023
2398
|
`));
|
|
1024
2399
|
return false;
|
|
1025
2400
|
}
|
|
1026
2401
|
if (verbose) {
|
|
1027
|
-
process.stderr.write(
|
|
2402
|
+
process.stderr.write(chalk9.green(`\u2714 Pushed ${facts.length} facts successfully
|
|
1028
2403
|
`));
|
|
1029
2404
|
}
|
|
1030
2405
|
return true;
|
|
1031
2406
|
} catch (err) {
|
|
1032
|
-
process.stderr.write(
|
|
2407
|
+
process.stderr.write(chalk9.red(`Push failed: ${err instanceof Error ? err.message : String(err)}
|
|
1033
2408
|
`));
|
|
1034
2409
|
return false;
|
|
1035
2410
|
}
|
|
@@ -1096,21 +2471,21 @@ var ProgressTracker = class {
|
|
|
1096
2471
|
const parts = [];
|
|
1097
2472
|
for (const [lang, st] of this.langStatus) {
|
|
1098
2473
|
if (st.done) {
|
|
1099
|
-
parts.push(
|
|
2474
|
+
parts.push(chalk9.green(`${lang} \u2713`));
|
|
1100
2475
|
} else if (st.phase === "waiting") {
|
|
1101
|
-
parts.push(
|
|
2476
|
+
parts.push(chalk9.dim(`${lang} \xB7`));
|
|
1102
2477
|
} else if (st.phase === "discovering") {
|
|
1103
|
-
parts.push(
|
|
2478
|
+
parts.push(chalk9.yellow(`${lang} \u2026`));
|
|
1104
2479
|
} else if (st.phase === "scanning" && st.fileCount > 0) {
|
|
1105
2480
|
const pct = Math.round(st.fileIndex / st.fileCount * 100);
|
|
1106
|
-
parts.push(
|
|
2481
|
+
parts.push(chalk9.cyan(`${lang} ${st.fileIndex}/${st.fileCount} ${pct}%`));
|
|
1107
2482
|
} else if (st.phase === "extracting") {
|
|
1108
|
-
parts.push(
|
|
2483
|
+
parts.push(chalk9.cyan(`${lang} extracting`));
|
|
1109
2484
|
} else {
|
|
1110
|
-
parts.push(
|
|
2485
|
+
parts.push(chalk9.dim(`${lang} ${st.phase}`));
|
|
1111
2486
|
}
|
|
1112
2487
|
}
|
|
1113
|
-
const line = ` ${parts.join(" ")} ${
|
|
2488
|
+
const line = ` ${parts.join(" ")} ${chalk9.dim(`${this.totalFacts} facts`)}`;
|
|
1114
2489
|
const padding = Math.max(0, this.lastLineLen - line.length);
|
|
1115
2490
|
process.stderr.write(`\r${line}${" ".repeat(padding)}`);
|
|
1116
2491
|
this.lastLineLen = line.length;
|
|
@@ -1122,10 +2497,10 @@ var ProgressTracker = class {
|
|
|
1122
2497
|
}
|
|
1123
2498
|
}
|
|
1124
2499
|
};
|
|
1125
|
-
var extractCommand = new
|
|
1126
|
-
const rootDir =
|
|
2500
|
+
var extractCommand = new Command8("extract").description("Analyze source code and emit validated HCS facts (NDJSON)").argument("[path]", "Path to source directory", ".").option("-o, --out <file>", "Write NDJSON to file (default: stdout)").option("--language <langs>", "Comma-separated languages to analyze (default: auto-detect)").option("--include-tests", "Include test files in analysis").option("--push", "Stream validated facts to dashboard API").option("--dsn <dsn>", "DSN token for push (or use VIBGRATE_DSN env)").option("--concurrency <n>", "Number of parallel file workers").option("--timeout-mins <mins>", "Total analysis timeout in minutes", "60").option("--feedback <file>", "Load NDJSON diff artifact for refinement").option("--verbose", "Print worker stderr and summary statistics").action(async (targetPath, opts) => {
|
|
2501
|
+
const rootDir = path9.resolve(targetPath);
|
|
1127
2502
|
if (!await pathExists(rootDir)) {
|
|
1128
|
-
process.stderr.write(
|
|
2503
|
+
process.stderr.write(chalk9.red(`Path does not exist: ${rootDir}
|
|
1129
2504
|
`));
|
|
1130
2505
|
process.exit(EXIT_USAGE_ERROR);
|
|
1131
2506
|
}
|
|
@@ -1133,47 +2508,47 @@ var extractCommand = new Command5("extract").description("Analyze source code an
|
|
|
1133
2508
|
try {
|
|
1134
2509
|
stat3 = await fs2.stat(rootDir);
|
|
1135
2510
|
} catch {
|
|
1136
|
-
process.stderr.write(
|
|
2511
|
+
process.stderr.write(chalk9.red(`Cannot read path: ${rootDir}
|
|
1137
2512
|
`));
|
|
1138
2513
|
process.exit(EXIT_USAGE_ERROR);
|
|
1139
2514
|
}
|
|
1140
2515
|
if (!stat3.isDirectory()) {
|
|
1141
|
-
process.stderr.write(
|
|
2516
|
+
process.stderr.write(chalk9.red(`Path must be a directory: ${rootDir}
|
|
1142
2517
|
`));
|
|
1143
2518
|
process.exit(EXIT_USAGE_ERROR);
|
|
1144
2519
|
}
|
|
1145
2520
|
const dsn = opts.dsn || process.env.VIBGRATE_DSN;
|
|
1146
2521
|
if (opts.push && !dsn) {
|
|
1147
|
-
process.stderr.write(
|
|
2522
|
+
process.stderr.write(chalk9.red("--push requires --dsn or VIBGRATE_DSN environment variable\n"));
|
|
1148
2523
|
process.exit(EXIT_USAGE_ERROR);
|
|
1149
2524
|
}
|
|
1150
|
-
if (dsn && !
|
|
1151
|
-
process.stderr.write(
|
|
1152
|
-
process.stderr.write(
|
|
2525
|
+
if (dsn && !parseDsn2(dsn)) {
|
|
2526
|
+
process.stderr.write(chalk9.red("Invalid DSN format.\n"));
|
|
2527
|
+
process.stderr.write(chalk9.dim("Expected: vibgrate+https://<key_id>:<secret>@<host>/<workspace_id>\n"));
|
|
1153
2528
|
process.exit(EXIT_USAGE_ERROR);
|
|
1154
2529
|
}
|
|
1155
2530
|
const concurrency = opts.concurrency ? parseInt(opts.concurrency, 10) : os2.cpus().length;
|
|
1156
2531
|
if (isNaN(concurrency) || concurrency < 1) {
|
|
1157
|
-
process.stderr.write(
|
|
2532
|
+
process.stderr.write(chalk9.red("--concurrency must be >= 1\n"));
|
|
1158
2533
|
process.exit(EXIT_USAGE_ERROR);
|
|
1159
2534
|
}
|
|
1160
2535
|
const timeoutMins = parseInt(opts.timeoutMins, 10);
|
|
1161
2536
|
if (isNaN(timeoutMins) || timeoutMins < 1) {
|
|
1162
|
-
process.stderr.write(
|
|
2537
|
+
process.stderr.write(chalk9.red("--timeout-mins must be >= 1\n"));
|
|
1163
2538
|
process.exit(EXIT_USAGE_ERROR);
|
|
1164
2539
|
}
|
|
1165
2540
|
const timeoutMs = timeoutMins * 60 * 1e3;
|
|
1166
2541
|
if (opts.out) {
|
|
1167
|
-
const outDir =
|
|
2542
|
+
const outDir = path9.dirname(path9.resolve(opts.out));
|
|
1168
2543
|
if (!await pathExists(outDir)) {
|
|
1169
|
-
process.stderr.write(
|
|
2544
|
+
process.stderr.write(chalk9.red(`Output directory does not exist: ${outDir}
|
|
1170
2545
|
`));
|
|
1171
2546
|
process.exit(EXIT_USAGE_ERROR);
|
|
1172
2547
|
}
|
|
1173
2548
|
try {
|
|
1174
|
-
const outStat = await fs2.stat(
|
|
2549
|
+
const outStat = await fs2.stat(path9.resolve(opts.out));
|
|
1175
2550
|
if (outStat.isDirectory()) {
|
|
1176
|
-
process.stderr.write(
|
|
2551
|
+
process.stderr.write(chalk9.red(`--out cannot be a directory: ${opts.out}
|
|
1177
2552
|
`));
|
|
1178
2553
|
process.exit(EXIT_USAGE_ERROR);
|
|
1179
2554
|
}
|
|
@@ -1182,16 +2557,16 @@ var extractCommand = new Command5("extract").description("Analyze source code an
|
|
|
1182
2557
|
}
|
|
1183
2558
|
let feedbackLines;
|
|
1184
2559
|
if (opts.feedback) {
|
|
1185
|
-
const feedbackPath =
|
|
2560
|
+
const feedbackPath = path9.resolve(opts.feedback);
|
|
1186
2561
|
if (!await pathExists(feedbackPath)) {
|
|
1187
|
-
process.stderr.write(
|
|
2562
|
+
process.stderr.write(chalk9.red(`Feedback file not found: ${feedbackPath}
|
|
1188
2563
|
`));
|
|
1189
2564
|
process.exit(EXIT_USAGE_ERROR);
|
|
1190
2565
|
}
|
|
1191
2566
|
try {
|
|
1192
2567
|
feedbackLines = await loadFeedback(feedbackPath);
|
|
1193
2568
|
} catch (err) {
|
|
1194
|
-
process.stderr.write(
|
|
2569
|
+
process.stderr.write(chalk9.red(`Invalid feedback file: ${err instanceof Error ? err.message : String(err)}
|
|
1195
2570
|
`));
|
|
1196
2571
|
process.exit(EXIT_USAGE_ERROR);
|
|
1197
2572
|
}
|
|
@@ -1201,9 +2576,9 @@ var extractCommand = new Command5("extract").description("Analyze source code an
|
|
|
1201
2576
|
targetLanguages = opts.language.split(",").map((l) => normalizeLanguageToken(l)).filter(Boolean);
|
|
1202
2577
|
for (const lang of targetLanguages) {
|
|
1203
2578
|
if (!SUPPORTED_LANGUAGES.has(lang)) {
|
|
1204
|
-
process.stderr.write(
|
|
2579
|
+
process.stderr.write(chalk9.red(`Unknown language: "${lang}"
|
|
1205
2580
|
`));
|
|
1206
|
-
process.stderr.write(
|
|
2581
|
+
process.stderr.write(chalk9.dim(`Supported: ${[...SUPPORTED_LANGUAGES].sort().join(", ")}
|
|
1207
2582
|
`));
|
|
1208
2583
|
process.exit(EXIT_USAGE_ERROR);
|
|
1209
2584
|
}
|
|
@@ -1211,14 +2586,14 @@ var extractCommand = new Command5("extract").description("Analyze source code an
|
|
|
1211
2586
|
} else {
|
|
1212
2587
|
const detected = await detectLanguages(rootDir, opts.includeTests ?? false);
|
|
1213
2588
|
if (detected.length === 0) {
|
|
1214
|
-
process.stderr.write(
|
|
2589
|
+
process.stderr.write(chalk9.yellow("No supported source files detected.\n"));
|
|
1215
2590
|
process.exit(EXIT_SUCCESS);
|
|
1216
2591
|
}
|
|
1217
2592
|
targetLanguages = detected.map((d) => d.language);
|
|
1218
2593
|
if (opts.verbose) {
|
|
1219
|
-
process.stderr.write(
|
|
2594
|
+
process.stderr.write(chalk9.dim("Detected languages:\n"));
|
|
1220
2595
|
for (const d of detected) {
|
|
1221
|
-
process.stderr.write(
|
|
2596
|
+
process.stderr.write(chalk9.dim(` ${d.language}: ${d.fileCount} files
|
|
1222
2597
|
`));
|
|
1223
2598
|
}
|
|
1224
2599
|
}
|
|
@@ -1226,20 +2601,20 @@ var extractCommand = new Command5("extract").description("Analyze source code an
|
|
|
1226
2601
|
const runnableLanguages = targetLanguages.filter((l) => ALL_WORKER_LANGS.has(l));
|
|
1227
2602
|
const unknownWorkerLangs = targetLanguages.filter((l) => !ALL_WORKER_LANGS.has(l));
|
|
1228
2603
|
if (unknownWorkerLangs.length > 0 && opts.verbose) {
|
|
1229
|
-
process.stderr.write(
|
|
2604
|
+
process.stderr.write(chalk9.dim(
|
|
1230
2605
|
`No worker registered for: ${unknownWorkerLangs.join(", ")} \u2014 skipping.
|
|
1231
2606
|
`
|
|
1232
2607
|
));
|
|
1233
2608
|
}
|
|
1234
2609
|
if (runnableLanguages.length === 0) {
|
|
1235
|
-
process.stderr.write(
|
|
2610
|
+
process.stderr.write(chalk9.yellow("No languages with available HCS workers found.\n"));
|
|
1236
2611
|
process.exit(EXIT_SUCCESS);
|
|
1237
2612
|
}
|
|
1238
2613
|
const startTime = Date.now();
|
|
1239
2614
|
const globalDeadline = startTime + timeoutMs;
|
|
1240
2615
|
process.stderr.write(
|
|
1241
|
-
|
|
1242
|
-
`) +
|
|
2616
|
+
chalk9.bold(`Extracting HCS facts from ${rootDir}
|
|
2617
|
+
`) + chalk9.dim(`Languages: ${runnableLanguages.join(", ")} Concurrency: ${concurrency} Timeout: ${timeoutMins}m
|
|
1243
2618
|
`)
|
|
1244
2619
|
);
|
|
1245
2620
|
const progress = new ProgressTracker(runnableLanguages);
|
|
@@ -1279,7 +2654,7 @@ var extractCommand = new Command5("extract").description("Analyze source code an
|
|
|
1279
2654
|
hasSchemaFailure = true;
|
|
1280
2655
|
allErrors.push(`[${language}] Schema validation: ${validation.error}`);
|
|
1281
2656
|
if (opts.verbose) {
|
|
1282
|
-
process.stderr.write(
|
|
2657
|
+
process.stderr.write(chalk9.red(`[${language}] Invalid fact: ${validation.error}
|
|
1283
2658
|
`));
|
|
1284
2659
|
}
|
|
1285
2660
|
}
|
|
@@ -1307,9 +2682,9 @@ var extractCommand = new Command5("extract").description("Analyze source code an
|
|
|
1307
2682
|
const allLines = [...allPreamble, ...allFacts];
|
|
1308
2683
|
const ndjsonOutput = allLines.join("\n") + (allLines.length > 0 ? "\n" : "");
|
|
1309
2684
|
if (opts.out) {
|
|
1310
|
-
const outPath =
|
|
2685
|
+
const outPath = path9.resolve(opts.out);
|
|
1311
2686
|
await fs2.writeFile(outPath, ndjsonOutput, "utf-8");
|
|
1312
|
-
process.stderr.write(
|
|
2687
|
+
process.stderr.write(chalk9.green(`\u2714 Wrote ${allFacts.length} facts to ${outPath}
|
|
1313
2688
|
`));
|
|
1314
2689
|
} else {
|
|
1315
2690
|
process.stdout.write(ndjsonOutput);
|
|
@@ -1321,48 +2696,48 @@ var extractCommand = new Command5("extract").description("Analyze source code an
|
|
|
1321
2696
|
}
|
|
1322
2697
|
}
|
|
1323
2698
|
if (opts.verbose) {
|
|
1324
|
-
process.stderr.write(
|
|
1325
|
-
process.stderr.write(
|
|
2699
|
+
process.stderr.write(chalk9.dim("\n\u2500\u2500 Summary \u2500\u2500\n"));
|
|
2700
|
+
process.stderr.write(chalk9.dim(` Facts emitted : ${allFacts.length}
|
|
1326
2701
|
`));
|
|
1327
|
-
process.stderr.write(
|
|
2702
|
+
process.stderr.write(chalk9.dim(` Languages : ${runnableLanguages.join(", ")}
|
|
1328
2703
|
`));
|
|
1329
|
-
process.stderr.write(
|
|
2704
|
+
process.stderr.write(chalk9.dim(` Elapsed : ${elapsed}s
|
|
1330
2705
|
`));
|
|
1331
2706
|
if (allErrors.length > 0) {
|
|
1332
|
-
process.stderr.write(
|
|
2707
|
+
process.stderr.write(chalk9.dim(` Errors : ${allErrors.length}
|
|
1333
2708
|
`));
|
|
1334
2709
|
for (const err of allErrors.slice(0, 10)) {
|
|
1335
|
-
process.stderr.write(
|
|
2710
|
+
process.stderr.write(chalk9.dim(` ${err}
|
|
1336
2711
|
`));
|
|
1337
2712
|
}
|
|
1338
2713
|
if (allErrors.length > 10) {
|
|
1339
|
-
process.stderr.write(
|
|
2714
|
+
process.stderr.write(chalk9.dim(` ... and ${allErrors.length - 10} more
|
|
1340
2715
|
`));
|
|
1341
2716
|
}
|
|
1342
2717
|
}
|
|
1343
|
-
process.stderr.write(
|
|
2718
|
+
process.stderr.write(chalk9.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n"));
|
|
1344
2719
|
}
|
|
1345
2720
|
if (hasTimeout) {
|
|
1346
|
-
process.stderr.write(
|
|
2721
|
+
process.stderr.write(chalk9.red(`Timeout exceeded (${timeoutMins} minutes)
|
|
1347
2722
|
`));
|
|
1348
2723
|
process.exit(EXIT_TIMEOUT);
|
|
1349
2724
|
}
|
|
1350
2725
|
if (hasSchemaFailure) {
|
|
1351
|
-
process.stderr.write(
|
|
2726
|
+
process.stderr.write(chalk9.red("Fact schema validation failures detected\n"));
|
|
1352
2727
|
process.exit(EXIT_SCHEMA_FAILURE);
|
|
1353
2728
|
}
|
|
1354
2729
|
if (hasParseFailure) {
|
|
1355
|
-
process.stderr.write(
|
|
2730
|
+
process.stderr.write(chalk9.red("Parsing failures detected\n"));
|
|
1356
2731
|
process.exit(EXIT_PARSE_FAILURE);
|
|
1357
2732
|
}
|
|
1358
2733
|
});
|
|
1359
2734
|
|
|
1360
2735
|
// src/commands/diff.ts
|
|
1361
|
-
import * as
|
|
2736
|
+
import * as path10 from "path";
|
|
1362
2737
|
import * as fs3 from "fs/promises";
|
|
1363
|
-
import * as
|
|
1364
|
-
import { Command as
|
|
1365
|
-
import
|
|
2738
|
+
import * as crypto3 from "crypto";
|
|
2739
|
+
import { Command as Command9 } from "commander";
|
|
2740
|
+
import chalk10 from "chalk";
|
|
1366
2741
|
var EXIT_INVALID_PATH = 2;
|
|
1367
2742
|
var EXIT_USAGE_ERROR2 = 5;
|
|
1368
2743
|
var VALID_FORMATS = /* @__PURE__ */ new Set(["ndjson", "json", "patch"]);
|
|
@@ -1384,8 +2759,8 @@ async function discoverFiles(root, includeGlobs, excludeGlobs) {
|
|
|
1384
2759
|
}
|
|
1385
2760
|
for (const entry of entries) {
|
|
1386
2761
|
if (entry.name.startsWith(".") && SKIP_DIRS2.has(entry.name)) continue;
|
|
1387
|
-
const absPath =
|
|
1388
|
-
const relPath =
|
|
2762
|
+
const absPath = path10.join(dir, entry.name);
|
|
2763
|
+
const relPath = path10.join(relBase, entry.name);
|
|
1389
2764
|
if (entry.isDirectory()) {
|
|
1390
2765
|
if (!SKIP_DIRS2.has(entry.name)) {
|
|
1391
2766
|
await walk(absPath, relPath);
|
|
@@ -1556,7 +2931,7 @@ function lcsRatio(a, b) {
|
|
|
1556
2931
|
return 2 * lcsLen / (n + m);
|
|
1557
2932
|
}
|
|
1558
2933
|
function generateDeltaId(kind, filePath) {
|
|
1559
|
-
const hash =
|
|
2934
|
+
const hash = crypto3.createHash("sha256").update(`${kind}:${filePath}`).digest("hex").substring(0, 16);
|
|
1560
2935
|
return `hcs:delta:${hash}`;
|
|
1561
2936
|
}
|
|
1562
2937
|
function formatNdjson(deltas) {
|
|
@@ -1568,8 +2943,8 @@ function formatJson(deltas) {
|
|
|
1568
2943
|
function formatPatch(deltas, originalPath, generatedPath) {
|
|
1569
2944
|
const lines = [];
|
|
1570
2945
|
for (const delta of deltas) {
|
|
1571
|
-
const aFile = delta.originalPath ?
|
|
1572
|
-
const bFile = delta.generatedPath ?
|
|
2946
|
+
const aFile = delta.originalPath ? path10.join("a", delta.originalPath) : "/dev/null";
|
|
2947
|
+
const bFile = delta.generatedPath ? path10.join("b", delta.generatedPath) : "/dev/null";
|
|
1573
2948
|
if (delta.changeKind === "renamed" && delta.originalPath && delta.generatedPath) {
|
|
1574
2949
|
lines.push(`diff --vibgrate ${aFile} ${bFile}`);
|
|
1575
2950
|
lines.push(`similarity index ${Math.round((delta.similarity ?? 0) * 100)}%`);
|
|
@@ -1590,12 +2965,12 @@ function formatPatch(deltas, originalPath, generatedPath) {
|
|
|
1590
2965
|
}
|
|
1591
2966
|
return lines.join("\n");
|
|
1592
2967
|
}
|
|
1593
|
-
var diffCommand = new
|
|
1594
|
-
const origDir =
|
|
1595
|
-
const genDir =
|
|
2968
|
+
var diffCommand = new Command9("diff").description("Compare two directory trees and emit structured delta facts").argument("<original_path>", "Original source directory").argument("<generated_path>", "Generated source directory").option("-o, --out <file>", "Output file path", "vibgrate.diff.ndjson").option("--format <fmt>", "Output format (ndjson|json|patch)", "ndjson").option("--ignore-whitespace", "Ignore whitespace-only changes").option("--context <n>", "Context lines for patch output", "3").option("--include <globs>", "Only include matching files (comma-separated)").option("--exclude <globs>", "Exclude matching files (comma-separated)").option("--stats", "Include insertions/deletions summary per file").option("--verbose", "Print file match decisions and rename detection").action(async (originalPath, generatedPath, opts) => {
|
|
2969
|
+
const origDir = path10.resolve(originalPath);
|
|
2970
|
+
const genDir = path10.resolve(generatedPath);
|
|
1596
2971
|
for (const [label, dir] of [["original_path", origDir], ["generated_path", genDir]]) {
|
|
1597
2972
|
if (!await pathExists(dir)) {
|
|
1598
|
-
process.stderr.write(
|
|
2973
|
+
process.stderr.write(chalk10.red(`${label} does not exist: ${dir}
|
|
1599
2974
|
`));
|
|
1600
2975
|
process.exit(EXIT_INVALID_PATH);
|
|
1601
2976
|
}
|
|
@@ -1603,25 +2978,25 @@ var diffCommand = new Command6("diff").description("Compare two directory trees
|
|
|
1603
2978
|
try {
|
|
1604
2979
|
stat3 = await fs3.stat(dir);
|
|
1605
2980
|
} catch {
|
|
1606
|
-
process.stderr.write(
|
|
2981
|
+
process.stderr.write(chalk10.red(`Cannot read ${label}: ${dir}
|
|
1607
2982
|
`));
|
|
1608
2983
|
process.exit(EXIT_INVALID_PATH);
|
|
1609
2984
|
}
|
|
1610
2985
|
if (!stat3.isDirectory()) {
|
|
1611
|
-
process.stderr.write(
|
|
2986
|
+
process.stderr.write(chalk10.red(`${label} must be a directory: ${dir}
|
|
1612
2987
|
`));
|
|
1613
2988
|
process.exit(EXIT_INVALID_PATH);
|
|
1614
2989
|
}
|
|
1615
2990
|
}
|
|
1616
2991
|
const format = opts.format.toLowerCase();
|
|
1617
2992
|
if (!VALID_FORMATS.has(format)) {
|
|
1618
|
-
process.stderr.write(
|
|
2993
|
+
process.stderr.write(chalk10.red(`Invalid format: "${opts.format}". Allowed: ndjson, json, patch
|
|
1619
2994
|
`));
|
|
1620
2995
|
process.exit(EXIT_USAGE_ERROR2);
|
|
1621
2996
|
}
|
|
1622
2997
|
const contextLines = parseInt(opts.context, 10);
|
|
1623
2998
|
if (isNaN(contextLines) || contextLines < 0) {
|
|
1624
|
-
process.stderr.write(
|
|
2999
|
+
process.stderr.write(chalk10.red("--context must be >= 0\n"));
|
|
1625
3000
|
process.exit(EXIT_USAGE_ERROR2);
|
|
1626
3001
|
}
|
|
1627
3002
|
const includeGlobs = opts.include ? opts.include.split(",").map((g) => g.trim()).filter(Boolean) : void 0;
|
|
@@ -1633,9 +3008,9 @@ var diffCommand = new Command6("diff").description("Compare two directory trees
|
|
|
1633
3008
|
const origMap = new Map(origFiles.map((f) => [f.relPath, f]));
|
|
1634
3009
|
const genMap = new Map(genFiles.map((f) => [f.relPath, f]));
|
|
1635
3010
|
if (opts.verbose) {
|
|
1636
|
-
process.stderr.write(
|
|
3011
|
+
process.stderr.write(chalk10.dim(`Original: ${origFiles.length} files
|
|
1637
3012
|
`));
|
|
1638
|
-
process.stderr.write(
|
|
3013
|
+
process.stderr.write(chalk10.dim(`Generated: ${genFiles.length} files
|
|
1639
3014
|
`));
|
|
1640
3015
|
}
|
|
1641
3016
|
const deltas = [];
|
|
@@ -1669,7 +3044,7 @@ var diffCommand = new Command6("diff").description("Compare two directory trees
|
|
|
1669
3044
|
}
|
|
1670
3045
|
deltas.push(delta);
|
|
1671
3046
|
if (opts.verbose) {
|
|
1672
|
-
process.stderr.write(
|
|
3047
|
+
process.stderr.write(chalk10.dim(` modified: ${relPath} (+${diffResult.insertions} -${diffResult.deletions})
|
|
1673
3048
|
`));
|
|
1674
3049
|
}
|
|
1675
3050
|
}
|
|
@@ -1700,7 +3075,7 @@ var diffCommand = new Command6("diff").description("Compare two directory trees
|
|
|
1700
3075
|
const pairs = [];
|
|
1701
3076
|
for (const [origPath, origContent] of origContents) {
|
|
1702
3077
|
for (const [genPath, genContent] of genContents) {
|
|
1703
|
-
if (
|
|
3078
|
+
if (path10.extname(origPath) !== path10.extname(genPath)) continue;
|
|
1704
3079
|
const similarity = computeSimilarity(origContent, genContent);
|
|
1705
3080
|
if (similarity >= RENAME_THRESHOLD) {
|
|
1706
3081
|
pairs.push({ origPath, genPath, similarity });
|
|
@@ -1733,7 +3108,7 @@ var diffCommand = new Command6("diff").description("Compare two directory trees
|
|
|
1733
3108
|
}
|
|
1734
3109
|
deltas.push(delta);
|
|
1735
3110
|
if (opts.verbose) {
|
|
1736
|
-
process.stderr.write(
|
|
3111
|
+
process.stderr.write(chalk10.dim(
|
|
1737
3112
|
` renamed: ${pair.origPath} \u2192 ${pair.genPath} (${(pair.similarity * 100).toFixed(0)}% similar)
|
|
1738
3113
|
`
|
|
1739
3114
|
));
|
|
@@ -1758,7 +3133,7 @@ var diffCommand = new Command6("diff").description("Compare two directory trees
|
|
|
1758
3133
|
}
|
|
1759
3134
|
deltas.push(delta);
|
|
1760
3135
|
if (opts.verbose) {
|
|
1761
|
-
process.stderr.write(
|
|
3136
|
+
process.stderr.write(chalk10.dim(` removed: ${file.relPath}
|
|
1762
3137
|
`));
|
|
1763
3138
|
}
|
|
1764
3139
|
}
|
|
@@ -1780,7 +3155,7 @@ var diffCommand = new Command6("diff").description("Compare two directory trees
|
|
|
1780
3155
|
}
|
|
1781
3156
|
deltas.push(delta);
|
|
1782
3157
|
if (opts.verbose) {
|
|
1783
|
-
process.stderr.write(
|
|
3158
|
+
process.stderr.write(chalk10.dim(` added: ${file.relPath}
|
|
1784
3159
|
`));
|
|
1785
3160
|
}
|
|
1786
3161
|
}
|
|
@@ -1801,16 +3176,16 @@ var diffCommand = new Command6("diff").description("Compare two directory trees
|
|
|
1801
3176
|
output = formatPatch(deltas, originalPath, generatedPath);
|
|
1802
3177
|
break;
|
|
1803
3178
|
}
|
|
1804
|
-
const outPath =
|
|
3179
|
+
const outPath = path10.resolve(opts.out);
|
|
1805
3180
|
await fs3.writeFile(outPath, output, "utf-8");
|
|
1806
3181
|
const added = deltas.filter((d) => d.changeKind === "added").length;
|
|
1807
3182
|
const removed = deltas.filter((d) => d.changeKind === "removed").length;
|
|
1808
3183
|
const modified = deltas.filter((d) => d.changeKind === "modified").length;
|
|
1809
3184
|
const renamed = deltas.filter((d) => d.changeKind === "renamed").length;
|
|
1810
3185
|
process.stderr.write(
|
|
1811
|
-
|
|
3186
|
+
chalk10.green(`\u2714 `) + `${deltas.length} changes: ` + chalk10.green(`+${added} added`) + ", " + chalk10.red(`-${removed} removed`) + ", " + chalk10.yellow(`~${modified} modified`) + ", " + chalk10.blue(`\u2192${renamed} renamed`) + "\n"
|
|
1812
3187
|
);
|
|
1813
|
-
process.stderr.write(
|
|
3188
|
+
process.stderr.write(chalk10.dim(` Written to ${outPath}
|
|
1814
3189
|
`));
|
|
1815
3190
|
if (opts.verbose && opts.stats) {
|
|
1816
3191
|
let totalIns = 0;
|
|
@@ -1821,193 +3196,193 @@ var diffCommand = new Command6("diff").description("Compare two directory trees
|
|
|
1821
3196
|
totalDel += d.stats.deletions;
|
|
1822
3197
|
}
|
|
1823
3198
|
}
|
|
1824
|
-
process.stderr.write(
|
|
3199
|
+
process.stderr.write(chalk10.dim(` Total: +${totalIns} insertions, -${totalDel} deletions
|
|
1825
3200
|
`));
|
|
1826
3201
|
}
|
|
1827
3202
|
});
|
|
1828
3203
|
|
|
1829
3204
|
// src/commands/help.ts
|
|
1830
|
-
import { Command as
|
|
1831
|
-
import
|
|
3205
|
+
import { Command as Command10 } from "commander";
|
|
3206
|
+
import chalk11 from "chalk";
|
|
1832
3207
|
var HELP_URL = "https://vibgrate.com/help";
|
|
1833
3208
|
function printFooter() {
|
|
1834
3209
|
console.log("");
|
|
1835
|
-
console.log(
|
|
3210
|
+
console.log(chalk11.dim(`See ${HELP_URL} for more guidance`));
|
|
1836
3211
|
}
|
|
1837
3212
|
var detailedHelp = {
|
|
1838
3213
|
scan: () => {
|
|
1839
3214
|
console.log("");
|
|
1840
|
-
console.log(
|
|
3215
|
+
console.log(chalk11.bold.underline("vibgrate scan") + chalk11.dim(" \u2014 Scan a project for upgrade drift"));
|
|
1841
3216
|
console.log("");
|
|
1842
|
-
console.log(
|
|
3217
|
+
console.log(chalk11.bold("Usage:"));
|
|
1843
3218
|
console.log(" vibgrate scan [path] [options]");
|
|
1844
3219
|
console.log("");
|
|
1845
|
-
console.log(
|
|
1846
|
-
console.log(` ${
|
|
3220
|
+
console.log(chalk11.bold("Arguments:"));
|
|
3221
|
+
console.log(` ${chalk11.cyan("[path]")} Path to scan (default: current directory)`);
|
|
1847
3222
|
console.log("");
|
|
1848
|
-
console.log(
|
|
1849
|
-
console.log(` ${
|
|
1850
|
-
console.log(` ${
|
|
3223
|
+
console.log(chalk11.bold("Output options:"));
|
|
3224
|
+
console.log(` ${chalk11.cyan("--format <format>")} Output format: ${chalk11.white("text")} | json | sarif | md (default: text)`);
|
|
3225
|
+
console.log(` ${chalk11.cyan("--out <file>")} Write output to a file instead of stdout`);
|
|
1851
3226
|
console.log("");
|
|
1852
|
-
console.log(
|
|
1853
|
-
console.log(` ${
|
|
1854
|
-
console.log(` ${
|
|
1855
|
-
console.log(` ${
|
|
1856
|
-
console.log(` ${
|
|
3227
|
+
console.log(chalk11.bold("Baseline & gating:"));
|
|
3228
|
+
console.log(` ${chalk11.cyan("--baseline <file>")} Compare results against a saved baseline`);
|
|
3229
|
+
console.log(` ${chalk11.cyan("--drift-budget <score>")} Fail if drift score exceeds this value (0\u2013100)`);
|
|
3230
|
+
console.log(` ${chalk11.cyan("--drift-worsening <percent>")} Fail if drift worsens by more than % since baseline`);
|
|
3231
|
+
console.log(` ${chalk11.cyan("--fail-on <level>")} Fail exit code on warn or error findings`);
|
|
1857
3232
|
console.log("");
|
|
1858
|
-
console.log(
|
|
1859
|
-
console.log(` ${
|
|
1860
|
-
console.log(` ${
|
|
3233
|
+
console.log(chalk11.bold("Performance:"));
|
|
3234
|
+
console.log(` ${chalk11.cyan("--concurrency <n>")} Max concurrent registry calls (default: 8)`);
|
|
3235
|
+
console.log(` ${chalk11.cyan("--changed-only")} Only scan files changed since last git commit`);
|
|
1861
3236
|
console.log("");
|
|
1862
|
-
console.log(
|
|
1863
|
-
console.log(` ${
|
|
1864
|
-
console.log(` ${
|
|
1865
|
-
console.log(` ${
|
|
1866
|
-
console.log(` ${
|
|
3237
|
+
console.log(chalk11.bold("Privacy & offline:"));
|
|
3238
|
+
console.log(` ${chalk11.cyan("--offline")} Run without any network calls; skip result upload`);
|
|
3239
|
+
console.log(` ${chalk11.cyan("--package-manifest <file>")} Use a local package-version manifest (JSON or ZIP) for offline mode`);
|
|
3240
|
+
console.log(` ${chalk11.cyan("--no-local-artifacts")} Do not write .vibgrate JSON artifacts to disk`);
|
|
3241
|
+
console.log(` ${chalk11.cyan("--max-privacy")} Strongest privacy mode: minimal scanners + no local artifacts`);
|
|
1867
3242
|
console.log("");
|
|
1868
|
-
console.log(
|
|
1869
|
-
console.log(` ${
|
|
1870
|
-
console.log(` ${
|
|
3243
|
+
console.log(chalk11.bold("Tooling:"));
|
|
3244
|
+
console.log(` ${chalk11.cyan("--install-tools")} Auto-install missing security scanners via Homebrew`);
|
|
3245
|
+
console.log(` ${chalk11.cyan("--ui-purpose")} Enable UI purpose evidence extraction (slower)`);
|
|
1871
3246
|
console.log("");
|
|
1872
|
-
console.log(
|
|
1873
|
-
console.log(` ${
|
|
1874
|
-
console.log(` ${
|
|
1875
|
-
console.log(` ${
|
|
1876
|
-
console.log(` ${
|
|
3247
|
+
console.log(chalk11.bold("Uploading results:"));
|
|
3248
|
+
console.log(` ${chalk11.cyan("--push")} Auto-push results to Vibgrate API after scan`);
|
|
3249
|
+
console.log(` ${chalk11.cyan("--dsn <dsn>")} DSN token for push (or set VIBGRATE_DSN env var)`);
|
|
3250
|
+
console.log(` ${chalk11.cyan("--region <region>")} Data residency region: us | eu (default: us)`);
|
|
3251
|
+
console.log(` ${chalk11.cyan("--strict")} Fail if the upload to Vibgrate API fails`);
|
|
1877
3252
|
console.log("");
|
|
1878
|
-
console.log(
|
|
1879
|
-
console.log(` ${
|
|
3253
|
+
console.log(chalk11.bold("Examples:"));
|
|
3254
|
+
console.log(` ${chalk11.dim("# Scan the current directory and display a text report")}`);
|
|
1880
3255
|
console.log(" vibgrate scan .");
|
|
1881
3256
|
console.log("");
|
|
1882
|
-
console.log(` ${
|
|
3257
|
+
console.log(` ${chalk11.dim("# Scan, fail if drift score > 40, and write SARIF for GitHub Actions")}`);
|
|
1883
3258
|
console.log(" vibgrate scan . --drift-budget 40 --format sarif --out drift.sarif");
|
|
1884
3259
|
console.log("");
|
|
1885
|
-
console.log(` ${
|
|
3260
|
+
console.log(` ${chalk11.dim("# Scan and automatically upload results via a DSN")}`);
|
|
1886
3261
|
console.log(" vibgrate scan . --push --dsn $VIBGRATE_DSN");
|
|
1887
3262
|
console.log("");
|
|
1888
|
-
console.log(` ${
|
|
3263
|
+
console.log(` ${chalk11.dim("# Offline scan using a pre-downloaded package manifest")}`);
|
|
1889
3264
|
console.log(" vibgrate scan . --offline --package-manifest ./manifest.zip");
|
|
1890
3265
|
},
|
|
1891
3266
|
init: () => {
|
|
1892
3267
|
console.log("");
|
|
1893
|
-
console.log(
|
|
3268
|
+
console.log(chalk11.bold.underline("vibgrate init") + chalk11.dim(" \u2014 Initialise vibgrate in a project directory"));
|
|
1894
3269
|
console.log("");
|
|
1895
|
-
console.log(
|
|
3270
|
+
console.log(chalk11.bold("Usage:"));
|
|
1896
3271
|
console.log(" vibgrate init [path] [options]");
|
|
1897
3272
|
console.log("");
|
|
1898
|
-
console.log(
|
|
1899
|
-
console.log(` ${
|
|
3273
|
+
console.log(chalk11.bold("Arguments:"));
|
|
3274
|
+
console.log(` ${chalk11.cyan("[path]")} Directory to initialise (default: current directory)`);
|
|
1900
3275
|
console.log("");
|
|
1901
|
-
console.log(
|
|
1902
|
-
console.log(` ${
|
|
1903
|
-
console.log(` ${
|
|
3276
|
+
console.log(chalk11.bold("Options:"));
|
|
3277
|
+
console.log(` ${chalk11.cyan("--baseline")} Create an initial drift baseline after init`);
|
|
3278
|
+
console.log(` ${chalk11.cyan("--yes")} Skip all confirmation prompts`);
|
|
1904
3279
|
console.log("");
|
|
1905
|
-
console.log(
|
|
3280
|
+
console.log(chalk11.bold("What it does:"));
|
|
1906
3281
|
console.log(" \u2022 Creates a .vibgrate/ directory");
|
|
1907
3282
|
console.log(" \u2022 Writes a vibgrate.config.ts starter config");
|
|
1908
3283
|
console.log(" \u2022 Optionally runs an initial baseline scan (--baseline)");
|
|
1909
3284
|
console.log("");
|
|
1910
|
-
console.log(
|
|
3285
|
+
console.log(chalk11.bold("Examples:"));
|
|
1911
3286
|
console.log(" vibgrate init");
|
|
1912
3287
|
console.log(" vibgrate init ./my-project --baseline");
|
|
1913
3288
|
},
|
|
1914
3289
|
baseline: () => {
|
|
1915
3290
|
console.log("");
|
|
1916
|
-
console.log(
|
|
3291
|
+
console.log(chalk11.bold.underline("vibgrate baseline") + chalk11.dim(" \u2014 Save a drift baseline snapshot"));
|
|
1917
3292
|
console.log("");
|
|
1918
|
-
console.log(
|
|
3293
|
+
console.log(chalk11.bold("Usage:"));
|
|
1919
3294
|
console.log(" vibgrate baseline [path]");
|
|
1920
3295
|
console.log("");
|
|
1921
|
-
console.log(
|
|
1922
|
-
console.log(` ${
|
|
3296
|
+
console.log(chalk11.bold("Arguments:"));
|
|
3297
|
+
console.log(` ${chalk11.cyan("[path]")} Path to baseline (default: current directory)`);
|
|
1923
3298
|
console.log("");
|
|
1924
|
-
console.log(
|
|
3299
|
+
console.log(chalk11.bold("What it does:"));
|
|
1925
3300
|
console.log(" Runs a full scan and saves the result as .vibgrate/baseline.json.");
|
|
1926
3301
|
console.log(" Future scans can compare against this file using --baseline.");
|
|
1927
3302
|
console.log("");
|
|
1928
|
-
console.log(
|
|
3303
|
+
console.log(chalk11.bold("Examples:"));
|
|
1929
3304
|
console.log(" vibgrate baseline .");
|
|
1930
3305
|
console.log(" vibgrate scan . --baseline .vibgrate/baseline.json --drift-worsening 10");
|
|
1931
3306
|
},
|
|
1932
3307
|
report: () => {
|
|
1933
3308
|
console.log("");
|
|
1934
|
-
console.log(
|
|
3309
|
+
console.log(chalk11.bold.underline("vibgrate report") + chalk11.dim(" \u2014 Generate a report from a saved scan artifact"));
|
|
1935
3310
|
console.log("");
|
|
1936
|
-
console.log(
|
|
3311
|
+
console.log(chalk11.bold("Usage:"));
|
|
1937
3312
|
console.log(" vibgrate report [options]");
|
|
1938
3313
|
console.log("");
|
|
1939
|
-
console.log(
|
|
1940
|
-
console.log(` ${
|
|
1941
|
-
console.log(` ${
|
|
3314
|
+
console.log(chalk11.bold("Options:"));
|
|
3315
|
+
console.log(` ${chalk11.cyan("--in <file>")} Input artifact file (default: .vibgrate/scan_result.json)`);
|
|
3316
|
+
console.log(` ${chalk11.cyan("--format <format>")} Output format: ${chalk11.white("text")} | md | json (default: text)`);
|
|
1942
3317
|
console.log("");
|
|
1943
|
-
console.log(
|
|
3318
|
+
console.log(chalk11.bold("Examples:"));
|
|
1944
3319
|
console.log(" vibgrate report");
|
|
1945
3320
|
console.log(" vibgrate report --format md > DRIFT-REPORT.md");
|
|
1946
3321
|
console.log(" vibgrate report --in ./ci/scan_result.json --format json");
|
|
1947
3322
|
},
|
|
1948
3323
|
sbom: () => {
|
|
1949
3324
|
console.log("");
|
|
1950
|
-
console.log(
|
|
3325
|
+
console.log(chalk11.bold.underline("vibgrate sbom") + chalk11.dim(" \u2014 Export a Software Bill of Materials from a scan artifact"));
|
|
1951
3326
|
console.log("");
|
|
1952
|
-
console.log(
|
|
3327
|
+
console.log(chalk11.bold("Usage:"));
|
|
1953
3328
|
console.log(" vibgrate sbom [options]");
|
|
1954
3329
|
console.log("");
|
|
1955
|
-
console.log(
|
|
1956
|
-
console.log(` ${
|
|
1957
|
-
console.log(` ${
|
|
1958
|
-
console.log(` ${
|
|
3330
|
+
console.log(chalk11.bold("Options:"));
|
|
3331
|
+
console.log(` ${chalk11.cyan("--in <file>")} Input artifact (default: .vibgrate/scan_result.json)`);
|
|
3332
|
+
console.log(` ${chalk11.cyan("--format <format>")} SBOM format: ${chalk11.white("cyclonedx")} | spdx (default: cyclonedx)`);
|
|
3333
|
+
console.log(` ${chalk11.cyan("--out <file>")} Write SBOM to file instead of stdout`);
|
|
1959
3334
|
console.log("");
|
|
1960
|
-
console.log(
|
|
3335
|
+
console.log(chalk11.bold("Examples:"));
|
|
1961
3336
|
console.log(" vibgrate sbom --format cyclonedx --out sbom.json");
|
|
1962
3337
|
console.log(" vibgrate sbom --format spdx --out sbom.spdx.json");
|
|
1963
3338
|
},
|
|
1964
3339
|
push: () => {
|
|
1965
3340
|
console.log("");
|
|
1966
|
-
console.log(
|
|
3341
|
+
console.log(chalk11.bold.underline("vibgrate push") + chalk11.dim(" \u2014 Upload a scan artifact to the Vibgrate API"));
|
|
1967
3342
|
console.log("");
|
|
1968
|
-
console.log(
|
|
3343
|
+
console.log(chalk11.bold("Usage:"));
|
|
1969
3344
|
console.log(" vibgrate push [options]");
|
|
1970
3345
|
console.log("");
|
|
1971
|
-
console.log(
|
|
1972
|
-
console.log(` ${
|
|
1973
|
-
console.log(` ${
|
|
1974
|
-
console.log(` ${
|
|
1975
|
-
console.log(` ${
|
|
3346
|
+
console.log(chalk11.bold("Options:"));
|
|
3347
|
+
console.log(` ${chalk11.cyan("--dsn <dsn>")} DSN token (or set VIBGRATE_DSN env var)`);
|
|
3348
|
+
console.log(` ${chalk11.cyan("--file <file>")} Artifact to upload (default: .vibgrate/scan_result.json)`);
|
|
3349
|
+
console.log(` ${chalk11.cyan("--region <region>")} Override data residency region: us | eu`);
|
|
3350
|
+
console.log(` ${chalk11.cyan("--strict")} Fail with non-zero exit code on upload error`);
|
|
1976
3351
|
console.log("");
|
|
1977
|
-
console.log(
|
|
3352
|
+
console.log(chalk11.bold("Examples:"));
|
|
1978
3353
|
console.log(" vibgrate push --dsn $VIBGRATE_DSN");
|
|
1979
3354
|
console.log(" vibgrate push --file ./ci/scan_result.json --strict");
|
|
1980
3355
|
},
|
|
1981
3356
|
dsn: () => {
|
|
1982
3357
|
console.log("");
|
|
1983
|
-
console.log(
|
|
3358
|
+
console.log(chalk11.bold.underline("vibgrate dsn") + chalk11.dim(" \u2014 Manage DSN tokens for API authentication"));
|
|
1984
3359
|
console.log("");
|
|
1985
|
-
console.log(
|
|
1986
|
-
console.log(` ${
|
|
3360
|
+
console.log(chalk11.bold("Subcommands:"));
|
|
3361
|
+
console.log(` ${chalk11.cyan("vibgrate dsn create")} Generate a new DSN token`);
|
|
1987
3362
|
console.log("");
|
|
1988
|
-
console.log(
|
|
1989
|
-
console.log(` ${
|
|
1990
|
-
console.log(` ${
|
|
1991
|
-
console.log(` ${
|
|
1992
|
-
console.log(` ${
|
|
3363
|
+
console.log(chalk11.bold("dsn create options:"));
|
|
3364
|
+
console.log(` ${chalk11.cyan("--workspace <id>")} Workspace ID or "new" to auto-generate ${chalk11.red("(required)")}`);
|
|
3365
|
+
console.log(` ${chalk11.cyan("--region <region>")} Data residency region: us | eu (default: us)`);
|
|
3366
|
+
console.log(` ${chalk11.cyan("--ingest <url>")} Override ingest API URL`);
|
|
3367
|
+
console.log(` ${chalk11.cyan("--write <path>")} Write the DSN to a file (add to .gitignore!)`);
|
|
1993
3368
|
console.log("");
|
|
1994
|
-
console.log(
|
|
3369
|
+
console.log(chalk11.bold("Examples:"));
|
|
1995
3370
|
console.log(" vibgrate dsn create --workspace new");
|
|
1996
3371
|
console.log(" vibgrate dsn create --workspace abc123");
|
|
1997
3372
|
console.log(" vibgrate dsn create --workspace new --region eu --write .vibgrate/.dsn");
|
|
1998
3373
|
},
|
|
1999
3374
|
update: () => {
|
|
2000
3375
|
console.log("");
|
|
2001
|
-
console.log(
|
|
3376
|
+
console.log(chalk11.bold.underline("vibgrate update") + chalk11.dim(" \u2014 Update the vibgrate CLI to the latest version"));
|
|
2002
3377
|
console.log("");
|
|
2003
|
-
console.log(
|
|
3378
|
+
console.log(chalk11.bold("Usage:"));
|
|
2004
3379
|
console.log(" vibgrate update [options]");
|
|
2005
3380
|
console.log("");
|
|
2006
|
-
console.log(
|
|
2007
|
-
console.log(` ${
|
|
2008
|
-
console.log(` ${
|
|
3381
|
+
console.log(chalk11.bold("Options:"));
|
|
3382
|
+
console.log(` ${chalk11.cyan("--check")} Check for a newer version without installing`);
|
|
3383
|
+
console.log(` ${chalk11.cyan("--pm <manager>")} Force a package manager: npm | pnpm | yarn | bun`);
|
|
2009
3384
|
console.log("");
|
|
2010
|
-
console.log(
|
|
3385
|
+
console.log(chalk11.bold("Examples:"));
|
|
2011
3386
|
console.log(" vibgrate update");
|
|
2012
3387
|
console.log(" vibgrate update --check");
|
|
2013
3388
|
console.log(" vibgrate update --pm pnpm");
|
|
@@ -2015,40 +3390,40 @@ var detailedHelp = {
|
|
|
2015
3390
|
};
|
|
2016
3391
|
function printSummaryHelp() {
|
|
2017
3392
|
console.log("");
|
|
2018
|
-
console.log(
|
|
3393
|
+
console.log(chalk11.bold("vibgrate") + chalk11.dim(" \u2014 Continuous Drift Intelligence"));
|
|
2019
3394
|
console.log("");
|
|
2020
|
-
console.log(
|
|
3395
|
+
console.log(chalk11.bold("Usage:"));
|
|
2021
3396
|
console.log(" vibgrate <command> [options]");
|
|
2022
3397
|
console.log(" vibgrate help [command] Show detailed help for a command");
|
|
2023
3398
|
console.log("");
|
|
2024
|
-
console.log(
|
|
2025
|
-
console.log(` ${
|
|
3399
|
+
console.log(chalk11.bold("Getting started:"));
|
|
3400
|
+
console.log(` ${chalk11.cyan("init")} Initialise vibgrate in a project (creates config & .vibgrate/ dir)`);
|
|
2026
3401
|
console.log("");
|
|
2027
|
-
console.log(
|
|
2028
|
-
console.log(` ${
|
|
2029
|
-
console.log(` ${
|
|
3402
|
+
console.log(chalk11.bold("Core scanning:"));
|
|
3403
|
+
console.log(` ${chalk11.cyan("scan")} Scan a project for upgrade drift and generate a report`);
|
|
3404
|
+
console.log(` ${chalk11.cyan("baseline")} Save a baseline snapshot to compare future scans against`);
|
|
2030
3405
|
console.log("");
|
|
2031
|
-
console.log(
|
|
2032
|
-
console.log(` ${
|
|
2033
|
-
console.log(` ${
|
|
3406
|
+
console.log(chalk11.bold("Reporting & export:"));
|
|
3407
|
+
console.log(` ${chalk11.cyan("report")} Re-generate a report from a previously saved scan artifact`);
|
|
3408
|
+
console.log(` ${chalk11.cyan("sbom")} Export a Software Bill of Materials (CycloneDX or SPDX)`);
|
|
2034
3409
|
console.log("");
|
|
2035
|
-
console.log(
|
|
2036
|
-
console.log(` ${
|
|
2037
|
-
console.log(` ${
|
|
3410
|
+
console.log(chalk11.bold("CI/CD integration:"));
|
|
3411
|
+
console.log(` ${chalk11.cyan("push")} Upload a scan artifact to the Vibgrate API`);
|
|
3412
|
+
console.log(` ${chalk11.cyan("dsn")} Create and manage DSN tokens for API authentication`);
|
|
2038
3413
|
console.log("");
|
|
2039
|
-
console.log(
|
|
2040
|
-
console.log(` ${
|
|
3414
|
+
console.log(chalk11.bold("Maintenance:"));
|
|
3415
|
+
console.log(` ${chalk11.cyan("update")} Update the vibgrate CLI to the latest version`);
|
|
2041
3416
|
console.log("");
|
|
2042
|
-
console.log(
|
|
3417
|
+
console.log(chalk11.dim("Run") + ` ${chalk11.cyan("vibgrate help <command>")} ` + chalk11.dim("for detailed options, e.g.") + ` ${chalk11.cyan("vibgrate help scan")}`);
|
|
2043
3418
|
}
|
|
2044
|
-
var helpCommand = new
|
|
3419
|
+
var helpCommand = new Command10("help").description("Show help for vibgrate commands").argument("[command]", "Command to show detailed help for").helpOption(false).action((cmd) => {
|
|
2045
3420
|
const name = cmd?.toLowerCase().trim();
|
|
2046
3421
|
if (name && detailedHelp[name]) {
|
|
2047
3422
|
detailedHelp[name]();
|
|
2048
3423
|
} else if (name) {
|
|
2049
3424
|
console.log("");
|
|
2050
|
-
console.log(
|
|
2051
|
-
console.log(
|
|
3425
|
+
console.log(chalk11.red(`Unknown command: ${name}`));
|
|
3426
|
+
console.log(chalk11.dim(`Available commands: ${Object.keys(detailedHelp).join(", ")}`));
|
|
2052
3427
|
printSummaryHelp();
|
|
2053
3428
|
} else {
|
|
2054
3429
|
printSummaryHelp();
|
|
@@ -2057,7 +3432,7 @@ var helpCommand = new Command7("help").description("Show help for vibgrate comma
|
|
|
2057
3432
|
});
|
|
2058
3433
|
|
|
2059
3434
|
// src/cli.ts
|
|
2060
|
-
var program = new
|
|
3435
|
+
var program = new Command11();
|
|
2061
3436
|
program.name("vibgrate").description("Continuous Drift Intelligence").version(VERSION).addHelpText("after", "\nSee https://vibgrate.com/help for more guidance");
|
|
2062
3437
|
program.addCommand(helpCommand);
|
|
2063
3438
|
program.addCommand(initCommand);
|
|
@@ -2074,8 +3449,8 @@ function notifyIfUpdateAvailable() {
|
|
|
2074
3449
|
void checkForUpdate().then((update) => {
|
|
2075
3450
|
if (!update?.updateAvailable) return;
|
|
2076
3451
|
console.error("");
|
|
2077
|
-
console.error(
|
|
2078
|
-
console.error(
|
|
3452
|
+
console.error(chalk12.yellow(` Update available: ${update.current} \u2192 ${update.latest}`));
|
|
3453
|
+
console.error(chalk12.dim(' Run "vibgrate update" to install the latest version.'));
|
|
2079
3454
|
console.error("");
|
|
2080
3455
|
}).catch(() => {
|
|
2081
3456
|
});
|