@vibgrate/cli 2026.615.4 → 2026.618.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
pathExists,
|
|
7
7
|
readJsonFile,
|
|
8
8
|
writeTextFile
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-LWB7JBWP.js";
|
|
10
10
|
import {
|
|
11
11
|
computeRepoFingerprint,
|
|
12
12
|
detectVcs,
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
resolveRepositoryName,
|
|
17
17
|
runScan,
|
|
18
18
|
writeDefaultConfig
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-BMLE722B.js";
|
|
20
20
|
import {
|
|
21
21
|
require_semver
|
|
22
22
|
} from "./chunk-5IXVOEZN.js";
|
|
@@ -29,8 +29,8 @@ import {
|
|
|
29
29
|
} from "./chunk-JSBRDJBE.js";
|
|
30
30
|
|
|
31
31
|
// src/cli.ts
|
|
32
|
-
import { Command as
|
|
33
|
-
import
|
|
32
|
+
import { Command as Command13 } from "commander";
|
|
33
|
+
import chalk15 from "chalk";
|
|
34
34
|
|
|
35
35
|
// src/commands/init.ts
|
|
36
36
|
import * as path from "path";
|
|
@@ -49,7 +49,7 @@ var initCommand = new Command("init").description("Initialize vibgrate in a proj
|
|
|
49
49
|
console.log(chalk.green("\u2714") + ` Created ${chalk.bold("vibgrate.config.ts")}`);
|
|
50
50
|
}
|
|
51
51
|
if (opts.baseline) {
|
|
52
|
-
const { runBaseline } = await import("./baseline-
|
|
52
|
+
const { runBaseline } = await import("./baseline-K2GQA6G5.js");
|
|
53
53
|
await runBaseline(rootDir);
|
|
54
54
|
}
|
|
55
55
|
console.log("");
|
|
@@ -60,9 +60,9 @@ var initCommand = new Command("init").description("Initialize vibgrate in a proj
|
|
|
60
60
|
});
|
|
61
61
|
|
|
62
62
|
// src/commands/scan.ts
|
|
63
|
-
import * as
|
|
63
|
+
import * as path4 from "path";
|
|
64
64
|
import { Command as Command3 } from "commander";
|
|
65
|
-
import
|
|
65
|
+
import chalk4 from "chalk";
|
|
66
66
|
|
|
67
67
|
// src/version.ts
|
|
68
68
|
import { createRequire } from "module";
|
|
@@ -129,6 +129,9 @@ function dashHostForIngestHost(ingestHost) {
|
|
|
129
129
|
const match = REGIONS.find((r) => r.ingestHost === ingestHost);
|
|
130
130
|
return match?.dashHost ?? "dash.vibgrate.com";
|
|
131
131
|
}
|
|
132
|
+
function ingestHostForRegionId(region) {
|
|
133
|
+
return findRegion(region.toLowerCase())?.ingestHost;
|
|
134
|
+
}
|
|
132
135
|
|
|
133
136
|
// src/commands/dsn.ts
|
|
134
137
|
async function provisionDsn(keyId, secret, workspaceId, ingestHost, region) {
|
|
@@ -210,6 +213,49 @@ dsnCommand.command("create").description("Create a new DSN token").option("--ing
|
|
|
210
213
|
}
|
|
211
214
|
});
|
|
212
215
|
|
|
216
|
+
// src/credentials.ts
|
|
217
|
+
import * as os from "os";
|
|
218
|
+
import * as path3 from "path";
|
|
219
|
+
import * as fs from "fs";
|
|
220
|
+
function credentialsDir() {
|
|
221
|
+
return path3.join(os.homedir(), ".vibgrate");
|
|
222
|
+
}
|
|
223
|
+
function credentialsPath() {
|
|
224
|
+
return path3.join(credentialsDir(), "credentials.json");
|
|
225
|
+
}
|
|
226
|
+
function readStoredCredentials() {
|
|
227
|
+
try {
|
|
228
|
+
const raw = fs.readFileSync(credentialsPath(), "utf8");
|
|
229
|
+
const parsed = JSON.parse(raw);
|
|
230
|
+
return parsed && typeof parsed.dsn === "string" && parsed.dsn ? parsed : null;
|
|
231
|
+
} catch {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function writeStoredCredentials(creds) {
|
|
236
|
+
const dir = credentialsDir();
|
|
237
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
238
|
+
const file = credentialsPath();
|
|
239
|
+
fs.writeFileSync(file, JSON.stringify(creds, null, 2) + "\n", "utf8");
|
|
240
|
+
try {
|
|
241
|
+
fs.chmodSync(file, 384);
|
|
242
|
+
} catch {
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function clearStoredCredentials() {
|
|
246
|
+
try {
|
|
247
|
+
fs.rmSync(credentialsPath());
|
|
248
|
+
return true;
|
|
249
|
+
} catch {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function resolveDsn(explicitDsn) {
|
|
254
|
+
if (explicitDsn) return explicitDsn;
|
|
255
|
+
if (process.env.VIBGRATE_DSN) return process.env.VIBGRATE_DSN;
|
|
256
|
+
return readStoredCredentials()?.dsn ?? void 0;
|
|
257
|
+
}
|
|
258
|
+
|
|
213
259
|
// src/utils/ingest-id-output.ts
|
|
214
260
|
function emitIngestIdLine(ingestId, options) {
|
|
215
261
|
console.log(`VIBGRATE_INGEST_ID=${ingestId}`);
|
|
@@ -218,18 +264,62 @@ function emitIngestIdLine(ingestId, options) {
|
|
|
218
264
|
}
|
|
219
265
|
}
|
|
220
266
|
|
|
267
|
+
// src/utils/upload.ts
|
|
268
|
+
import chalk3 from "chalk";
|
|
269
|
+
function postOnce(input, host) {
|
|
270
|
+
const url = `${input.scheme}://${host}/v1/ingest/scan`;
|
|
271
|
+
return fetch(url, {
|
|
272
|
+
method: "POST",
|
|
273
|
+
headers: {
|
|
274
|
+
"Content-Type": "application/json",
|
|
275
|
+
"Content-Encoding": input.contentEncoding,
|
|
276
|
+
"X-Vibgrate-Timestamp": input.timestamp,
|
|
277
|
+
"Authorization": `VibgrateDSN ${input.keyId}:${input.secret}`,
|
|
278
|
+
"Connection": "close"
|
|
279
|
+
// Prevent keep-alive delays on exit
|
|
280
|
+
},
|
|
281
|
+
body: input.body
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
async function regionRedirectHost(response) {
|
|
285
|
+
try {
|
|
286
|
+
const payload = await response.clone().json();
|
|
287
|
+
if (payload?.code !== "REGION_MISMATCH" || !payload.region) return void 0;
|
|
288
|
+
return ingestHostForRegionId(payload.region);
|
|
289
|
+
} catch {
|
|
290
|
+
return void 0;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async function uploadScanArtifact(input) {
|
|
294
|
+
let host = input.host;
|
|
295
|
+
let response = await postOnce(input, host);
|
|
296
|
+
if (response.status === 409) {
|
|
297
|
+
const target = await regionRedirectHost(response);
|
|
298
|
+
if (target && target !== host) {
|
|
299
|
+
console.log(
|
|
300
|
+
chalk3.yellow(
|
|
301
|
+
`\u21BB Workspace is pinned to a different region \u2014 retrying upload to ${target}...`
|
|
302
|
+
)
|
|
303
|
+
);
|
|
304
|
+
host = target;
|
|
305
|
+
response = await postOnce(input, host);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return { response, host };
|
|
309
|
+
}
|
|
310
|
+
|
|
221
311
|
// src/commands/scan.ts
|
|
222
312
|
async function autoPush(artifact, rootDir, opts) {
|
|
223
|
-
const dsn = opts.dsn
|
|
313
|
+
const dsn = resolveDsn(opts.dsn);
|
|
224
314
|
if (!dsn) {
|
|
225
|
-
console.error(
|
|
226
|
-
console.error(
|
|
315
|
+
console.error(chalk4.red("No DSN provided for push."));
|
|
316
|
+
console.error(chalk4.dim('Run "vibgrate login", set VIBGRATE_DSN, or use the --dsn flag.'));
|
|
227
317
|
if (opts.strict) process.exit(1);
|
|
228
318
|
return;
|
|
229
319
|
}
|
|
230
320
|
const parsed = parseDsn(dsn);
|
|
231
321
|
if (!parsed) {
|
|
232
|
-
console.error(
|
|
322
|
+
console.error(chalk4.red("Invalid DSN format."));
|
|
233
323
|
if (opts.strict) process.exit(1);
|
|
234
324
|
return;
|
|
235
325
|
}
|
|
@@ -240,29 +330,26 @@ async function autoPush(artifact, rootDir, opts) {
|
|
|
240
330
|
try {
|
|
241
331
|
host = resolveIngestHost(opts.region);
|
|
242
332
|
} catch (e) {
|
|
243
|
-
console.error(
|
|
333
|
+
console.error(chalk4.red(e instanceof Error ? e.message : String(e)));
|
|
244
334
|
if (opts.strict) process.exit(1);
|
|
245
335
|
return;
|
|
246
336
|
}
|
|
247
337
|
}
|
|
248
|
-
const url = `${parsed.scheme}://${host}/v1/ingest/scan`;
|
|
249
338
|
const originalSize = JSON.stringify(artifact).length;
|
|
250
339
|
const compressedSize = body.length;
|
|
251
340
|
const ratio = ((1 - compressedSize / originalSize) * 100).toFixed(0);
|
|
252
|
-
console.log(
|
|
341
|
+
console.log(chalk4.dim(`Uploading to ${host}... (${(compressedSize / 1024).toFixed(0)} KB, ${ratio}% smaller)`));
|
|
253
342
|
try {
|
|
254
|
-
const response = await
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
// Prevent keep-alive delays on exit
|
|
263
|
-
},
|
|
264
|
-
body
|
|
343
|
+
const { response, host: uploadedHost } = await uploadScanArtifact({
|
|
344
|
+
scheme: parsed.scheme,
|
|
345
|
+
host,
|
|
346
|
+
keyId: parsed.keyId,
|
|
347
|
+
secret: parsed.secret,
|
|
348
|
+
body,
|
|
349
|
+
contentEncoding,
|
|
350
|
+
timestamp
|
|
265
351
|
});
|
|
352
|
+
host = uploadedHost;
|
|
266
353
|
if (!response.ok) {
|
|
267
354
|
const text = await response.text();
|
|
268
355
|
throw new Error(`HTTP ${response.status}: ${text}`);
|
|
@@ -270,36 +357,36 @@ async function autoPush(artifact, rootDir, opts) {
|
|
|
270
357
|
const result = await response.json();
|
|
271
358
|
if (result.unchanged) {
|
|
272
359
|
console.log(
|
|
273
|
-
|
|
360
|
+
chalk4.green("\u2714") + ` Repository unchanged since ${result.lastScannedAt ?? "last scan"} \u2014 skipped upload (no credit used).`
|
|
274
361
|
);
|
|
275
362
|
if (result.previousIngestId) {
|
|
276
363
|
emitIngestIdLine(result.previousIngestId, { unchanged: true });
|
|
277
|
-
const dashUrl = `https
|
|
278
|
-
console.log(
|
|
364
|
+
const dashUrl = `https://${dashHostForIngestHost(host)}/${parsed.workspaceId}/scan/${result.previousIngestId}`;
|
|
365
|
+
console.log(chalk4.dim(` Previous report: ${dashUrl}`));
|
|
279
366
|
} else if (opts.strict) {
|
|
280
|
-
console.error(
|
|
367
|
+
console.error(chalk4.red("Repository unchanged but no previous ingest id returned."));
|
|
281
368
|
process.exit(1);
|
|
282
369
|
}
|
|
283
370
|
return;
|
|
284
371
|
}
|
|
285
|
-
console.log(
|
|
372
|
+
console.log(chalk4.green("\u2714") + ` Scan queued for processing (${result.ingestId ?? "ok"})`);
|
|
286
373
|
if (result.ingestId) {
|
|
287
374
|
emitIngestIdLine(result.ingestId);
|
|
288
|
-
const dashUrl = `https
|
|
375
|
+
const dashUrl = `https://${dashHostForIngestHost(host)}/${parsed.workspaceId}/scan/${result.ingestId}`;
|
|
289
376
|
const CLEAR_LINE = process.platform === "win32" ? "\x1B[0G\x1B[2K" : "";
|
|
290
377
|
console.log("");
|
|
291
|
-
console.log(CLEAR_LINE +
|
|
378
|
+
console.log(CLEAR_LINE + chalk4.dim(" Processing continues in the background. Results available shortly."));
|
|
292
379
|
console.log("");
|
|
293
|
-
console.log(CLEAR_LINE +
|
|
294
|
-
console.log(CLEAR_LINE +
|
|
295
|
-
console.log(CLEAR_LINE + " " +
|
|
296
|
-
console.log(CLEAR_LINE +
|
|
380
|
+
console.log(CLEAR_LINE + chalk4.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"));
|
|
381
|
+
console.log(CLEAR_LINE + chalk4.bold(" \u{1F4CA} View Scan Report"));
|
|
382
|
+
console.log(CLEAR_LINE + " " + chalk4.underline.cyan(dashUrl));
|
|
383
|
+
console.log(CLEAR_LINE + chalk4.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"));
|
|
297
384
|
console.log("");
|
|
298
385
|
console.log("");
|
|
299
386
|
}
|
|
300
387
|
} catch (e) {
|
|
301
388
|
const msg = e instanceof Error ? e.message : String(e);
|
|
302
|
-
console.error(
|
|
389
|
+
console.error(chalk4.red(`Upload failed: ${msg}`));
|
|
303
390
|
if (opts.strict) process.exit(1);
|
|
304
391
|
}
|
|
305
392
|
}
|
|
@@ -320,15 +407,16 @@ var scanCommand = new Command3("scan").description("Scan a project for upgrade d
|
|
|
320
407
|
collectExcludes,
|
|
321
408
|
[]
|
|
322
409
|
).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) => {
|
|
323
|
-
const rootDir =
|
|
410
|
+
const rootDir = path4.resolve(targetPath);
|
|
324
411
|
if (!await pathExists2(rootDir)) {
|
|
325
|
-
console.error(
|
|
412
|
+
console.error(chalk4.red(`Path does not exist: ${rootDir}`));
|
|
326
413
|
process.exit(1);
|
|
327
414
|
}
|
|
328
|
-
const hasDsn = !!(opts.dsn
|
|
415
|
+
const hasDsn = !!resolveDsn(opts.dsn);
|
|
329
416
|
const willPush = !opts.offline && (opts.push || hasDsn);
|
|
417
|
+
let pinnedRegion;
|
|
330
418
|
if (willPush && hasDsn) {
|
|
331
|
-
const dsn = opts.dsn
|
|
419
|
+
const dsn = resolveDsn(opts.dsn);
|
|
332
420
|
const parsed = parseDsn(dsn);
|
|
333
421
|
if (parsed) {
|
|
334
422
|
const ingestHost = opts.region ? resolveIngestHost(opts.region) : parsed.host;
|
|
@@ -340,10 +428,20 @@ var scanCommand = new Command3("scan").description("Scan a project for upgrade d
|
|
|
340
428
|
repositoryName,
|
|
341
429
|
vcsSha: fingerprint.vcsSha
|
|
342
430
|
});
|
|
431
|
+
pinnedRegion = preflight.region;
|
|
432
|
+
if (preflight.vm && !preflight.vm.allowed) {
|
|
433
|
+
console.error(chalk4.red(preflight.error ?? "VM meter usage exhausted"));
|
|
434
|
+
console.error(
|
|
435
|
+
chalk4.dim(
|
|
436
|
+
`VM minutes: ${preflight.vm.used}/${preflight.vm.limit} (${preflight.plan.label} plan) \u2014 enable overages or upgrade your plan.`
|
|
437
|
+
)
|
|
438
|
+
);
|
|
439
|
+
process.exit(1);
|
|
440
|
+
}
|
|
343
441
|
if (preflight.status === "error" || !preflight.scans.allowed) {
|
|
344
|
-
console.error(
|
|
442
|
+
console.error(chalk4.red(preflight.error ?? "Scan ingestion not allowed for this workspace."));
|
|
345
443
|
console.error(
|
|
346
|
-
|
|
444
|
+
chalk4.dim(
|
|
347
445
|
`Credits: ${preflight.scans.used}/${preflight.scans.limit} (${preflight.plan.label} plan)`
|
|
348
446
|
)
|
|
349
447
|
);
|
|
@@ -351,27 +449,27 @@ var scanCommand = new Command3("scan").description("Scan a project for upgrade d
|
|
|
351
449
|
process.exit(1);
|
|
352
450
|
}
|
|
353
451
|
console.log(
|
|
354
|
-
|
|
452
|
+
chalk4.dim(
|
|
355
453
|
`Plan: ${preflight.plan.label} \u2014 scan credits ${preflight.scans.used}/${preflight.scans.limit} this month`
|
|
356
454
|
)
|
|
357
455
|
);
|
|
358
456
|
if (preflight.repository?.unchanged) {
|
|
359
457
|
console.log(
|
|
360
|
-
|
|
458
|
+
chalk4.green("\u2714") + ` Repository unchanged at ${preflight.repository.lastVcsSha?.slice(0, 7) ?? "same revision"} \u2014 skipping scan.`
|
|
361
459
|
);
|
|
362
460
|
if (preflight.repository.lastIngestId) {
|
|
363
461
|
emitIngestIdLine(preflight.repository.lastIngestId, { unchanged: true });
|
|
364
|
-
const dashUrl = `https
|
|
365
|
-
console.log(
|
|
462
|
+
const dashUrl = `https://${dashHostForIngestHost(ingestHost)}/${parsed.workspaceId}/scan/${preflight.repository.lastIngestId}`;
|
|
463
|
+
console.log(chalk4.dim(` Latest report: ${dashUrl}`));
|
|
366
464
|
} else if (opts.strict) {
|
|
367
|
-
console.error(
|
|
465
|
+
console.error(chalk4.red("Repository unchanged but no previous ingest id available."));
|
|
368
466
|
process.exit(1);
|
|
369
467
|
}
|
|
370
468
|
return;
|
|
371
469
|
}
|
|
372
470
|
} catch (e) {
|
|
373
471
|
const msg = e instanceof Error ? e.message : String(e);
|
|
374
|
-
console.error(
|
|
472
|
+
console.error(chalk4.yellow(`Preflight check failed: ${msg}`));
|
|
375
473
|
if (opts.strict) process.exit(1);
|
|
376
474
|
}
|
|
377
475
|
}
|
|
@@ -387,7 +485,9 @@ var scanCommand = new Command3("scan").description("Scan a project for upgrade d
|
|
|
387
485
|
concurrency: parseInt(opts.concurrency, 10) || 8,
|
|
388
486
|
push: opts.push,
|
|
389
487
|
dsn: opts.dsn,
|
|
390
|
-
region
|
|
488
|
+
// An explicit --region wins; otherwise route to the workspace's pinned
|
|
489
|
+
// region as reported by preflight.
|
|
490
|
+
region: opts.region ?? pinnedRegion,
|
|
391
491
|
strict: opts.strict,
|
|
392
492
|
uiPurpose: opts.uiPurpose,
|
|
393
493
|
noLocalArtifacts: opts.noLocalArtifacts,
|
|
@@ -403,24 +503,24 @@ var scanCommand = new Command3("scan").description("Scan a project for upgrade d
|
|
|
403
503
|
const hasErrors = artifact.findings.some((f) => f.level === "error");
|
|
404
504
|
const hasWarnings = artifact.findings.some((f) => f.level === "warning");
|
|
405
505
|
if (opts.failOn === "error" && hasErrors) {
|
|
406
|
-
console.error(
|
|
506
|
+
console.error(chalk4.red(`
|
|
407
507
|
Failing: ${artifact.findings.filter((f) => f.level === "error").length} error finding(s) detected.`));
|
|
408
508
|
process.exit(2);
|
|
409
509
|
}
|
|
410
510
|
if (opts.failOn === "warn" && (hasErrors || hasWarnings)) {
|
|
411
|
-
console.error(
|
|
511
|
+
console.error(chalk4.red(`
|
|
412
512
|
Failing: findings detected at warn level or above.`));
|
|
413
513
|
process.exit(2);
|
|
414
514
|
}
|
|
415
515
|
}
|
|
416
516
|
if (scanOpts.driftBudget !== void 0 && artifact.drift.score > scanOpts.driftBudget) {
|
|
417
|
-
console.error(
|
|
517
|
+
console.error(chalk4.red(`
|
|
418
518
|
Failing fitness function: drift score ${artifact.drift.score}/100 exceeds budget ${scanOpts.driftBudget}.`));
|
|
419
519
|
process.exit(2);
|
|
420
520
|
}
|
|
421
521
|
if (scanOpts.driftWorseningPercent !== void 0) {
|
|
422
522
|
if (artifact.delta === void 0) {
|
|
423
|
-
console.error(
|
|
523
|
+
console.error(chalk4.red("\nFailing fitness function: --drift-worsening requires --baseline to compare against previous drift."));
|
|
424
524
|
process.exit(2);
|
|
425
525
|
}
|
|
426
526
|
if (artifact.delta > 0) {
|
|
@@ -428,7 +528,7 @@ Failing fitness function: drift score ${artifact.drift.score}/100 exceeds budget
|
|
|
428
528
|
const denominator = Math.max(Math.abs(baselineScore), 1e-4);
|
|
429
529
|
const worseningPercent = artifact.delta / denominator * 100;
|
|
430
530
|
if (worseningPercent > scanOpts.driftWorseningPercent) {
|
|
431
|
-
console.error(
|
|
531
|
+
console.error(chalk4.red(`
|
|
432
532
|
Failing fitness function: drift worsened by ${worseningPercent.toFixed(2)}% (threshold ${scanOpts.driftWorseningPercent}%).`));
|
|
433
533
|
process.exit(2);
|
|
434
534
|
}
|
|
@@ -440,30 +540,30 @@ Failing fitness function: drift worsened by ${worseningPercent.toFixed(2)}% (thr
|
|
|
440
540
|
});
|
|
441
541
|
|
|
442
542
|
// src/commands/report.ts
|
|
443
|
-
import * as
|
|
543
|
+
import * as path5 from "path";
|
|
444
544
|
import { Command as Command4 } from "commander";
|
|
445
|
-
import
|
|
545
|
+
import chalk6 from "chalk";
|
|
446
546
|
|
|
447
547
|
// src/formatters/text.ts
|
|
448
|
-
import
|
|
548
|
+
import chalk5 from "chalk";
|
|
449
549
|
function formatText(artifact) {
|
|
450
550
|
const lines = [];
|
|
451
|
-
const teal =
|
|
452
|
-
const mint =
|
|
551
|
+
const teal = chalk5.hex("#3FB0A4");
|
|
552
|
+
const mint = chalk5.hex("#4FE3C1");
|
|
453
553
|
lines.push("");
|
|
454
554
|
lines.push(" " + teal("\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u256E") + mint("\u279C"));
|
|
455
|
-
lines.push(" " +
|
|
456
|
-
lines.push(" " +
|
|
555
|
+
lines.push(" " + chalk5.dim("\u2524") + teal("\u2502") + " " + mint("\u25FC") + " " + mint("\u25FC") + " " + teal("\u2502") + chalk5.dim("\u251C") + " " + chalk5.bold.white("vibgrate"));
|
|
556
|
+
lines.push(" " + chalk5.dim("\u2524") + teal("\u2502") + " " + chalk5.dim("\u2581\u2581") + " " + teal("\u2502") + chalk5.dim("\u251C") + " " + chalk5.dim(`Drift Intelligence Engine v${VERSION}`));
|
|
457
557
|
lines.push(" " + teal("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u256F"));
|
|
458
558
|
lines.push("");
|
|
459
559
|
lines.push(teal("\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"));
|
|
460
|
-
lines.push(teal("\u2551 ") +
|
|
560
|
+
lines.push(teal("\u2551 ") + chalk5.bold.white("Vibgrate Drift Report") + teal(" \u2551"));
|
|
461
561
|
lines.push(teal("\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"));
|
|
462
562
|
lines.push("");
|
|
463
563
|
for (const project of artifact.projects) {
|
|
464
|
-
lines.push(
|
|
564
|
+
lines.push(chalk5.bold(` \u2500\u2500 ${project.name} `) + chalk5.dim(`(${project.type}) ${project.path}`));
|
|
465
565
|
if (project.runtime) {
|
|
466
|
-
const behindStr = project.runtimeMajorsBehind !== void 0 && project.runtimeMajorsBehind > 0 ?
|
|
566
|
+
const behindStr = project.runtimeMajorsBehind !== void 0 && project.runtimeMajorsBehind > 0 ? chalk5.yellow(` (${project.runtimeMajorsBehind} major${project.runtimeMajorsBehind > 1 ? "s" : ""} behind)`) : chalk5.green(" (current)");
|
|
467
567
|
lines.push(` Runtime: ${project.runtime}${behindStr}`);
|
|
468
568
|
}
|
|
469
569
|
if (project.targetFramework) {
|
|
@@ -472,7 +572,7 @@ function formatText(artifact) {
|
|
|
472
572
|
if (project.frameworks.length > 0) {
|
|
473
573
|
lines.push(" Frameworks:");
|
|
474
574
|
for (const fw of project.frameworks) {
|
|
475
|
-
const lag = fw.majorsBehind !== null ? fw.majorsBehind === 0 ?
|
|
575
|
+
const lag = fw.majorsBehind !== null ? fw.majorsBehind === 0 ? chalk5.green("current") : chalk5.yellow(`${fw.majorsBehind} behind`) : chalk5.dim("unknown");
|
|
476
576
|
lines.push(` ${fw.name}: ${fw.currentVersion ?? "?"} \u2192 ${fw.latestVersion ?? "?"} (${lag})`);
|
|
477
577
|
}
|
|
478
578
|
}
|
|
@@ -480,13 +580,13 @@ function formatText(artifact) {
|
|
|
480
580
|
const total = b.current + b.oneBehind + b.twoPlusBehind + b.unknown;
|
|
481
581
|
if (total > 0) {
|
|
482
582
|
lines.push(" Dependencies:");
|
|
483
|
-
lines.push(` ${
|
|
583
|
+
lines.push(` ${chalk5.green(`${b.current} current`)} ${chalk5.yellow(`${b.oneBehind} 1-behind`)} ${chalk5.red(`${b.twoPlusBehind} 2+ behind`)} ${chalk5.dim(`${b.unknown} unknown`)}`);
|
|
484
584
|
}
|
|
485
585
|
lines.push("");
|
|
486
586
|
}
|
|
487
587
|
if (artifact.delta !== void 0) {
|
|
488
|
-
const deltaStr = artifact.delta > 0 ?
|
|
489
|
-
lines.push(
|
|
588
|
+
const deltaStr = artifact.delta > 0 ? chalk5.green(`+${artifact.delta}`) : artifact.delta < 0 ? chalk5.red(`${artifact.delta}`) : chalk5.dim("0");
|
|
589
|
+
lines.push(chalk5.bold(" Drift Delta: ") + deltaStr + " (vs baseline)");
|
|
490
590
|
lines.push("");
|
|
491
591
|
}
|
|
492
592
|
if (artifact.extended) {
|
|
@@ -497,30 +597,30 @@ function formatText(artifact) {
|
|
|
497
597
|
const warnings = artifact.findings.filter((f) => f.level === "warning");
|
|
498
598
|
const notes = artifact.findings.filter((f) => f.level === "note");
|
|
499
599
|
const summary = [
|
|
500
|
-
errors.length > 0 ?
|
|
501
|
-
warnings.length > 0 ?
|
|
502
|
-
notes.length > 0 ?
|
|
503
|
-
].filter(Boolean).join(
|
|
504
|
-
lines.push(
|
|
600
|
+
errors.length > 0 ? chalk5.red(`${errors.length} error${errors.length !== 1 ? "s" : ""}`) : "",
|
|
601
|
+
warnings.length > 0 ? chalk5.yellow(`${warnings.length} warning${warnings.length !== 1 ? "s" : ""}`) : "",
|
|
602
|
+
notes.length > 0 ? chalk5.blue(`${notes.length} note${notes.length !== 1 ? "s" : ""}`) : ""
|
|
603
|
+
].filter(Boolean).join(chalk5.dim(", "));
|
|
604
|
+
lines.push(chalk5.bold.underline(` Findings`) + chalk5.dim(` (${summary})`));
|
|
505
605
|
for (const f of artifact.findings) {
|
|
506
|
-
const icon = f.level === "error" ?
|
|
606
|
+
const icon = f.level === "error" ? chalk5.red("\u2716") : f.level === "warning" ? chalk5.yellow("\u26A0") : chalk5.blue("\u2139");
|
|
507
607
|
lines.push(` ${icon} ${f.message}`);
|
|
508
|
-
lines.push(
|
|
608
|
+
lines.push(chalk5.dim(` ${f.ruleId} in ${f.location}`));
|
|
509
609
|
}
|
|
510
610
|
lines.push("");
|
|
511
611
|
}
|
|
512
612
|
const actions = generatePriorityActions(artifact);
|
|
513
613
|
if (actions.length > 0) {
|
|
514
|
-
lines.push(
|
|
515
|
-
lines.push(
|
|
516
|
-
lines.push(
|
|
614
|
+
lines.push(chalk5.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"));
|
|
615
|
+
lines.push(chalk5.bold.cyan("\u2551 Top Priority Actions \u2551"));
|
|
616
|
+
lines.push(chalk5.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"));
|
|
517
617
|
lines.push("");
|
|
518
618
|
for (let i = 0; i < actions.length; i++) {
|
|
519
619
|
const a = actions[i];
|
|
520
|
-
const num =
|
|
521
|
-
lines.push(`${num} ${
|
|
522
|
-
lines.push(
|
|
523
|
-
if (a.impact) lines.push(` Impact: ${
|
|
620
|
+
const num = chalk5.bold.cyan(` ${i + 1}.`);
|
|
621
|
+
lines.push(`${num} ${chalk5.bold(a.title)}`);
|
|
622
|
+
lines.push(chalk5.dim(` ${a.explanation}`));
|
|
623
|
+
if (a.impact) lines.push(` Impact: ${chalk5.green(a.impact)}`);
|
|
524
624
|
lines.push("");
|
|
525
625
|
}
|
|
526
626
|
}
|
|
@@ -528,38 +628,38 @@ function formatText(artifact) {
|
|
|
528
628
|
lines.push(...formatArchitectureDiagram(artifact.extended.architecture));
|
|
529
629
|
}
|
|
530
630
|
if (artifact.solutions && artifact.solutions.length > 0) {
|
|
531
|
-
lines.push(
|
|
532
|
-
lines.push(
|
|
533
|
-
lines.push(
|
|
631
|
+
lines.push(chalk5.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"));
|
|
632
|
+
lines.push(chalk5.bold.cyan("\u2551 Solution Drift Summary \u2551"));
|
|
633
|
+
lines.push(chalk5.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"));
|
|
534
634
|
lines.push("");
|
|
535
635
|
for (const solution of artifact.solutions) {
|
|
536
636
|
const solScore = solution.drift?.score;
|
|
537
|
-
const color = typeof solScore === "number" ? solScore >= 70 ?
|
|
538
|
-
lines.push(` \u2022 ${solution.name} (${solution.projectPaths.length} projects) \u2014 ${typeof solScore === "number" ? color(`${solScore}/100`) :
|
|
637
|
+
const color = typeof solScore === "number" ? solScore >= 70 ? chalk5.green : solScore >= 40 ? chalk5.yellow : chalk5.red : chalk5.dim;
|
|
638
|
+
lines.push(` \u2022 ${solution.name} (${solution.projectPaths.length} projects) \u2014 ${typeof solScore === "number" ? color(`${solScore}/100`) : chalk5.dim("n/a")}`);
|
|
539
639
|
}
|
|
540
640
|
lines.push("");
|
|
541
641
|
}
|
|
542
|
-
const scoreColor = artifact.drift.score >= 70 ?
|
|
543
|
-
lines.push(
|
|
544
|
-
lines.push(
|
|
545
|
-
lines.push(
|
|
642
|
+
const scoreColor = artifact.drift.score >= 70 ? chalk5.green : artifact.drift.score >= 40 ? chalk5.yellow : chalk5.red;
|
|
643
|
+
lines.push(chalk5.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"));
|
|
644
|
+
lines.push(chalk5.bold.cyan("\u2551 Drift Score Summary \u2551"));
|
|
645
|
+
lines.push(chalk5.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"));
|
|
546
646
|
lines.push("");
|
|
547
|
-
lines.push(
|
|
548
|
-
lines.push(
|
|
549
|
-
lines.push(
|
|
647
|
+
lines.push(chalk5.bold(" Drift Score: ") + scoreColor.bold(`${artifact.drift.score}/100`));
|
|
648
|
+
lines.push(chalk5.bold(" Risk Level: ") + riskBadge(artifact.drift.riskLevel));
|
|
649
|
+
lines.push(chalk5.bold(" Projects: ") + `${artifact.projects.length}`);
|
|
550
650
|
if (artifact.vcs) {
|
|
551
651
|
const vcsParts = [artifact.vcs.type];
|
|
552
652
|
if (artifact.vcs.branch) vcsParts.push(artifact.vcs.branch);
|
|
553
|
-
if (artifact.vcs.shortSha) vcsParts.push(
|
|
554
|
-
lines.push(
|
|
653
|
+
if (artifact.vcs.shortSha) vcsParts.push(chalk5.dim(artifact.vcs.shortSha));
|
|
654
|
+
lines.push(chalk5.bold(" VCS: ") + vcsParts.join(" "));
|
|
555
655
|
}
|
|
556
656
|
lines.push("");
|
|
557
657
|
const m = new Set(artifact.drift.measured ?? ["runtime", "framework", "dependency", "eol"]);
|
|
558
|
-
lines.push(" " +
|
|
559
|
-
lines.push(` Runtime: ${m.has("runtime") ? scoreBar(artifact.drift.components.runtimeScore) :
|
|
560
|
-
lines.push(` Frameworks: ${m.has("framework") ? scoreBar(artifact.drift.components.frameworkScore) :
|
|
561
|
-
lines.push(` Dependencies: ${m.has("dependency") ? scoreBar(artifact.drift.components.dependencyScore) :
|
|
562
|
-
lines.push(` EOL Risk: ${m.has("eol") ? scoreBar(artifact.drift.components.eolScore) :
|
|
658
|
+
lines.push(" " + chalk5.bold.underline("Score Breakdown"));
|
|
659
|
+
lines.push(` Runtime: ${m.has("runtime") ? scoreBar(artifact.drift.components.runtimeScore) : chalk5.dim("n/a")}`);
|
|
660
|
+
lines.push(` Frameworks: ${m.has("framework") ? scoreBar(artifact.drift.components.frameworkScore) : chalk5.dim("n/a")}`);
|
|
661
|
+
lines.push(` Dependencies: ${m.has("dependency") ? scoreBar(artifact.drift.components.dependencyScore) : chalk5.dim("n/a")}`);
|
|
662
|
+
lines.push(` EOL Risk: ${m.has("eol") ? scoreBar(artifact.drift.components.eolScore) : chalk5.dim("n/a")}`);
|
|
563
663
|
lines.push("");
|
|
564
664
|
const scannedParts = [`Scanned at ${artifact.timestamp}`];
|
|
565
665
|
if (artifact.durationMs !== void 0) {
|
|
@@ -573,18 +673,18 @@ function formatText(artifact) {
|
|
|
573
673
|
scannedParts.push(`${artifact.treeSummary.totalFiles.toLocaleString()} workspace files`);
|
|
574
674
|
scannedParts.push(`${artifact.treeSummary.totalDirs.toLocaleString()} dirs`);
|
|
575
675
|
}
|
|
576
|
-
lines.push(
|
|
676
|
+
lines.push(chalk5.dim(` ${scannedParts.join(" \xB7 ")}`));
|
|
577
677
|
lines.push("");
|
|
578
678
|
return lines.join("\n");
|
|
579
679
|
}
|
|
580
680
|
function riskBadge(level) {
|
|
581
681
|
switch (level) {
|
|
582
682
|
case "low":
|
|
583
|
-
return
|
|
683
|
+
return chalk5.bgGreen.black(" LOW ");
|
|
584
684
|
case "moderate":
|
|
585
|
-
return
|
|
685
|
+
return chalk5.bgYellow.black(" MODERATE ");
|
|
586
686
|
case "high":
|
|
587
|
-
return
|
|
687
|
+
return chalk5.bgRed.white(" HIGH ");
|
|
588
688
|
default:
|
|
589
689
|
return level;
|
|
590
690
|
}
|
|
@@ -593,8 +693,8 @@ function scoreBar(score) {
|
|
|
593
693
|
const width = 20;
|
|
594
694
|
const filled = Math.round(score / 100 * width);
|
|
595
695
|
const empty = width - filled;
|
|
596
|
-
const color = score >= 70 ?
|
|
597
|
-
return color("\u2588".repeat(filled)) +
|
|
696
|
+
const color = score >= 70 ? chalk5.green : score >= 40 ? chalk5.yellow : chalk5.red;
|
|
697
|
+
return color("\u2588".repeat(filled)) + chalk5.dim("\u2591".repeat(empty)) + ` ${Math.round(score)}`;
|
|
598
698
|
}
|
|
599
699
|
var CATEGORY_LABELS = {
|
|
600
700
|
frontend: "Frontend",
|
|
@@ -623,11 +723,11 @@ function formatExtended(ext) {
|
|
|
623
723
|
const inv = ext.toolingInventory;
|
|
624
724
|
const categories = Object.entries(inv).filter(([, items]) => items.length > 0);
|
|
625
725
|
if (categories.length > 0) {
|
|
626
|
-
lines.push(
|
|
726
|
+
lines.push(chalk5.bold.underline(" Tech Stack"));
|
|
627
727
|
for (const [cat, items] of categories) {
|
|
628
728
|
const label = CATEGORY_LABELS[cat] ?? cat;
|
|
629
|
-
const names = items.map((i) =>
|
|
630
|
-
lines.push(` ${
|
|
729
|
+
const names = items.map((i) => chalk5.white(i.name)).join(chalk5.dim(", "));
|
|
730
|
+
lines.push(` ${chalk5.cyan(label)}: ${names}`);
|
|
631
731
|
}
|
|
632
732
|
lines.push("");
|
|
633
733
|
}
|
|
@@ -636,14 +736,14 @@ function formatExtended(ext) {
|
|
|
636
736
|
const svc = ext.serviceDependencies;
|
|
637
737
|
const categories = Object.entries(svc).filter(([, items]) => items.length > 0);
|
|
638
738
|
if (categories.length > 0) {
|
|
639
|
-
lines.push(
|
|
739
|
+
lines.push(chalk5.bold.underline(" Services & Integrations"));
|
|
640
740
|
for (const [cat, items] of categories) {
|
|
641
741
|
const label = CATEGORY_LABELS[cat] ?? cat;
|
|
642
742
|
const names = items.map((i) => {
|
|
643
|
-
const ver = i.version ?
|
|
644
|
-
return
|
|
645
|
-
}).join(
|
|
646
|
-
lines.push(` ${
|
|
743
|
+
const ver = i.version ? chalk5.dim(` ${i.version}`) : "";
|
|
744
|
+
return chalk5.white(i.name) + ver;
|
|
745
|
+
}).join(chalk5.dim(", "));
|
|
746
|
+
lines.push(` ${chalk5.cyan(label)}: ${names}`);
|
|
647
747
|
}
|
|
648
748
|
lines.push("");
|
|
649
749
|
}
|
|
@@ -651,19 +751,19 @@ function formatExtended(ext) {
|
|
|
651
751
|
if (ext.breakingChangeExposure) {
|
|
652
752
|
const bc = ext.breakingChangeExposure;
|
|
653
753
|
if (bc.deprecatedPackages.length > 0 || bc.legacyPolyfills.length > 0) {
|
|
654
|
-
lines.push(
|
|
655
|
-
const exposureColor = bc.exposureScore >= 40 ?
|
|
754
|
+
lines.push(chalk5.bold.underline(" Breaking Change Exposure"));
|
|
755
|
+
const exposureColor = bc.exposureScore >= 40 ? chalk5.red : bc.exposureScore >= 20 ? chalk5.yellow : chalk5.green;
|
|
656
756
|
lines.push(` Exposure Score: ${exposureColor.bold(`${bc.exposureScore}/100`)}`);
|
|
657
757
|
if (bc.deprecatedPackages.length > 0) {
|
|
658
|
-
lines.push(` ${
|
|
758
|
+
lines.push(` ${chalk5.red("Deprecated")}: ${bc.deprecatedPackages.map((p) => chalk5.dim(p)).join(", ")}`);
|
|
659
759
|
}
|
|
660
760
|
if (bc.legacyPolyfills.length > 0) {
|
|
661
|
-
lines.push(` ${
|
|
761
|
+
lines.push(` ${chalk5.yellow("Polyfills")}: ${bc.legacyPolyfills.map((p) => chalk5.dim(p)).join(", ")}`);
|
|
662
762
|
}
|
|
663
763
|
if (bc.peerConflictsDetected) {
|
|
664
|
-
lines.push(` ${
|
|
764
|
+
lines.push(` ${chalk5.red("\u26A0")} Peer dependency conflicts detected`);
|
|
665
765
|
}
|
|
666
|
-
lines.push(` Recommendation: ${
|
|
766
|
+
lines.push(` Recommendation: ${chalk5.bold(bc.overallRecommendation)}`);
|
|
667
767
|
const projectsWithPlans = bc.projectIntelligence.filter((p) => p.packages.length > 0).slice(0, 3);
|
|
668
768
|
if (projectsWithPlans.length > 0) {
|
|
669
769
|
lines.push(" Major Upgrade Intelligence:");
|
|
@@ -679,21 +779,21 @@ function formatExtended(ext) {
|
|
|
679
779
|
}
|
|
680
780
|
if (ext.tsModernity && ext.tsModernity.typescriptVersion) {
|
|
681
781
|
const ts = ext.tsModernity;
|
|
682
|
-
lines.push(
|
|
782
|
+
lines.push(chalk5.bold.underline(" TypeScript"));
|
|
683
783
|
const parts = [];
|
|
684
784
|
parts.push(`v${ts.typescriptVersion}`);
|
|
685
|
-
if (ts.strict === true) parts.push(
|
|
686
|
-
else if (ts.strict === false) parts.push(
|
|
785
|
+
if (ts.strict === true) parts.push(chalk5.green("strict \u2714"));
|
|
786
|
+
else if (ts.strict === false) parts.push(chalk5.yellow("strict \u2716"));
|
|
687
787
|
if (ts.moduleType) parts.push(ts.moduleType.toUpperCase());
|
|
688
788
|
if (ts.target) parts.push(`target: ${ts.target}`);
|
|
689
|
-
lines.push(` ${parts.join(
|
|
789
|
+
lines.push(` ${parts.join(chalk5.dim(" \xB7 "))}`);
|
|
690
790
|
lines.push("");
|
|
691
791
|
}
|
|
692
792
|
if (ext.buildDeploy) {
|
|
693
793
|
const bd = ext.buildDeploy;
|
|
694
794
|
const hasSomething = bd.ci.length > 0 || bd.docker.dockerfileCount > 0 || bd.packageManagers.length > 0;
|
|
695
795
|
if (hasSomething) {
|
|
696
|
-
lines.push(
|
|
796
|
+
lines.push(chalk5.bold.underline(" Build & Deploy"));
|
|
697
797
|
if (bd.ci.length > 0) lines.push(` CI: ${bd.ci.join(", ")}`);
|
|
698
798
|
if (bd.docker.dockerfileCount > 0) {
|
|
699
799
|
lines.push(` Docker: ${bd.docker.dockerfileCount} Dockerfile${bd.docker.dockerfileCount !== 1 ? "s" : ""} (${bd.docker.baseImages.join(", ")})`);
|
|
@@ -706,42 +806,42 @@ function formatExtended(ext) {
|
|
|
706
806
|
}
|
|
707
807
|
if (ext.uiPurpose) {
|
|
708
808
|
const up = ext.uiPurpose;
|
|
709
|
-
lines.push(
|
|
710
|
-
lines.push(` Frameworks: ${up.detectedFrameworks.length > 0 ? up.detectedFrameworks.join(", ") :
|
|
711
|
-
lines.push(` Evidence: ${up.topEvidence.length}${up.capped ?
|
|
809
|
+
lines.push(chalk5.bold.underline(" Product Purpose Signals"));
|
|
810
|
+
lines.push(` Frameworks: ${up.detectedFrameworks.length > 0 ? up.detectedFrameworks.join(", ") : chalk5.dim("unknown")}`);
|
|
811
|
+
lines.push(` Evidence: ${up.topEvidence.length}${up.capped ? chalk5.dim(` of ${up.evidenceCount} (capped)`) : ""}`);
|
|
712
812
|
const top = up.topEvidence.slice(0, 8);
|
|
713
813
|
if (top.length > 0) {
|
|
714
814
|
lines.push(" Top Signals:");
|
|
715
815
|
for (const item of top) {
|
|
716
|
-
lines.push(` - [${item.kind}] ${item.value} ${
|
|
816
|
+
lines.push(` - [${item.kind}] ${item.value} ${chalk5.dim(`(${item.file})`)}`);
|
|
717
817
|
}
|
|
718
818
|
}
|
|
719
819
|
if (up.unknownSignals.length > 0) {
|
|
720
820
|
lines.push(" Unknowns:");
|
|
721
821
|
for (const u of up.unknownSignals.slice(0, 4)) {
|
|
722
|
-
lines.push(` - ${
|
|
822
|
+
lines.push(` - ${chalk5.yellow(u)}`);
|
|
723
823
|
}
|
|
724
824
|
}
|
|
725
825
|
lines.push("");
|
|
726
826
|
}
|
|
727
827
|
if (ext.securityPosture) {
|
|
728
828
|
const sec = ext.securityPosture;
|
|
729
|
-
lines.push(
|
|
829
|
+
lines.push(chalk5.bold.underline(" Security Posture"));
|
|
730
830
|
const checks = [];
|
|
731
|
-
checks.push(sec.lockfilePresent ?
|
|
732
|
-
checks.push(sec.gitignoreCoversEnv ?
|
|
733
|
-
checks.push(sec.gitignoreCoversNodeModules ?
|
|
734
|
-
if (sec.multipleLockfileTypes) checks.push(
|
|
735
|
-
if (sec.envFilesTracked) checks.push(
|
|
736
|
-
lines.push(` ${checks.join(
|
|
831
|
+
checks.push(sec.lockfilePresent ? chalk5.green("Lockfile \u2714") : chalk5.red("Lockfile \u2716"));
|
|
832
|
+
checks.push(sec.gitignoreCoversEnv ? chalk5.green(".env \u2714") : chalk5.red(".env \u2716"));
|
|
833
|
+
checks.push(sec.gitignoreCoversNodeModules ? chalk5.green("node_modules \u2714") : chalk5.yellow("node_modules \u2716"));
|
|
834
|
+
if (sec.multipleLockfileTypes) checks.push(chalk5.yellow("Multiple lockfiles \u26A0"));
|
|
835
|
+
if (sec.envFilesTracked) checks.push(chalk5.red("Env files tracked \u2716"));
|
|
836
|
+
lines.push(` ${checks.join(chalk5.dim(" \xB7 "))}`);
|
|
737
837
|
lines.push("");
|
|
738
838
|
}
|
|
739
839
|
if (ext.platformMatrix) {
|
|
740
840
|
const pm = ext.platformMatrix;
|
|
741
841
|
if (pm.nativeModules.length > 0 || pm.dockerBaseImages.length > 0) {
|
|
742
|
-
lines.push(
|
|
842
|
+
lines.push(chalk5.bold.underline(" Platform"));
|
|
743
843
|
if (pm.nativeModules.length > 0) {
|
|
744
|
-
lines.push(` Native modules: ${pm.nativeModules.map((m) =>
|
|
844
|
+
lines.push(` Native modules: ${pm.nativeModules.map((m) => chalk5.dim(m)).join(", ")}`);
|
|
745
845
|
}
|
|
746
846
|
if (pm.osAssumptions.length > 0) {
|
|
747
847
|
lines.push(` OS assumptions: ${pm.osAssumptions.join(", ")}`);
|
|
@@ -751,25 +851,25 @@ function formatExtended(ext) {
|
|
|
751
851
|
}
|
|
752
852
|
if (ext.codeQuality) {
|
|
753
853
|
const cq = ext.codeQuality;
|
|
754
|
-
lines.push(
|
|
755
|
-
lines.push(` Files: ${
|
|
854
|
+
lines.push(chalk5.bold.underline(" Code Quality"));
|
|
855
|
+
lines.push(` Files: ${chalk5.white(`${cq.filesAnalyzed}`)} \xB7 Functions: ${chalk5.white(`${cq.functionsAnalyzed}`)} \xB7 Avg complexity: ${chalk5.white(`${cq.avgCyclomaticComplexity}`)} \xB7 Avg length: ${chalk5.white(`${cq.avgFunctionLength}`)} lines`);
|
|
756
856
|
lines.push(` Max nesting: ${cq.maxNestingDepth} \xB7 Circular deps: ${cq.circularDependencies} \xB7 Dead code: ${cq.deadCodePercent}%`);
|
|
757
857
|
if (cq.godFiles.length > 0) {
|
|
758
858
|
const preview = cq.godFiles.slice(0, 3).map((f) => `${f.path} (${f.lines} lines)`).join(", ");
|
|
759
|
-
lines.push(` ${
|
|
859
|
+
lines.push(` ${chalk5.yellow("God files")}: ${preview}`);
|
|
760
860
|
}
|
|
761
861
|
lines.push("");
|
|
762
862
|
}
|
|
763
863
|
if (ext.dependencyGraph) {
|
|
764
864
|
const dg = ext.dependencyGraph;
|
|
765
865
|
if (dg.lockfileType) {
|
|
766
|
-
lines.push(
|
|
767
|
-
lines.push(` ${dg.lockfileType}: ${
|
|
866
|
+
lines.push(chalk5.bold.underline(" Dependency Graph"));
|
|
867
|
+
lines.push(` ${dg.lockfileType}: ${chalk5.white(`${dg.totalUnique}`)} unique, ${chalk5.white(`${dg.totalInstalled}`)} installed`);
|
|
768
868
|
if (dg.duplicatedPackages.length > 0) {
|
|
769
|
-
lines.push(` ${
|
|
869
|
+
lines.push(` ${chalk5.yellow(`${dg.duplicatedPackages.length} duplicated`)} packages`);
|
|
770
870
|
}
|
|
771
871
|
if (dg.phantomDependencies.length > 0) {
|
|
772
|
-
lines.push(` ${
|
|
872
|
+
lines.push(` ${chalk5.red(`${dg.phantomDependencies.length} phantom`)} dependencies`);
|
|
773
873
|
}
|
|
774
874
|
lines.push("");
|
|
775
875
|
}
|
|
@@ -778,17 +878,17 @@ function formatExtended(ext) {
|
|
|
778
878
|
}
|
|
779
879
|
function formatArchitectureDiagram(arch) {
|
|
780
880
|
const lines = [];
|
|
781
|
-
lines.push(
|
|
782
|
-
lines.push(
|
|
783
|
-
lines.push(
|
|
881
|
+
lines.push(chalk5.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"));
|
|
882
|
+
lines.push(chalk5.bold.cyan("\u2551 Architecture Layers \u2551"));
|
|
883
|
+
lines.push(chalk5.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"));
|
|
784
884
|
lines.push("");
|
|
785
|
-
lines.push(
|
|
786
|
-
lines.push(` Files classified: ${arch.totalClassified}` + (arch.unclassified > 0 ?
|
|
885
|
+
lines.push(chalk5.bold(" Archetype: ") + `${arch.archetype}` + chalk5.dim(` (${Math.round(arch.archetypeConfidence * 100)}% confidence)`));
|
|
886
|
+
lines.push(` Files classified: ${arch.totalClassified}` + (arch.unclassified > 0 ? chalk5.dim(` (${arch.unclassified} unclassified)`) : ""));
|
|
787
887
|
lines.push("");
|
|
788
888
|
if (arch.layers.length > 0) {
|
|
789
889
|
for (const layer of arch.layers) {
|
|
790
|
-
const risk = layer.riskLevel === "none" ?
|
|
791
|
-
lines.push(` ${
|
|
890
|
+
const risk = layer.riskLevel === "none" ? chalk5.dim("none") : layer.riskLevel === "low" ? chalk5.green("low") : layer.riskLevel === "moderate" ? chalk5.yellow("moderate") : chalk5.red("high");
|
|
891
|
+
lines.push(` ${chalk5.bold(layer.layer)} ${layer.fileCount} file${layer.fileCount !== 1 ? "s" : ""} drift ${scoreBar(layer.driftScore)} risk ${risk}`);
|
|
792
892
|
}
|
|
793
893
|
lines.push("");
|
|
794
894
|
}
|
|
@@ -1157,10 +1257,10 @@ function formatMarkdown(artifact) {
|
|
|
1157
1257
|
|
|
1158
1258
|
// src/commands/report.ts
|
|
1159
1259
|
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) => {
|
|
1160
|
-
const artifactPath =
|
|
1260
|
+
const artifactPath = path5.resolve(opts.in);
|
|
1161
1261
|
if (!await pathExists(artifactPath)) {
|
|
1162
|
-
console.error(
|
|
1163
|
-
console.error(
|
|
1262
|
+
console.error(chalk6.red(`Artifact not found: ${artifactPath}`));
|
|
1263
|
+
console.error(chalk6.dim('Run "vibgrate scan" first to generate a scan artifact.'));
|
|
1164
1264
|
process.exit(1);
|
|
1165
1265
|
}
|
|
1166
1266
|
const artifact = await readJsonFile(artifactPath);
|
|
@@ -1178,11 +1278,145 @@ var reportCommand = new Command4("report").description("Generate a drift report
|
|
|
1178
1278
|
}
|
|
1179
1279
|
});
|
|
1180
1280
|
|
|
1281
|
+
// src/commands/login.ts
|
|
1282
|
+
import { Command as Command5 } from "commander";
|
|
1283
|
+
import chalk7 from "chalk";
|
|
1284
|
+
|
|
1285
|
+
// src/utils/open-url.ts
|
|
1286
|
+
import { spawn } from "child_process";
|
|
1287
|
+
function openUrl(url) {
|
|
1288
|
+
const platform = process.platform;
|
|
1289
|
+
let command;
|
|
1290
|
+
let args;
|
|
1291
|
+
if (platform === "darwin") {
|
|
1292
|
+
command = "open";
|
|
1293
|
+
args = [url];
|
|
1294
|
+
} else if (platform === "win32") {
|
|
1295
|
+
command = "cmd";
|
|
1296
|
+
args = ["/c", "start", "", url];
|
|
1297
|
+
} else {
|
|
1298
|
+
command = "xdg-open";
|
|
1299
|
+
args = [url];
|
|
1300
|
+
}
|
|
1301
|
+
try {
|
|
1302
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
1303
|
+
child.on("error", () => {
|
|
1304
|
+
});
|
|
1305
|
+
child.unref();
|
|
1306
|
+
return true;
|
|
1307
|
+
} catch {
|
|
1308
|
+
return false;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// src/commands/login.ts
|
|
1313
|
+
var delay = (ms) => new Promise((resolve10) => setTimeout(resolve10, ms));
|
|
1314
|
+
var loginCommand = new Command5("login").description("Authenticate the CLI with your Vibgrate workspace via the browser").option("--ingest <url>", "Ingest API URL (overrides --region)").option("--region <region>", `Data residency region (${availableRegionIds().join(", ")})`, "us").option("--no-browser", "Do not attempt to open a browser automatically").action(async (opts) => {
|
|
1315
|
+
let ingestHost;
|
|
1316
|
+
try {
|
|
1317
|
+
ingestHost = resolveIngestHost(opts.region, opts.ingest);
|
|
1318
|
+
} catch (e) {
|
|
1319
|
+
console.error(chalk7.red(e instanceof Error ? e.message : String(e)));
|
|
1320
|
+
process.exit(1);
|
|
1321
|
+
}
|
|
1322
|
+
const base = `https://${ingestHost}/v1/auth/device`;
|
|
1323
|
+
let start;
|
|
1324
|
+
try {
|
|
1325
|
+
const res = await fetch(`${base}/start`, {
|
|
1326
|
+
method: "POST",
|
|
1327
|
+
headers: { "Content-Type": "application/json", Connection: "close" },
|
|
1328
|
+
body: "{}"
|
|
1329
|
+
});
|
|
1330
|
+
if (!res.ok) {
|
|
1331
|
+
console.error(chalk7.red(`Failed to start login (HTTP ${res.status}).`));
|
|
1332
|
+
process.exit(1);
|
|
1333
|
+
}
|
|
1334
|
+
start = await res.json();
|
|
1335
|
+
} catch (e) {
|
|
1336
|
+
console.error(chalk7.red(`Could not reach ${ingestHost}: ${e instanceof Error ? e.message : String(e)}`));
|
|
1337
|
+
process.exit(1);
|
|
1338
|
+
}
|
|
1339
|
+
console.log("");
|
|
1340
|
+
console.log("To finish signing in, open this URL and approve the request:");
|
|
1341
|
+
console.log("");
|
|
1342
|
+
console.log(" " + chalk7.cyan(start.verificationUri));
|
|
1343
|
+
console.log("");
|
|
1344
|
+
console.log(" Your code: " + chalk7.bold(start.userCode));
|
|
1345
|
+
console.log("");
|
|
1346
|
+
if (opts.browser) {
|
|
1347
|
+
const opened = openUrl(start.verificationUriComplete);
|
|
1348
|
+
if (opened) {
|
|
1349
|
+
console.log(chalk7.dim("Opening your browser\u2026 (if it does not open, use the URL above)"));
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
console.log(chalk7.dim("Waiting for approval\u2026"));
|
|
1353
|
+
const intervalMs = Math.max(2, start.interval || 5) * 1e3;
|
|
1354
|
+
const deadline = Date.now() + (start.expiresIn || 900) * 1e3;
|
|
1355
|
+
while (Date.now() < deadline) {
|
|
1356
|
+
await delay(intervalMs);
|
|
1357
|
+
let token;
|
|
1358
|
+
try {
|
|
1359
|
+
const res = await fetch(`${base}/token`, {
|
|
1360
|
+
method: "POST",
|
|
1361
|
+
headers: { "Content-Type": "application/json", Connection: "close" },
|
|
1362
|
+
body: JSON.stringify({ deviceCode: start.deviceCode })
|
|
1363
|
+
});
|
|
1364
|
+
token = await res.json();
|
|
1365
|
+
} catch {
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1368
|
+
if (token.status === "authorization_pending") continue;
|
|
1369
|
+
if (token.status === "complete" && token.dsn) {
|
|
1370
|
+
writeStoredCredentials({
|
|
1371
|
+
dsn: token.dsn,
|
|
1372
|
+
workspaceId: token.workspaceId,
|
|
1373
|
+
keyId: token.keyId,
|
|
1374
|
+
ingestHost: token.ingestHost ?? ingestHost,
|
|
1375
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1376
|
+
});
|
|
1377
|
+
console.log("");
|
|
1378
|
+
console.log(chalk7.green("\u2714") + " Logged in.");
|
|
1379
|
+
if (token.workspaceId) {
|
|
1380
|
+
console.log(" Workspace: " + chalk7.bold(token.workspaceId));
|
|
1381
|
+
}
|
|
1382
|
+
console.log(chalk7.dim(` Credentials saved to ${credentialsPath()}`));
|
|
1383
|
+
console.log(chalk7.dim(' You can now run "vibgrate scan --push".'));
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
if (token.status === "access_denied") {
|
|
1387
|
+
console.error(chalk7.red("\u2716 Login was denied in the browser."));
|
|
1388
|
+
process.exit(1);
|
|
1389
|
+
}
|
|
1390
|
+
if (token.status === "expired" || token.status === "invalid") {
|
|
1391
|
+
console.error(chalk7.red('\u2716 Login request expired. Run "vibgrate login" again.'));
|
|
1392
|
+
process.exit(1);
|
|
1393
|
+
}
|
|
1394
|
+
if (token.status === "error") {
|
|
1395
|
+
console.error(chalk7.red(`\u2716 ${token.error ?? "Login failed."}`));
|
|
1396
|
+
process.exit(1);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
console.error(chalk7.red('\u2716 Timed out waiting for approval. Run "vibgrate login" again.'));
|
|
1400
|
+
process.exit(1);
|
|
1401
|
+
});
|
|
1402
|
+
|
|
1403
|
+
// src/commands/logout.ts
|
|
1404
|
+
import { Command as Command6 } from "commander";
|
|
1405
|
+
import chalk8 from "chalk";
|
|
1406
|
+
var logoutCommand = new Command6("logout").description("Clear stored Vibgrate login credentials").action(() => {
|
|
1407
|
+
const cleared = clearStoredCredentials();
|
|
1408
|
+
if (cleared) {
|
|
1409
|
+
console.log(chalk8.green("\u2714") + " Logged out. Stored credentials removed.");
|
|
1410
|
+
} else {
|
|
1411
|
+
console.log(chalk8.dim(`No stored credentials found at ${credentialsPath()}.`));
|
|
1412
|
+
}
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1181
1415
|
// src/commands/push.ts
|
|
1182
1416
|
import * as crypto2 from "crypto";
|
|
1183
|
-
import * as
|
|
1184
|
-
import { Command as
|
|
1185
|
-
import
|
|
1417
|
+
import * as path6 from "path";
|
|
1418
|
+
import { Command as Command7 } from "commander";
|
|
1419
|
+
import chalk9 from "chalk";
|
|
1186
1420
|
|
|
1187
1421
|
// src/utils/compact-artifact.ts
|
|
1188
1422
|
import * as zlib from "zlib";
|
|
@@ -1454,25 +1688,25 @@ function parseDsn2(dsn) {
|
|
|
1454
1688
|
function computeHmac(body, secret) {
|
|
1455
1689
|
return crypto2.createHmac("sha256", secret).update(body).digest("base64");
|
|
1456
1690
|
}
|
|
1457
|
-
var pushCommand = new
|
|
1458
|
-
const dsn = opts.dsn
|
|
1691
|
+
var pushCommand = new Command7("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 (${availableRegionIds().join(", ")})`).option("--file <file>", "Scan artifact file", ".vibgrate/scan_result.json").option("--strict", "Fail on upload errors").action(async (opts) => {
|
|
1692
|
+
const dsn = resolveDsn(opts.dsn);
|
|
1459
1693
|
if (!dsn) {
|
|
1460
|
-
console.error(
|
|
1461
|
-
console.error(
|
|
1694
|
+
console.error(chalk9.red("No DSN provided."));
|
|
1695
|
+
console.error(chalk9.dim('Run "vibgrate login", set VIBGRATE_DSN, or use the --dsn flag.'));
|
|
1462
1696
|
if (opts.strict) process.exit(1);
|
|
1463
1697
|
return;
|
|
1464
1698
|
}
|
|
1465
1699
|
const parsed = parseDsn2(dsn);
|
|
1466
1700
|
if (!parsed) {
|
|
1467
|
-
console.error(
|
|
1468
|
-
console.error(
|
|
1701
|
+
console.error(chalk9.red("Invalid DSN format."));
|
|
1702
|
+
console.error(chalk9.dim("Expected: vibgrate+https://<key_id>:<secret>@<host>/<workspace_id>"));
|
|
1469
1703
|
if (opts.strict) process.exit(1);
|
|
1470
1704
|
return;
|
|
1471
1705
|
}
|
|
1472
|
-
const filePath =
|
|
1706
|
+
const filePath = path6.resolve(opts.file);
|
|
1473
1707
|
if (!await pathExists(filePath)) {
|
|
1474
|
-
console.error(
|
|
1475
|
-
console.error(
|
|
1708
|
+
console.error(chalk9.red(`Scan artifact not found: ${filePath}`));
|
|
1709
|
+
console.error(chalk9.dim('Run "vibgrate scan" first.'));
|
|
1476
1710
|
if (opts.strict) process.exit(1);
|
|
1477
1711
|
return;
|
|
1478
1712
|
}
|
|
@@ -1484,64 +1718,61 @@ var pushCommand = new Command5("push").description("Push scan results to Vibgrat
|
|
|
1484
1718
|
try {
|
|
1485
1719
|
host = resolveIngestHost(opts.region);
|
|
1486
1720
|
} catch (e) {
|
|
1487
|
-
console.error(
|
|
1721
|
+
console.error(chalk9.red(e instanceof Error ? e.message : String(e)));
|
|
1488
1722
|
if (opts.strict) process.exit(1);
|
|
1489
1723
|
return;
|
|
1490
1724
|
}
|
|
1491
1725
|
}
|
|
1492
|
-
const url = `${parsed.scheme}://${host}/v1/ingest/scan`;
|
|
1493
1726
|
const originalSize = JSON.stringify(artifact).length;
|
|
1494
1727
|
const compressedSize = body.length;
|
|
1495
1728
|
const ratio = ((1 - compressedSize / originalSize) * 100).toFixed(0);
|
|
1496
|
-
console.log(
|
|
1729
|
+
console.log(chalk9.dim(`Uploading to ${host}... (${(compressedSize / 1024).toFixed(0)} KB, ${ratio}% smaller)`));
|
|
1497
1730
|
try {
|
|
1498
|
-
const response = await
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
// Prevent keep-alive delays on exit
|
|
1507
|
-
},
|
|
1508
|
-
body
|
|
1731
|
+
const { response, host: uploadedHost } = await uploadScanArtifact({
|
|
1732
|
+
scheme: parsed.scheme,
|
|
1733
|
+
host,
|
|
1734
|
+
keyId: parsed.keyId,
|
|
1735
|
+
secret: parsed.secret,
|
|
1736
|
+
body,
|
|
1737
|
+
contentEncoding,
|
|
1738
|
+
timestamp
|
|
1509
1739
|
});
|
|
1740
|
+
host = uploadedHost;
|
|
1510
1741
|
if (!response.ok) {
|
|
1511
1742
|
const text = await response.text();
|
|
1512
1743
|
throw new Error(`HTTP ${response.status}: ${text}`);
|
|
1513
1744
|
}
|
|
1514
1745
|
const result = await response.json();
|
|
1515
|
-
console.log(
|
|
1746
|
+
console.log(chalk9.green("\u2714") + ` Scan queued for processing (${result.ingestId ?? "ok"})`);
|
|
1516
1747
|
console.log();
|
|
1517
|
-
console.log(
|
|
1748
|
+
console.log(chalk9.dim("Processing continues in the background. Results available shortly."));
|
|
1518
1749
|
console.log();
|
|
1519
1750
|
if (result.ingestId) {
|
|
1520
1751
|
const dashHost = dashHostForIngestHost(host);
|
|
1521
1752
|
const reportUrl = `https://${dashHost}/${parsed.workspaceId}/scan/${result.ingestId}`;
|
|
1522
|
-
console.log(
|
|
1753
|
+
console.log(chalk9.dim("View report: ") + chalk9.underline(reportUrl));
|
|
1523
1754
|
}
|
|
1524
1755
|
} catch (e) {
|
|
1525
1756
|
const msg = e instanceof Error ? e.message : String(e);
|
|
1526
|
-
console.error(
|
|
1757
|
+
console.error(chalk9.red(`Upload failed: ${msg}`));
|
|
1527
1758
|
if (opts.strict) process.exit(1);
|
|
1528
1759
|
}
|
|
1529
1760
|
});
|
|
1530
1761
|
|
|
1531
1762
|
// src/commands/update.ts
|
|
1532
1763
|
import { execSync } from "child_process";
|
|
1533
|
-
import * as
|
|
1534
|
-
import { Command as
|
|
1535
|
-
import
|
|
1764
|
+
import * as path8 from "path";
|
|
1765
|
+
import { Command as Command8 } from "commander";
|
|
1766
|
+
import chalk10 from "chalk";
|
|
1536
1767
|
|
|
1537
1768
|
// src/utils/update-check.ts
|
|
1538
1769
|
var import_semver = __toESM(require_semver(), 1);
|
|
1539
|
-
import * as
|
|
1540
|
-
import * as
|
|
1541
|
-
import * as
|
|
1770
|
+
import * as fs2 from "fs/promises";
|
|
1771
|
+
import * as path7 from "path";
|
|
1772
|
+
import * as os2 from "os";
|
|
1542
1773
|
var REGISTRY_URL = "https://registry.npmjs.org/@vibgrate%2fcli/latest";
|
|
1543
|
-
var CACHE_DIR =
|
|
1544
|
-
var CACHE_FILE =
|
|
1774
|
+
var CACHE_DIR = path7.join(os2.homedir(), ".vibgrate");
|
|
1775
|
+
var CACHE_FILE = path7.join(CACHE_DIR, "update-check.json");
|
|
1545
1776
|
var CHECK_INTERVAL_MS = 12 * 60 * 60 * 1e3;
|
|
1546
1777
|
async function checkForUpdate() {
|
|
1547
1778
|
try {
|
|
@@ -1605,7 +1836,7 @@ async function fetchLatestVersion() {
|
|
|
1605
1836
|
}
|
|
1606
1837
|
async function readCache() {
|
|
1607
1838
|
try {
|
|
1608
|
-
const raw = await
|
|
1839
|
+
const raw = await fs2.readFile(CACHE_FILE, "utf-8");
|
|
1609
1840
|
const data = JSON.parse(raw);
|
|
1610
1841
|
if (data.latest && typeof data.checkedAt === "number") return data;
|
|
1611
1842
|
return null;
|
|
@@ -1615,8 +1846,8 @@ async function readCache() {
|
|
|
1615
1846
|
}
|
|
1616
1847
|
async function writeCache(data) {
|
|
1617
1848
|
try {
|
|
1618
|
-
await
|
|
1619
|
-
await
|
|
1849
|
+
await fs2.mkdir(CACHE_DIR, { recursive: true });
|
|
1850
|
+
await fs2.writeFile(CACHE_FILE, JSON.stringify(data), "utf-8");
|
|
1620
1851
|
} catch {
|
|
1621
1852
|
}
|
|
1622
1853
|
}
|
|
@@ -1649,9 +1880,9 @@ function getGlobalUpdateCommand(pm, pkg2, version) {
|
|
|
1649
1880
|
}
|
|
1650
1881
|
}
|
|
1651
1882
|
async function detectPackageManager(cwd) {
|
|
1652
|
-
if (await pathExists(
|
|
1653
|
-
if (await pathExists(
|
|
1654
|
-
if (await pathExists(
|
|
1883
|
+
if (await pathExists(path8.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
1884
|
+
if (await pathExists(path8.join(cwd, "bun.lockb"))) return "bun";
|
|
1885
|
+
if (await pathExists(path8.join(cwd, "yarn.lock"))) return "yarn";
|
|
1655
1886
|
return "npm";
|
|
1656
1887
|
}
|
|
1657
1888
|
function getInstallCommand(pm, pkg2, version, isDev) {
|
|
@@ -1670,7 +1901,7 @@ function getInstallCommand(pm, pkg2, version, isDev) {
|
|
|
1670
1901
|
}
|
|
1671
1902
|
async function isDevDependency(cwd) {
|
|
1672
1903
|
try {
|
|
1673
|
-
const pkgPath =
|
|
1904
|
+
const pkgPath = path8.join(cwd, "package.json");
|
|
1674
1905
|
const raw = await (await import("fs/promises")).readFile(pkgPath, "utf-8");
|
|
1675
1906
|
const pkg2 = JSON.parse(raw);
|
|
1676
1907
|
return Boolean(pkg2.devDependencies?.["@vibgrate/cli"]);
|
|
@@ -1678,22 +1909,22 @@ async function isDevDependency(cwd) {
|
|
|
1678
1909
|
return true;
|
|
1679
1910
|
}
|
|
1680
1911
|
}
|
|
1681
|
-
var updateCommand = new
|
|
1682
|
-
console.log(
|
|
1683
|
-
console.log(
|
|
1912
|
+
var updateCommand = new Command8("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) => {
|
|
1913
|
+
console.log(chalk10.dim(`Current version: ${VERSION}`));
|
|
1914
|
+
console.log(chalk10.dim("Checking npm registry..."));
|
|
1684
1915
|
const latest = await fetchLatestVersion();
|
|
1685
1916
|
if (!latest) {
|
|
1686
|
-
console.error(
|
|
1917
|
+
console.error(chalk10.red("Could not reach the npm registry. Check your network connection."));
|
|
1687
1918
|
process.exit(1);
|
|
1688
1919
|
}
|
|
1689
1920
|
const semver2 = await import("./semver-2FJFIYVN.js");
|
|
1690
1921
|
if (!semver2.gt(latest, VERSION)) {
|
|
1691
|
-
console.log(
|
|
1922
|
+
console.log(chalk10.green("\u2714") + ` You are on the latest version (${VERSION}).`);
|
|
1692
1923
|
return;
|
|
1693
1924
|
}
|
|
1694
|
-
console.log(
|
|
1925
|
+
console.log(chalk10.yellow(`Update available: ${VERSION} \u2192 ${latest}`));
|
|
1695
1926
|
if (opts.check) {
|
|
1696
|
-
console.log(
|
|
1927
|
+
console.log(chalk10.dim('Run "vibgrate update" to install.'));
|
|
1697
1928
|
return;
|
|
1698
1929
|
}
|
|
1699
1930
|
const cwd = process.cwd();
|
|
@@ -1703,26 +1934,26 @@ var updateCommand = new Command6("update").description("Update vibgrate to the l
|
|
|
1703
1934
|
let cmd;
|
|
1704
1935
|
if (isGlobal) {
|
|
1705
1936
|
cmd = getGlobalUpdateCommand(pm, "@vibgrate/cli", latest);
|
|
1706
|
-
console.log(
|
|
1937
|
+
console.log(chalk10.dim(`Updating global installation with ${pm}: ${cmd}`));
|
|
1707
1938
|
} else {
|
|
1708
1939
|
const isDev = await isDevDependency(cwd);
|
|
1709
1940
|
cmd = getInstallCommand(pm, "@vibgrate/cli", latest, isDev);
|
|
1710
|
-
console.log(
|
|
1941
|
+
console.log(chalk10.dim(`Using ${pm}: ${cmd}`));
|
|
1711
1942
|
}
|
|
1712
1943
|
try {
|
|
1713
1944
|
execSync(cmd, { cwd, stdio: "inherit" });
|
|
1714
|
-
console.log(
|
|
1945
|
+
console.log(chalk10.green("\u2714") + ` Updated to @vibgrate/cli@${latest}`);
|
|
1715
1946
|
} catch {
|
|
1716
|
-
console.error(
|
|
1947
|
+
console.error(chalk10.red(`Update failed. Run manually: ${cmd}`));
|
|
1717
1948
|
process.exit(1);
|
|
1718
1949
|
}
|
|
1719
1950
|
});
|
|
1720
1951
|
|
|
1721
1952
|
// src/commands/sbom.ts
|
|
1722
|
-
import * as
|
|
1953
|
+
import * as path9 from "path";
|
|
1723
1954
|
import { randomUUID } from "crypto";
|
|
1724
|
-
import { Command as
|
|
1725
|
-
import
|
|
1955
|
+
import { Command as Command9 } from "commander";
|
|
1956
|
+
import chalk11 from "chalk";
|
|
1726
1957
|
function flattenDependencies(artifact) {
|
|
1727
1958
|
const rows = [];
|
|
1728
1959
|
for (const project of artifact.projects) {
|
|
@@ -1860,50 +2091,50 @@ function formatDeltaText(base, current) {
|
|
|
1860
2091
|
return lines.join("\n");
|
|
1861
2092
|
}
|
|
1862
2093
|
async function readArtifactOrExit(filePath) {
|
|
1863
|
-
const absolutePath =
|
|
2094
|
+
const absolutePath = path9.resolve(filePath);
|
|
1864
2095
|
if (!await pathExists(absolutePath)) {
|
|
1865
|
-
console.error(
|
|
2096
|
+
console.error(chalk11.red(`Artifact not found: ${absolutePath}`));
|
|
1866
2097
|
process.exit(1);
|
|
1867
2098
|
}
|
|
1868
2099
|
return readJsonFile(absolutePath);
|
|
1869
2100
|
}
|
|
1870
|
-
var exportCommand = new
|
|
2101
|
+
var exportCommand = new Command9("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) => {
|
|
1871
2102
|
const artifact = await readArtifactOrExit(opts.in);
|
|
1872
2103
|
const format = opts.format.toLowerCase();
|
|
1873
2104
|
if (format !== "cyclonedx" && format !== "spdx") {
|
|
1874
|
-
console.error(
|
|
2105
|
+
console.error(chalk11.red("Invalid SBOM format. Use cyclonedx or spdx."));
|
|
1875
2106
|
process.exit(1);
|
|
1876
2107
|
}
|
|
1877
2108
|
const sbom = format === "cyclonedx" ? toCycloneDx(artifact) : toSpdx(artifact);
|
|
1878
2109
|
const body = JSON.stringify(sbom, null, 2);
|
|
1879
2110
|
if (opts.out) {
|
|
1880
|
-
await writeTextFile(
|
|
1881
|
-
console.log(
|
|
2111
|
+
await writeTextFile(path9.resolve(opts.out), body);
|
|
2112
|
+
console.log(chalk11.green("\u2714") + ` SBOM written to ${opts.out}`);
|
|
1882
2113
|
} else {
|
|
1883
2114
|
console.log(body);
|
|
1884
2115
|
}
|
|
1885
2116
|
});
|
|
1886
|
-
var deltaCommand = new
|
|
2117
|
+
var deltaCommand = new Command9("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) => {
|
|
1887
2118
|
const base = await readArtifactOrExit(opts.from);
|
|
1888
2119
|
const current = await readArtifactOrExit(opts.to);
|
|
1889
2120
|
const report = formatDeltaText(base, current);
|
|
1890
2121
|
if (opts.out) {
|
|
1891
|
-
await writeTextFile(
|
|
1892
|
-
console.log(
|
|
2122
|
+
await writeTextFile(path9.resolve(opts.out), report);
|
|
2123
|
+
console.log(chalk11.green("\u2714") + ` SBOM delta report written to ${opts.out}`);
|
|
1893
2124
|
} else {
|
|
1894
2125
|
console.log(report);
|
|
1895
2126
|
}
|
|
1896
2127
|
});
|
|
1897
|
-
var sbomCommand = new
|
|
2128
|
+
var sbomCommand = new Command9("sbom").description("SBOM export and delta reports for dependency drift tracking").addCommand(exportCommand).addCommand(deltaCommand);
|
|
1898
2129
|
|
|
1899
2130
|
// src/commands/extract.ts
|
|
1900
|
-
import * as
|
|
1901
|
-
import * as
|
|
1902
|
-
import * as
|
|
1903
|
-
import { existsSync } from "fs";
|
|
1904
|
-
import { spawn, spawnSync } from "child_process";
|
|
1905
|
-
import { Command as
|
|
1906
|
-
import
|
|
2131
|
+
import * as path12 from "path";
|
|
2132
|
+
import * as os4 from "os";
|
|
2133
|
+
import * as fs4 from "fs/promises";
|
|
2134
|
+
import { existsSync, rmSync as rmSync2 } from "fs";
|
|
2135
|
+
import { spawn as spawn3, spawnSync as spawnSync2 } from "child_process";
|
|
2136
|
+
import { Command as Command10 } from "commander";
|
|
2137
|
+
import chalk12 from "chalk";
|
|
1907
2138
|
|
|
1908
2139
|
// src/behavioural/derive.ts
|
|
1909
2140
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -2756,6 +2987,303 @@ function deriveBehaviouralFacts(facts, opts) {
|
|
|
2756
2987
|
return { envelopes, summary: { assertions: unique.length, surfaceFacts, assertedFacts, behaviouralConfidence } };
|
|
2757
2988
|
}
|
|
2758
2989
|
|
|
2990
|
+
// src/utils/vcs.ts
|
|
2991
|
+
import * as path10 from "path";
|
|
2992
|
+
import * as fs3 from "fs/promises";
|
|
2993
|
+
async function detectVcs2(rootDir) {
|
|
2994
|
+
try {
|
|
2995
|
+
return await detectGit(rootDir);
|
|
2996
|
+
} catch {
|
|
2997
|
+
return { type: "unknown" };
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
async function detectGit(rootDir) {
|
|
3001
|
+
const gitDir = await findGitDir(rootDir);
|
|
3002
|
+
if (!gitDir) {
|
|
3003
|
+
return { type: "unknown" };
|
|
3004
|
+
}
|
|
3005
|
+
const headPath = path10.join(gitDir, "HEAD");
|
|
3006
|
+
let headContent;
|
|
3007
|
+
try {
|
|
3008
|
+
headContent = (await fs3.readFile(headPath, "utf8")).trim();
|
|
3009
|
+
} catch {
|
|
3010
|
+
return { type: "unknown" };
|
|
3011
|
+
}
|
|
3012
|
+
let sha;
|
|
3013
|
+
let branch;
|
|
3014
|
+
if (headContent.startsWith("ref: ")) {
|
|
3015
|
+
const refPath = headContent.slice(5);
|
|
3016
|
+
branch = refPath.startsWith("refs/heads/") ? refPath.slice(11) : refPath;
|
|
3017
|
+
sha = await resolveRef(gitDir, refPath);
|
|
3018
|
+
} else if (/^[0-9a-f]{40}$/i.test(headContent)) {
|
|
3019
|
+
sha = headContent;
|
|
3020
|
+
}
|
|
3021
|
+
const remoteUrl = await readGitRemoteUrl(gitDir);
|
|
3022
|
+
return {
|
|
3023
|
+
type: "git",
|
|
3024
|
+
sha: sha ?? void 0,
|
|
3025
|
+
shortSha: sha ? sha.slice(0, 7) : void 0,
|
|
3026
|
+
branch: branch ?? void 0,
|
|
3027
|
+
remoteUrl
|
|
3028
|
+
};
|
|
3029
|
+
}
|
|
3030
|
+
async function findGitDir(startDir) {
|
|
3031
|
+
let dir = path10.resolve(startDir);
|
|
3032
|
+
const root = path10.parse(dir).root;
|
|
3033
|
+
while (dir !== root) {
|
|
3034
|
+
const gitPath = path10.join(dir, ".git");
|
|
3035
|
+
try {
|
|
3036
|
+
const stat4 = await fs3.stat(gitPath);
|
|
3037
|
+
if (stat4.isDirectory()) {
|
|
3038
|
+
return gitPath;
|
|
3039
|
+
}
|
|
3040
|
+
if (stat4.isFile()) {
|
|
3041
|
+
const content = (await fs3.readFile(gitPath, "utf8")).trim();
|
|
3042
|
+
if (content.startsWith("gitdir: ")) {
|
|
3043
|
+
const resolved = path10.resolve(dir, content.slice(8));
|
|
3044
|
+
return resolved;
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
} catch {
|
|
3048
|
+
}
|
|
3049
|
+
dir = path10.dirname(dir);
|
|
3050
|
+
}
|
|
3051
|
+
return null;
|
|
3052
|
+
}
|
|
3053
|
+
async function resolveRef(gitDir, refPath) {
|
|
3054
|
+
const loosePath = path10.join(gitDir, refPath);
|
|
3055
|
+
try {
|
|
3056
|
+
const sha = (await fs3.readFile(loosePath, "utf8")).trim();
|
|
3057
|
+
if (/^[0-9a-f]{40}$/i.test(sha)) {
|
|
3058
|
+
return sha;
|
|
3059
|
+
}
|
|
3060
|
+
} catch {
|
|
3061
|
+
}
|
|
3062
|
+
const packedPath = path10.join(gitDir, "packed-refs");
|
|
3063
|
+
try {
|
|
3064
|
+
const packed = await fs3.readFile(packedPath, "utf8");
|
|
3065
|
+
for (const line of packed.split("\n")) {
|
|
3066
|
+
if (line.startsWith("#") || line.startsWith("^")) continue;
|
|
3067
|
+
const parts = line.trim().split(" ");
|
|
3068
|
+
if (parts.length >= 2 && parts[1] === refPath) {
|
|
3069
|
+
return parts[0];
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
} catch {
|
|
3073
|
+
}
|
|
3074
|
+
return void 0;
|
|
3075
|
+
}
|
|
3076
|
+
async function readGitRemoteUrl(gitDir) {
|
|
3077
|
+
const configPath = await resolveGitConfigPath(gitDir);
|
|
3078
|
+
if (!configPath) return void 0;
|
|
3079
|
+
try {
|
|
3080
|
+
const config = await fs3.readFile(configPath, "utf8");
|
|
3081
|
+
const originBlock = config.match(/\[remote\s+"origin"\]([\s\S]*?)(?=\n\[|$)/);
|
|
3082
|
+
if (!originBlock) return void 0;
|
|
3083
|
+
const urlMatch = originBlock[1]?.match(/\n\s*url\s*=\s*(.+)\s*/);
|
|
3084
|
+
return urlMatch?.[1]?.trim();
|
|
3085
|
+
} catch {
|
|
3086
|
+
return void 0;
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
async function resolveGitConfigPath(gitDir) {
|
|
3090
|
+
const directConfig = path10.join(gitDir, "config");
|
|
3091
|
+
try {
|
|
3092
|
+
const stat4 = await fs3.stat(directConfig);
|
|
3093
|
+
if (stat4.isFile()) return directConfig;
|
|
3094
|
+
} catch {
|
|
3095
|
+
}
|
|
3096
|
+
const commonDirFile = path10.join(gitDir, "commondir");
|
|
3097
|
+
try {
|
|
3098
|
+
const commonDir = (await fs3.readFile(commonDirFile, "utf8")).trim();
|
|
3099
|
+
if (!commonDir) return void 0;
|
|
3100
|
+
const resolvedCommonDir = path10.resolve(gitDir, commonDir);
|
|
3101
|
+
const commonConfig = path10.join(resolvedCommonDir, "config");
|
|
3102
|
+
const stat4 = await fs3.stat(commonConfig);
|
|
3103
|
+
if (stat4.isFile()) return commonConfig;
|
|
3104
|
+
} catch {
|
|
3105
|
+
return void 0;
|
|
3106
|
+
}
|
|
3107
|
+
return void 0;
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
// src/utils/clone.ts
|
|
3111
|
+
import * as path11 from "path";
|
|
3112
|
+
import * as os3 from "os";
|
|
3113
|
+
import { mkdtemp, rm } from "fs/promises";
|
|
3114
|
+
import { spawn as spawn2, spawnSync } from "child_process";
|
|
3115
|
+
var SCP_LIKE = /^[A-Za-z0-9._-]+@[A-Za-z0-9._-]+:.+$/;
|
|
3116
|
+
function looksLikeRepoUrl(arg) {
|
|
3117
|
+
if (!arg) return false;
|
|
3118
|
+
if (/^(https?|ssh|git):\/\//i.test(arg)) return true;
|
|
3119
|
+
if (SCP_LIKE.test(arg)) return true;
|
|
3120
|
+
return false;
|
|
3121
|
+
}
|
|
3122
|
+
function redactRemoteUrl(url) {
|
|
3123
|
+
if (!url) return url;
|
|
3124
|
+
if (SCP_LIKE.test(url) && !url.includes("://")) return url;
|
|
3125
|
+
try {
|
|
3126
|
+
const parsed = new URL(url);
|
|
3127
|
+
if (parsed.username || parsed.password) {
|
|
3128
|
+
parsed.username = "";
|
|
3129
|
+
parsed.password = "";
|
|
3130
|
+
}
|
|
3131
|
+
return parsed.toString();
|
|
3132
|
+
} catch {
|
|
3133
|
+
return url.replace(/^([a-z]+:\/\/)[^/@]+@/i, "$1");
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
var CloneError = class extends Error {
|
|
3137
|
+
constructor(message) {
|
|
3138
|
+
super(message);
|
|
3139
|
+
this.name = "CloneError";
|
|
3140
|
+
}
|
|
3141
|
+
};
|
|
3142
|
+
function isGitAvailable() {
|
|
3143
|
+
const result = spawnSync("git", ["--version"], { stdio: "ignore" });
|
|
3144
|
+
return !result.error && result.status === 0;
|
|
3145
|
+
}
|
|
3146
|
+
function gitInstallHint() {
|
|
3147
|
+
switch (process.platform) {
|
|
3148
|
+
case "darwin":
|
|
3149
|
+
return "Install git with: brew install git (or xcode-select --install).";
|
|
3150
|
+
case "win32":
|
|
3151
|
+
return "Install git with: winget install Git.Git";
|
|
3152
|
+
default:
|
|
3153
|
+
return "Install git via your distro package manager (e.g. apt install git).";
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
function runGit(args, opts) {
|
|
3157
|
+
return new Promise((resolve10, reject) => {
|
|
3158
|
+
const child = spawn2("git", args, {
|
|
3159
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
3160
|
+
// Never prompt for credentials on a private/forbidden repo — fail fast.
|
|
3161
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_ASKPASS: "echo" }
|
|
3162
|
+
});
|
|
3163
|
+
let stderr = "";
|
|
3164
|
+
let killed = false;
|
|
3165
|
+
const timer = setTimeout(() => {
|
|
3166
|
+
killed = true;
|
|
3167
|
+
child.kill("SIGKILL");
|
|
3168
|
+
}, opts.timeoutMs);
|
|
3169
|
+
child.stderr.on("data", (d) => {
|
|
3170
|
+
if (stderr.length < 8192) stderr += scrubCredentials(String(d));
|
|
3171
|
+
});
|
|
3172
|
+
child.on("error", (err) => {
|
|
3173
|
+
clearTimeout(timer);
|
|
3174
|
+
reject(new CloneError(`Failed to run git: ${err.message}`));
|
|
3175
|
+
});
|
|
3176
|
+
child.on("close", (code) => {
|
|
3177
|
+
clearTimeout(timer);
|
|
3178
|
+
if (killed) {
|
|
3179
|
+
reject(new CloneError(`git clone timed out for ${opts.redacted}`));
|
|
3180
|
+
return;
|
|
3181
|
+
}
|
|
3182
|
+
resolve10({ code: code ?? 0, stderr: stderr.trim() });
|
|
3183
|
+
});
|
|
3184
|
+
});
|
|
3185
|
+
}
|
|
3186
|
+
function scrubCredentials(text) {
|
|
3187
|
+
return text.replace(/([a-z]+:\/\/)[^/\s@]+@/gi, "$1");
|
|
3188
|
+
}
|
|
3189
|
+
async function cloneRepo(url, opts) {
|
|
3190
|
+
const redacted = redactRemoteUrl(url);
|
|
3191
|
+
if (!looksLikeRepoUrl(url)) {
|
|
3192
|
+
throw new CloneError(
|
|
3193
|
+
`Not a supported repository URL: ${redacted}. Use an http(s), ssh, git://, or scp-like (git@host:owner/repo.git) URL.`
|
|
3194
|
+
);
|
|
3195
|
+
}
|
|
3196
|
+
if (/^file:\/\//i.test(url)) {
|
|
3197
|
+
throw new CloneError("file:// URLs are not allowed for remote clone.");
|
|
3198
|
+
}
|
|
3199
|
+
if (!isGitAvailable()) {
|
|
3200
|
+
throw new CloneError(`git is required to clone ${redacted} but was not found on PATH. ${gitInstallHint()}`);
|
|
3201
|
+
}
|
|
3202
|
+
const dir = await mkdtemp(path11.join(os3.tmpdir(), "vibgrate-hcs-clone-"));
|
|
3203
|
+
const cleanup = async () => {
|
|
3204
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {
|
|
3205
|
+
});
|
|
3206
|
+
};
|
|
3207
|
+
try {
|
|
3208
|
+
if (opts.verbose) process.stderr.write(`Cloning ${redacted} \u2026
|
|
3209
|
+
`);
|
|
3210
|
+
const clone = await runGit(
|
|
3211
|
+
["clone", "--depth", "1", "--quiet", "--", url, dir],
|
|
3212
|
+
{ timeoutMs: opts.timeoutMs, redacted }
|
|
3213
|
+
);
|
|
3214
|
+
if (clone.code !== 0) {
|
|
3215
|
+
throw new CloneError(`git clone failed for ${redacted}: ${clone.stderr || `exit ${clone.code}`}`);
|
|
3216
|
+
}
|
|
3217
|
+
if (opts.ref) {
|
|
3218
|
+
if (opts.verbose) process.stderr.write(`Checking out ${opts.ref} \u2026
|
|
3219
|
+
`);
|
|
3220
|
+
const fetch2 = await runGit(
|
|
3221
|
+
["-C", dir, "fetch", "--depth", "1", "--quiet", "origin", opts.ref],
|
|
3222
|
+
{ timeoutMs: opts.timeoutMs, redacted }
|
|
3223
|
+
);
|
|
3224
|
+
const checkoutTarget = fetch2.code === 0 ? "FETCH_HEAD" : opts.ref;
|
|
3225
|
+
const checkout = await runGit(
|
|
3226
|
+
["-C", dir, "checkout", "--quiet", "--detach", checkoutTarget],
|
|
3227
|
+
{ timeoutMs: opts.timeoutMs, redacted }
|
|
3228
|
+
);
|
|
3229
|
+
if (checkout.code !== 0) {
|
|
3230
|
+
throw new CloneError(
|
|
3231
|
+
`Could not check out ref "${opts.ref}" from ${redacted}: ${checkout.stderr || `exit ${checkout.code}`}`
|
|
3232
|
+
);
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
return { dir, cleanup };
|
|
3236
|
+
} catch (err) {
|
|
3237
|
+
await cleanup();
|
|
3238
|
+
throw err instanceof CloneError ? err : new CloneError(String(err));
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
|
|
3242
|
+
// src/utils/scan-manifest.ts
|
|
3243
|
+
import * as crypto3 from "crypto";
|
|
3244
|
+
function computeFactContentHash(factLines) {
|
|
3245
|
+
const hash = crypto3.createHash("sha256");
|
|
3246
|
+
for (const line of factLines) {
|
|
3247
|
+
hash.update(line);
|
|
3248
|
+
hash.update("\n");
|
|
3249
|
+
}
|
|
3250
|
+
return `sha256:${hash.digest("hex")}`;
|
|
3251
|
+
}
|
|
3252
|
+
function deriveRepoName(remoteUrl) {
|
|
3253
|
+
if (!remoteUrl) return void 0;
|
|
3254
|
+
const withoutScheme = remoteUrl.replace(/^[a-z]+:\/\//i, "").replace(/^[^/@]+@/, "");
|
|
3255
|
+
const pathPart = withoutScheme.replace(/^[^/:]+[/:]/, "");
|
|
3256
|
+
const segments = pathPart.split("/").filter(Boolean);
|
|
3257
|
+
if (segments.length === 0) return void 0;
|
|
3258
|
+
const repo = segments[segments.length - 1].replace(/\.git$/i, "");
|
|
3259
|
+
const owner = segments.length >= 2 ? segments[segments.length - 2] : void 0;
|
|
3260
|
+
return owner ? `${owner}/${repo}` : repo;
|
|
3261
|
+
}
|
|
3262
|
+
function buildScanManifest(input) {
|
|
3263
|
+
return {
|
|
3264
|
+
factType: "ScanManifest",
|
|
3265
|
+
manifestVersion: "1.0",
|
|
3266
|
+
emittedAt: input.emittedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
3267
|
+
repository: {
|
|
3268
|
+
remoteUrl: input.remoteUrl,
|
|
3269
|
+
name: input.repositoryName ?? deriveRepoName(input.remoteUrl)
|
|
3270
|
+
},
|
|
3271
|
+
vcs: {
|
|
3272
|
+
sha: input.sha,
|
|
3273
|
+
shortSha: input.shortSha,
|
|
3274
|
+
branch: input.branch,
|
|
3275
|
+
ref: input.ref
|
|
3276
|
+
},
|
|
3277
|
+
source: input.source,
|
|
3278
|
+
languages: [...input.languages].sort(),
|
|
3279
|
+
scanId: input.scanId,
|
|
3280
|
+
applicationId: input.applicationId,
|
|
3281
|
+
vibgrateVersion: input.vibgrateVersion,
|
|
3282
|
+
factCount: input.factLines.length,
|
|
3283
|
+
contentHash: computeFactContentHash(input.factLines)
|
|
3284
|
+
};
|
|
3285
|
+
}
|
|
3286
|
+
|
|
2759
3287
|
// src/commands/extract.ts
|
|
2760
3288
|
var EXIT_SUCCESS = 0;
|
|
2761
3289
|
var EXIT_SCHEMA_FAILURE = 1;
|
|
@@ -2840,14 +3368,14 @@ async function detectLanguages(rootDir, includeTests) {
|
|
|
2840
3368
|
async function walk(dir, relBase) {
|
|
2841
3369
|
let entries;
|
|
2842
3370
|
try {
|
|
2843
|
-
entries = await
|
|
3371
|
+
entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
2844
3372
|
} catch {
|
|
2845
3373
|
return;
|
|
2846
3374
|
}
|
|
2847
3375
|
for (const entry of entries) {
|
|
2848
3376
|
if (entry.name.startsWith(".")) continue;
|
|
2849
|
-
const absPath =
|
|
2850
|
-
const relPath =
|
|
3377
|
+
const absPath = path12.join(dir, entry.name);
|
|
3378
|
+
const relPath = path12.join(relBase, entry.name);
|
|
2851
3379
|
if (entry.isDirectory()) {
|
|
2852
3380
|
if (!SKIP_DIRS.has(entry.name)) {
|
|
2853
3381
|
await walk(absPath, relPath);
|
|
@@ -2856,7 +3384,7 @@ async function detectLanguages(rootDir, includeTests) {
|
|
|
2856
3384
|
}
|
|
2857
3385
|
if (!entry.isFile()) continue;
|
|
2858
3386
|
if (!includeTests && isTestPath(relPath)) continue;
|
|
2859
|
-
const ext =
|
|
3387
|
+
const ext = path12.extname(entry.name).toLowerCase();
|
|
2860
3388
|
for (const [lang, exts] of Object.entries(LANGUAGE_EXTENSIONS)) {
|
|
2861
3389
|
if (exts.has(ext)) {
|
|
2862
3390
|
counts.set(lang, (counts.get(lang) ?? 0) + 1);
|
|
@@ -2882,6 +3410,15 @@ function validateFactLine(line) {
|
|
|
2882
3410
|
}
|
|
2883
3411
|
return { valid: true };
|
|
2884
3412
|
}
|
|
3413
|
+
if (obj.factType === "ScanManifest") {
|
|
3414
|
+
if (typeof obj.manifestVersion !== "string") {
|
|
3415
|
+
return { valid: false, error: "ScanManifest missing manifestVersion" };
|
|
3416
|
+
}
|
|
3417
|
+
if (typeof obj.emittedAt !== "string") {
|
|
3418
|
+
return { valid: false, error: "ScanManifest missing emittedAt" };
|
|
3419
|
+
}
|
|
3420
|
+
return { valid: true };
|
|
3421
|
+
}
|
|
2885
3422
|
if (typeof obj.factId !== "string" || !obj.factId) {
|
|
2886
3423
|
return { valid: false, error: "Missing or invalid factId" };
|
|
2887
3424
|
}
|
|
@@ -2923,7 +3460,7 @@ function splitHcsOutput(lines) {
|
|
|
2923
3460
|
continue;
|
|
2924
3461
|
}
|
|
2925
3462
|
const obj = parsed;
|
|
2926
|
-
if (obj.factType === "Models" || obj.factType === "References") {
|
|
3463
|
+
if (obj.factType === "Models" || obj.factType === "References" || obj.factType === "ScanManifest") {
|
|
2927
3464
|
preamble.push(line);
|
|
2928
3465
|
} else {
|
|
2929
3466
|
facts.push(line);
|
|
@@ -2932,10 +3469,10 @@ function splitHcsOutput(lines) {
|
|
|
2932
3469
|
return { preamble, facts };
|
|
2933
3470
|
}
|
|
2934
3471
|
function resolveHcsWorkerBin() {
|
|
2935
|
-
const base = import.meta.dirname ??
|
|
2936
|
-
const bundledPath =
|
|
3472
|
+
const base = import.meta.dirname ?? path12.dirname(new URL(import.meta.url).pathname);
|
|
3473
|
+
const bundledPath = path12.resolve(base, "..", "dist", "hcs-worker.js");
|
|
2937
3474
|
if (existsSync(bundledPath)) return bundledPath;
|
|
2938
|
-
const siblingPath =
|
|
3475
|
+
const siblingPath = path12.resolve(base, "hcs-worker.js");
|
|
2939
3476
|
if (existsSync(siblingPath)) return siblingPath;
|
|
2940
3477
|
try {
|
|
2941
3478
|
const resolved = import.meta.resolve("@vibgrate/hcs-node-worker");
|
|
@@ -2943,9 +3480,9 @@ function resolveHcsWorkerBin() {
|
|
|
2943
3480
|
if (existsSync(resolvedPath)) return resolvedPath;
|
|
2944
3481
|
} catch {
|
|
2945
3482
|
}
|
|
2946
|
-
const monorepoPath =
|
|
3483
|
+
const monorepoPath = path12.resolve(base, "..", "..", "..", "vibgrate-hcs", "node", "dist", "main.js");
|
|
2947
3484
|
if (existsSync(monorepoPath)) return monorepoPath;
|
|
2948
|
-
const devPath =
|
|
3485
|
+
const devPath = path12.resolve(base, "..", "..", "..", "vibgrate-hcs", "node", "src", "main.ts");
|
|
2949
3486
|
if (existsSync(devPath)) return devPath;
|
|
2950
3487
|
throw new Error(
|
|
2951
3488
|
'Cannot locate HCS worker. Run "pnpm build" in the CLI package to bundle the worker into dist/.'
|
|
@@ -3024,7 +3561,7 @@ var NATIVE_RUNTIME_REQUIREMENTS = {
|
|
|
3024
3561
|
function commandExistsOnPath(command) {
|
|
3025
3562
|
const probeArgs = process.platform === "win32" ? ["/c", command, "--version"] : ["--version"];
|
|
3026
3563
|
const probeCmd = process.platform === "win32" ? "cmd" : command;
|
|
3027
|
-
const result =
|
|
3564
|
+
const result = spawnSync2(probeCmd, probeArgs, { stdio: "ignore" });
|
|
3028
3565
|
return !result.error && result.status === 0;
|
|
3029
3566
|
}
|
|
3030
3567
|
function buildNativeInstallHint(language) {
|
|
@@ -3048,12 +3585,12 @@ async function runNodeWorker(rootDir, language, opts) {
|
|
|
3048
3585
|
args.push("--cobol-copybook-paths", opts.copybookPaths);
|
|
3049
3586
|
}
|
|
3050
3587
|
}
|
|
3051
|
-
return new Promise((
|
|
3588
|
+
return new Promise((resolve10) => {
|
|
3052
3589
|
const facts = [];
|
|
3053
3590
|
const errors = [];
|
|
3054
3591
|
let stdoutBuf = "";
|
|
3055
3592
|
let killed = false;
|
|
3056
|
-
const child =
|
|
3593
|
+
const child = spawn3("node", ["--enable-source-maps", workerBin, ...args], {
|
|
3057
3594
|
cwd: rootDir,
|
|
3058
3595
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3059
3596
|
env: { ...process.env, NODE_OPTIONS: "--max-old-space-size=4096" }
|
|
@@ -3085,7 +3622,7 @@ async function runNodeWorker(rootDir, language, opts) {
|
|
|
3085
3622
|
continue;
|
|
3086
3623
|
}
|
|
3087
3624
|
if (opts.verbose) {
|
|
3088
|
-
process.stderr.write(
|
|
3625
|
+
process.stderr.write(chalk12.dim(`[${language}] ${trimmed}
|
|
3089
3626
|
`));
|
|
3090
3627
|
}
|
|
3091
3628
|
if (trimmed.startsWith("[error]")) {
|
|
@@ -3099,15 +3636,15 @@ async function runNodeWorker(rootDir, language, opts) {
|
|
|
3099
3636
|
facts.push(stdoutBuf.trim());
|
|
3100
3637
|
}
|
|
3101
3638
|
if (killed) {
|
|
3102
|
-
|
|
3639
|
+
resolve10({ language, facts, errors: ["Worker killed: timeout exceeded"], exitCode: EXIT_TIMEOUT });
|
|
3103
3640
|
} else {
|
|
3104
|
-
|
|
3641
|
+
resolve10({ language, facts, errors, exitCode: code ?? 0 });
|
|
3105
3642
|
}
|
|
3106
3643
|
});
|
|
3107
3644
|
child.on("error", (err) => {
|
|
3108
3645
|
clearTimeout(timer);
|
|
3109
3646
|
errors.push(`Failed to spawn worker: ${err.message}`);
|
|
3110
|
-
|
|
3647
|
+
resolve10({ language, facts, errors, exitCode: EXIT_PARSE_FAILURE });
|
|
3111
3648
|
});
|
|
3112
3649
|
});
|
|
3113
3650
|
}
|
|
@@ -3127,7 +3664,7 @@ function resolveDotnetPublishedWorker(workersDir) {
|
|
|
3127
3664
|
};
|
|
3128
3665
|
const candidates = candidatesByPlatform[process.platform] ?? [];
|
|
3129
3666
|
for (const filename of candidates) {
|
|
3130
|
-
const fullPath =
|
|
3667
|
+
const fullPath = path12.join(workersDir, filename);
|
|
3131
3668
|
if (existsSync(fullPath)) {
|
|
3132
3669
|
return fullPath;
|
|
3133
3670
|
}
|
|
@@ -3135,30 +3672,30 @@ function resolveDotnetPublishedWorker(workersDir) {
|
|
|
3135
3672
|
return null;
|
|
3136
3673
|
}
|
|
3137
3674
|
function resolveNativeWorker(language, projectDir) {
|
|
3138
|
-
const base = import.meta.dirname ??
|
|
3139
|
-
const hcsFromBundle =
|
|
3140
|
-
const hcsFromSrc =
|
|
3675
|
+
const base = import.meta.dirname ?? path12.dirname(new URL(import.meta.url).pathname);
|
|
3676
|
+
const hcsFromBundle = path12.resolve(base, "..", "..", "vibgrate-hcs");
|
|
3677
|
+
const hcsFromSrc = path12.resolve(base, "..", "..", "..", "vibgrate-hcs");
|
|
3141
3678
|
const hcsRoot = existsSync(hcsFromBundle) ? hcsFromBundle : hcsFromSrc;
|
|
3142
|
-
const workersDir =
|
|
3679
|
+
const workersDir = path12.resolve(base, "workers");
|
|
3143
3680
|
switch (language) {
|
|
3144
3681
|
case "go": {
|
|
3145
|
-
const bin =
|
|
3682
|
+
const bin = path12.join(workersDir, process.platform === "win32" ? "vibgrate-hcs-go.exe" : "vibgrate-hcs-go");
|
|
3146
3683
|
if (existsSync(bin)) {
|
|
3147
3684
|
return { cmd: bin, args: ["--project", projectDir, "--output", "ndjson"] };
|
|
3148
3685
|
}
|
|
3149
|
-
const src =
|
|
3150
|
-
if (existsSync(
|
|
3686
|
+
const src = path12.join(hcsRoot, "go");
|
|
3687
|
+
if (existsSync(path12.join(src, "main.go"))) {
|
|
3151
3688
|
return { cmd: "go", args: ["run", ".", "--project", projectDir, "--output", "ndjson"], cwd: src };
|
|
3152
3689
|
}
|
|
3153
3690
|
return null;
|
|
3154
3691
|
}
|
|
3155
3692
|
case "python": {
|
|
3156
|
-
const bin =
|
|
3693
|
+
const bin = path12.join(workersDir, "vibgrate-hcs-python");
|
|
3157
3694
|
if (existsSync(bin)) {
|
|
3158
3695
|
return { cmd: bin, args: ["--project", projectDir, "--output", "ndjson"] };
|
|
3159
3696
|
}
|
|
3160
|
-
const src =
|
|
3161
|
-
if (existsSync(
|
|
3697
|
+
const src = path12.join(hcsRoot, "python");
|
|
3698
|
+
if (existsSync(path12.join(src, "pyproject.toml"))) {
|
|
3162
3699
|
return {
|
|
3163
3700
|
cmd: "python3",
|
|
3164
3701
|
args: ["-m", "vibgrate_hcs_python.main", "--project", projectDir, "--output", "ndjson"],
|
|
@@ -3168,13 +3705,13 @@ function resolveNativeWorker(language, projectDir) {
|
|
|
3168
3705
|
return null;
|
|
3169
3706
|
}
|
|
3170
3707
|
case "java": {
|
|
3171
|
-
const jar =
|
|
3708
|
+
const jar = path12.join(workersDir, "vibgrate-hcs-jvm.jar");
|
|
3172
3709
|
if (existsSync(jar)) {
|
|
3173
3710
|
return { cmd: "java", args: ["-jar", jar, "--project", projectDir, "--output", "ndjson"] };
|
|
3174
3711
|
}
|
|
3175
|
-
const src =
|
|
3176
|
-
if (existsSync(
|
|
3177
|
-
const gradlew =
|
|
3712
|
+
const src = path12.join(hcsRoot, "jvm");
|
|
3713
|
+
if (existsSync(path12.join(src, "build.gradle.kts"))) {
|
|
3714
|
+
const gradlew = path12.join(src, process.platform === "win32" ? "gradlew.bat" : "gradlew");
|
|
3178
3715
|
const launcher = existsSync(gradlew) ? gradlew : "gradle";
|
|
3179
3716
|
return {
|
|
3180
3717
|
cmd: launcher,
|
|
@@ -3190,11 +3727,11 @@ function resolveNativeWorker(language, projectDir) {
|
|
|
3190
3727
|
if (publishedWorker) {
|
|
3191
3728
|
return { cmd: publishedWorker, args: ["--project", projectDir, "--output", "ndjson"] };
|
|
3192
3729
|
}
|
|
3193
|
-
const dll =
|
|
3730
|
+
const dll = path12.join(workersDir, "VibgrateHcsWorker.dll");
|
|
3194
3731
|
if (existsSync(dll)) {
|
|
3195
3732
|
return { cmd: "dotnet", args: [dll, "--project", projectDir, "--output", "ndjson"] };
|
|
3196
3733
|
}
|
|
3197
|
-
const csproj =
|
|
3734
|
+
const csproj = path12.join(hcsRoot, "dotnet", "src", "VibgrateHcsWorker", "VibgrateHcsWorker.csproj");
|
|
3198
3735
|
if (existsSync(csproj)) {
|
|
3199
3736
|
return {
|
|
3200
3737
|
cmd: "dotnet",
|
|
@@ -3220,19 +3757,19 @@ async function runNativeWorker(rootDir, language, opts) {
|
|
|
3220
3757
|
exitCode: EXIT_PARSE_FAILURE
|
|
3221
3758
|
};
|
|
3222
3759
|
}
|
|
3223
|
-
return new Promise((
|
|
3760
|
+
return new Promise((resolve10) => {
|
|
3224
3761
|
const facts = [];
|
|
3225
3762
|
const errors = [];
|
|
3226
3763
|
let stdoutBuf = "";
|
|
3227
3764
|
let killed = false;
|
|
3228
3765
|
if (opts.verbose) {
|
|
3229
|
-
process.stderr.write(
|
|
3766
|
+
process.stderr.write(chalk12.dim(`[${language}] Spawning: ${spec.cmd} ${spec.args.join(" ")}
|
|
3230
3767
|
`));
|
|
3231
3768
|
}
|
|
3232
3769
|
const runtimeReq = NATIVE_RUNTIME_REQUIREMENTS[language];
|
|
3233
3770
|
const usesPathCommand = runtimeReq && spec.cmd === runtimeReq.command;
|
|
3234
3771
|
if (usesPathCommand && !commandExistsOnPath(spec.cmd)) {
|
|
3235
|
-
|
|
3772
|
+
resolve10({
|
|
3236
3773
|
language,
|
|
3237
3774
|
facts: [],
|
|
3238
3775
|
errors: [
|
|
@@ -3244,7 +3781,7 @@ async function runNativeWorker(rootDir, language, opts) {
|
|
|
3244
3781
|
});
|
|
3245
3782
|
return;
|
|
3246
3783
|
}
|
|
3247
|
-
const child =
|
|
3784
|
+
const child = spawn3(spec.cmd, spec.args, {
|
|
3248
3785
|
cwd: spec.cwd ?? rootDir,
|
|
3249
3786
|
stdio: ["ignore", "pipe", "pipe"]
|
|
3250
3787
|
});
|
|
@@ -3267,7 +3804,7 @@ async function runNativeWorker(rootDir, language, opts) {
|
|
|
3267
3804
|
const trimmed = line.trim();
|
|
3268
3805
|
if (!trimmed) continue;
|
|
3269
3806
|
if (opts.verbose) {
|
|
3270
|
-
process.stderr.write(
|
|
3807
|
+
process.stderr.write(chalk12.dim(`[${language}] ${trimmed}
|
|
3271
3808
|
`));
|
|
3272
3809
|
}
|
|
3273
3810
|
if (trimmed.startsWith("[error]")) {
|
|
@@ -3279,9 +3816,9 @@ async function runNativeWorker(rootDir, language, opts) {
|
|
|
3279
3816
|
clearTimeout(timer);
|
|
3280
3817
|
if (stdoutBuf.trim()) facts.push(stdoutBuf.trim());
|
|
3281
3818
|
if (killed) {
|
|
3282
|
-
|
|
3819
|
+
resolve10({ language, facts, errors: ["Worker killed: timeout exceeded"], exitCode: EXIT_TIMEOUT });
|
|
3283
3820
|
} else {
|
|
3284
|
-
|
|
3821
|
+
resolve10({ language, facts, errors, exitCode: code ?? 0 });
|
|
3285
3822
|
}
|
|
3286
3823
|
});
|
|
3287
3824
|
child.on("error", (err) => {
|
|
@@ -3290,21 +3827,21 @@ async function runNativeWorker(rootDir, language, opts) {
|
|
|
3290
3827
|
errors.push(
|
|
3291
3828
|
isNotFound ? `[error] '${spec.cmd}' not found. ${buildNativeInstallHint(language)}` : `[error] Failed to spawn native worker: ${err.message}`
|
|
3292
3829
|
);
|
|
3293
|
-
|
|
3830
|
+
resolve10({ language, facts, errors, exitCode: EXIT_PARSE_FAILURE });
|
|
3294
3831
|
});
|
|
3295
3832
|
});
|
|
3296
3833
|
}
|
|
3297
3834
|
async function pushFacts(facts, dsn, verbose) {
|
|
3298
3835
|
const parsed = parseDsn2(dsn);
|
|
3299
3836
|
if (!parsed) {
|
|
3300
|
-
process.stderr.write(
|
|
3301
|
-
process.stderr.write(
|
|
3837
|
+
process.stderr.write(chalk12.red("Invalid DSN format.\n"));
|
|
3838
|
+
process.stderr.write(chalk12.dim("Expected: vibgrate+https://<key_id>:<secret>@<host>/<workspace_id>\n"));
|
|
3302
3839
|
return false;
|
|
3303
3840
|
}
|
|
3304
3841
|
const body = facts.join("\n") + "\n";
|
|
3305
3842
|
const url = `${parsed.scheme}://${parsed.host}/v1/ingest/hcs`;
|
|
3306
3843
|
if (verbose) {
|
|
3307
|
-
process.stderr.write(
|
|
3844
|
+
process.stderr.write(chalk12.dim(`Pushing ${facts.length} facts to ${parsed.host}...
|
|
3308
3845
|
`));
|
|
3309
3846
|
}
|
|
3310
3847
|
try {
|
|
@@ -3322,23 +3859,23 @@ async function pushFacts(facts, dsn, verbose) {
|
|
|
3322
3859
|
});
|
|
3323
3860
|
if (!response.ok) {
|
|
3324
3861
|
const text = await response.text().catch(() => "");
|
|
3325
|
-
process.stderr.write(
|
|
3862
|
+
process.stderr.write(chalk12.red(`Push failed: HTTP ${response.status} ${text}
|
|
3326
3863
|
`));
|
|
3327
3864
|
return false;
|
|
3328
3865
|
}
|
|
3329
3866
|
if (verbose) {
|
|
3330
|
-
process.stderr.write(
|
|
3867
|
+
process.stderr.write(chalk12.green(`\u2714 Pushed ${facts.length} facts successfully
|
|
3331
3868
|
`));
|
|
3332
3869
|
}
|
|
3333
3870
|
return true;
|
|
3334
3871
|
} catch (err) {
|
|
3335
|
-
process.stderr.write(
|
|
3872
|
+
process.stderr.write(chalk12.red(`Push failed: ${err instanceof Error ? err.message : String(err)}
|
|
3336
3873
|
`));
|
|
3337
3874
|
return false;
|
|
3338
3875
|
}
|
|
3339
3876
|
}
|
|
3340
3877
|
async function loadFeedback(filePath) {
|
|
3341
|
-
const content = await
|
|
3878
|
+
const content = await fs4.readFile(filePath, "utf-8");
|
|
3342
3879
|
const lines = content.split("\n").filter((l) => l.trim());
|
|
3343
3880
|
for (const line of lines) {
|
|
3344
3881
|
try {
|
|
@@ -3399,21 +3936,21 @@ var ProgressTracker = class {
|
|
|
3399
3936
|
const parts = [];
|
|
3400
3937
|
for (const [lang, st] of this.langStatus) {
|
|
3401
3938
|
if (st.done) {
|
|
3402
|
-
parts.push(
|
|
3939
|
+
parts.push(chalk12.green(`${lang} \u2713`));
|
|
3403
3940
|
} else if (st.phase === "waiting") {
|
|
3404
|
-
parts.push(
|
|
3941
|
+
parts.push(chalk12.dim(`${lang} \xB7`));
|
|
3405
3942
|
} else if (st.phase === "discovering") {
|
|
3406
|
-
parts.push(
|
|
3943
|
+
parts.push(chalk12.yellow(`${lang} \u2026`));
|
|
3407
3944
|
} else if (st.phase === "scanning" && st.fileCount > 0) {
|
|
3408
3945
|
const pct = Math.round(st.fileIndex / st.fileCount * 100);
|
|
3409
|
-
parts.push(
|
|
3946
|
+
parts.push(chalk12.cyan(`${lang} ${st.fileIndex}/${st.fileCount} ${pct}%`));
|
|
3410
3947
|
} else if (st.phase === "extracting") {
|
|
3411
|
-
parts.push(
|
|
3948
|
+
parts.push(chalk12.cyan(`${lang} extracting`));
|
|
3412
3949
|
} else {
|
|
3413
|
-
parts.push(
|
|
3950
|
+
parts.push(chalk12.dim(`${lang} ${st.phase}`));
|
|
3414
3951
|
}
|
|
3415
3952
|
}
|
|
3416
|
-
const line = ` ${parts.join(" ")} ${
|
|
3953
|
+
const line = ` ${parts.join(" ")} ${chalk12.dim(`${this.totalFacts} facts`)}`;
|
|
3417
3954
|
const padding = Math.max(0, this.lastLineLen - line.length);
|
|
3418
3955
|
process.stderr.write(`\r${line}${" ".repeat(padding)}`);
|
|
3419
3956
|
this.lastLineLen = line.length;
|
|
@@ -3425,58 +3962,95 @@ var ProgressTracker = class {
|
|
|
3425
3962
|
}
|
|
3426
3963
|
}
|
|
3427
3964
|
};
|
|
3428
|
-
var extractCommand = new
|
|
3429
|
-
const
|
|
3430
|
-
|
|
3431
|
-
|
|
3965
|
+
var extractCommand = new Command10("extract").description("Analyze source code and emit validated HCS facts (NDJSON)").argument("[path-or-url]", "Path to source directory, or a public repository URL to clone", ".").option("--repo-url <url>", "Public repository URL to clone and analyze (alternative to passing a URL positionally)").option("--ref <ref>", "Branch, tag, or commit SHA to check out when cloning a repository URL").option("--keep-clone", "Do not delete the temporary clone after extraction (prints its path)").option("--clone-timeout-mins <mins>", "Clone/fetch timeout in minutes", "10").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("--derive", "Derive behavioural spec facts (BehaviouralAssertion/BehaviouralSpec) from extracted facts").option("--scan-id <id>", 'Scan id for the derived BehaviouralSpec (default: "local")').option("--application-id <id>", "Application id for the derived BehaviouralSpec (default: directory name)").option("--verbose", "Print worker stderr and summary statistics").action(async (targetPath, opts) => {
|
|
3966
|
+
const urlArg = opts.repoUrl ?? (looksLikeRepoUrl(targetPath) ? targetPath : void 0);
|
|
3967
|
+
const source = urlArg ? "url" : "local";
|
|
3968
|
+
let rootDir;
|
|
3969
|
+
let manifestRemoteUrl;
|
|
3970
|
+
if (urlArg) {
|
|
3971
|
+
const cloneTimeoutMins = parseInt(opts.cloneTimeoutMins, 10);
|
|
3972
|
+
if (isNaN(cloneTimeoutMins) || cloneTimeoutMins < 1) {
|
|
3973
|
+
process.stderr.write(chalk12.red("--clone-timeout-mins must be >= 1\n"));
|
|
3974
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
3975
|
+
}
|
|
3976
|
+
manifestRemoteUrl = redactRemoteUrl(urlArg);
|
|
3977
|
+
try {
|
|
3978
|
+
const cloned = await cloneRepo(urlArg, {
|
|
3979
|
+
ref: opts.ref,
|
|
3980
|
+
timeoutMs: cloneTimeoutMins * 60 * 1e3,
|
|
3981
|
+
verbose: opts.verbose ?? false
|
|
3982
|
+
});
|
|
3983
|
+
rootDir = cloned.dir;
|
|
3984
|
+
if (opts.keepClone) {
|
|
3985
|
+
process.stderr.write(chalk12.dim(`Clone retained at: ${cloned.dir}
|
|
3432
3986
|
`));
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3987
|
+
} else {
|
|
3988
|
+
process.once("exit", () => {
|
|
3989
|
+
try {
|
|
3990
|
+
rmSync2(cloned.dir, { recursive: true, force: true });
|
|
3991
|
+
} catch {
|
|
3992
|
+
}
|
|
3993
|
+
});
|
|
3994
|
+
}
|
|
3995
|
+
} catch (err) {
|
|
3996
|
+
const msg = err instanceof CloneError ? err.message : err instanceof Error ? err.message : String(err);
|
|
3997
|
+
process.stderr.write(chalk12.red(`Clone failed: ${msg}
|
|
3440
3998
|
`));
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3999
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
4000
|
+
}
|
|
4001
|
+
} else {
|
|
4002
|
+
rootDir = path12.resolve(targetPath);
|
|
4003
|
+
if (!await pathExists(rootDir)) {
|
|
4004
|
+
process.stderr.write(chalk12.red(`Path does not exist: ${rootDir}
|
|
3445
4005
|
`));
|
|
3446
|
-
|
|
4006
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
4007
|
+
}
|
|
4008
|
+
let stat4;
|
|
4009
|
+
try {
|
|
4010
|
+
stat4 = await fs4.stat(rootDir);
|
|
4011
|
+
} catch {
|
|
4012
|
+
process.stderr.write(chalk12.red(`Cannot read path: ${rootDir}
|
|
4013
|
+
`));
|
|
4014
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
4015
|
+
}
|
|
4016
|
+
if (!stat4.isDirectory()) {
|
|
4017
|
+
process.stderr.write(chalk12.red(`Path must be a directory: ${rootDir}
|
|
4018
|
+
`));
|
|
4019
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
4020
|
+
}
|
|
3447
4021
|
}
|
|
3448
4022
|
const dsn = opts.dsn || process.env.VIBGRATE_DSN;
|
|
3449
4023
|
if (opts.push && !dsn) {
|
|
3450
|
-
process.stderr.write(
|
|
4024
|
+
process.stderr.write(chalk12.red("--push requires --dsn or VIBGRATE_DSN environment variable\n"));
|
|
3451
4025
|
process.exit(EXIT_USAGE_ERROR);
|
|
3452
4026
|
}
|
|
3453
4027
|
if (dsn && !parseDsn2(dsn)) {
|
|
3454
|
-
process.stderr.write(
|
|
3455
|
-
process.stderr.write(
|
|
4028
|
+
process.stderr.write(chalk12.red("Invalid DSN format.\n"));
|
|
4029
|
+
process.stderr.write(chalk12.dim("Expected: vibgrate+https://<key_id>:<secret>@<host>/<workspace_id>\n"));
|
|
3456
4030
|
process.exit(EXIT_USAGE_ERROR);
|
|
3457
4031
|
}
|
|
3458
|
-
const concurrency = opts.concurrency ? parseInt(opts.concurrency, 10) :
|
|
4032
|
+
const concurrency = opts.concurrency ? parseInt(opts.concurrency, 10) : os4.cpus().length;
|
|
3459
4033
|
if (isNaN(concurrency) || concurrency < 1) {
|
|
3460
|
-
process.stderr.write(
|
|
4034
|
+
process.stderr.write(chalk12.red("--concurrency must be >= 1\n"));
|
|
3461
4035
|
process.exit(EXIT_USAGE_ERROR);
|
|
3462
4036
|
}
|
|
3463
4037
|
const timeoutMins = parseInt(opts.timeoutMins, 10);
|
|
3464
4038
|
if (isNaN(timeoutMins) || timeoutMins < 1) {
|
|
3465
|
-
process.stderr.write(
|
|
4039
|
+
process.stderr.write(chalk12.red("--timeout-mins must be >= 1\n"));
|
|
3466
4040
|
process.exit(EXIT_USAGE_ERROR);
|
|
3467
4041
|
}
|
|
3468
4042
|
const timeoutMs = timeoutMins * 60 * 1e3;
|
|
3469
4043
|
if (opts.out) {
|
|
3470
|
-
const outDir =
|
|
4044
|
+
const outDir = path12.dirname(path12.resolve(opts.out));
|
|
3471
4045
|
if (!await pathExists(outDir)) {
|
|
3472
|
-
process.stderr.write(
|
|
4046
|
+
process.stderr.write(chalk12.red(`Output directory does not exist: ${outDir}
|
|
3473
4047
|
`));
|
|
3474
4048
|
process.exit(EXIT_USAGE_ERROR);
|
|
3475
4049
|
}
|
|
3476
4050
|
try {
|
|
3477
|
-
const outStat = await
|
|
4051
|
+
const outStat = await fs4.stat(path12.resolve(opts.out));
|
|
3478
4052
|
if (outStat.isDirectory()) {
|
|
3479
|
-
process.stderr.write(
|
|
4053
|
+
process.stderr.write(chalk12.red(`--out cannot be a directory: ${opts.out}
|
|
3480
4054
|
`));
|
|
3481
4055
|
process.exit(EXIT_USAGE_ERROR);
|
|
3482
4056
|
}
|
|
@@ -3485,16 +4059,16 @@ var extractCommand = new Command8("extract").description("Analyze source code an
|
|
|
3485
4059
|
}
|
|
3486
4060
|
let feedbackLines;
|
|
3487
4061
|
if (opts.feedback) {
|
|
3488
|
-
const feedbackPath =
|
|
4062
|
+
const feedbackPath = path12.resolve(opts.feedback);
|
|
3489
4063
|
if (!await pathExists(feedbackPath)) {
|
|
3490
|
-
process.stderr.write(
|
|
4064
|
+
process.stderr.write(chalk12.red(`Feedback file not found: ${feedbackPath}
|
|
3491
4065
|
`));
|
|
3492
4066
|
process.exit(EXIT_USAGE_ERROR);
|
|
3493
4067
|
}
|
|
3494
4068
|
try {
|
|
3495
4069
|
feedbackLines = await loadFeedback(feedbackPath);
|
|
3496
4070
|
} catch (err) {
|
|
3497
|
-
process.stderr.write(
|
|
4071
|
+
process.stderr.write(chalk12.red(`Invalid feedback file: ${err instanceof Error ? err.message : String(err)}
|
|
3498
4072
|
`));
|
|
3499
4073
|
process.exit(EXIT_USAGE_ERROR);
|
|
3500
4074
|
}
|
|
@@ -3504,9 +4078,9 @@ var extractCommand = new Command8("extract").description("Analyze source code an
|
|
|
3504
4078
|
targetLanguages = opts.language.split(",").map((l) => normalizeLanguageToken(l)).filter(Boolean);
|
|
3505
4079
|
for (const lang of targetLanguages) {
|
|
3506
4080
|
if (!SUPPORTED_LANGUAGES.has(lang)) {
|
|
3507
|
-
process.stderr.write(
|
|
4081
|
+
process.stderr.write(chalk12.red(`Unknown language: "${lang}"
|
|
3508
4082
|
`));
|
|
3509
|
-
process.stderr.write(
|
|
4083
|
+
process.stderr.write(chalk12.dim(`Supported: ${[...SUPPORTED_LANGUAGES].sort().join(", ")}
|
|
3510
4084
|
`));
|
|
3511
4085
|
process.exit(EXIT_USAGE_ERROR);
|
|
3512
4086
|
}
|
|
@@ -3514,14 +4088,14 @@ var extractCommand = new Command8("extract").description("Analyze source code an
|
|
|
3514
4088
|
} else {
|
|
3515
4089
|
const detected = await detectLanguages(rootDir, opts.includeTests ?? false);
|
|
3516
4090
|
if (detected.length === 0) {
|
|
3517
|
-
process.stderr.write(
|
|
4091
|
+
process.stderr.write(chalk12.yellow("No supported source files detected.\n"));
|
|
3518
4092
|
process.exit(EXIT_SUCCESS);
|
|
3519
4093
|
}
|
|
3520
4094
|
targetLanguages = detected.map((d) => d.language);
|
|
3521
4095
|
if (opts.verbose) {
|
|
3522
|
-
process.stderr.write(
|
|
4096
|
+
process.stderr.write(chalk12.dim("Detected languages:\n"));
|
|
3523
4097
|
for (const d of detected) {
|
|
3524
|
-
process.stderr.write(
|
|
4098
|
+
process.stderr.write(chalk12.dim(` ${d.language}: ${d.fileCount} files
|
|
3525
4099
|
`));
|
|
3526
4100
|
}
|
|
3527
4101
|
}
|
|
@@ -3529,20 +4103,20 @@ var extractCommand = new Command8("extract").description("Analyze source code an
|
|
|
3529
4103
|
const runnableLanguages = targetLanguages.filter((l) => ALL_WORKER_LANGS.has(l));
|
|
3530
4104
|
const unknownWorkerLangs = targetLanguages.filter((l) => !ALL_WORKER_LANGS.has(l));
|
|
3531
4105
|
if (unknownWorkerLangs.length > 0 && opts.verbose) {
|
|
3532
|
-
process.stderr.write(
|
|
4106
|
+
process.stderr.write(chalk12.dim(
|
|
3533
4107
|
`No worker registered for: ${unknownWorkerLangs.join(", ")} \u2014 skipping.
|
|
3534
4108
|
`
|
|
3535
4109
|
));
|
|
3536
4110
|
}
|
|
3537
4111
|
if (runnableLanguages.length === 0) {
|
|
3538
|
-
process.stderr.write(
|
|
4112
|
+
process.stderr.write(chalk12.yellow("No languages with available HCS workers found.\n"));
|
|
3539
4113
|
process.exit(EXIT_SUCCESS);
|
|
3540
4114
|
}
|
|
3541
4115
|
const startTime = Date.now();
|
|
3542
4116
|
const globalDeadline = startTime + timeoutMs;
|
|
3543
4117
|
process.stderr.write(
|
|
3544
|
-
|
|
3545
|
-
`) +
|
|
4118
|
+
chalk12.bold(`Extracting HCS facts from ${rootDir}
|
|
4119
|
+
`) + chalk12.dim(`Languages: ${runnableLanguages.join(", ")} Concurrency: ${concurrency} Timeout: ${timeoutMins}m
|
|
3546
4120
|
`)
|
|
3547
4121
|
);
|
|
3548
4122
|
const progress = new ProgressTracker(runnableLanguages);
|
|
@@ -3589,7 +4163,7 @@ var extractCommand = new Command8("extract").description("Analyze source code an
|
|
|
3589
4163
|
hasSchemaFailure = true;
|
|
3590
4164
|
allErrors.push(`[${language}] Schema validation: ${validation.error}`);
|
|
3591
4165
|
if (opts.verbose) {
|
|
3592
|
-
process.stderr.write(
|
|
4166
|
+
process.stderr.write(chalk12.red(`[${language}] Invalid fact: ${validation.error}
|
|
3593
4167
|
`));
|
|
3594
4168
|
}
|
|
3595
4169
|
}
|
|
@@ -3615,7 +4189,7 @@ var extractCommand = new Command8("extract").description("Analyze source code an
|
|
|
3615
4189
|
progress.finish(elapsed);
|
|
3616
4190
|
if (opts.derive) {
|
|
3617
4191
|
const scanId = opts.scanId || "local";
|
|
3618
|
-
const applicationId = opts.applicationId ||
|
|
4192
|
+
const applicationId = opts.applicationId || path12.basename(rootDir);
|
|
3619
4193
|
const { envelopes, summary } = deriveBehaviouralFacts(derivedInputs, { scanId, applicationId });
|
|
3620
4194
|
for (const env of envelopes) {
|
|
3621
4195
|
const line = JSON.stringify(env);
|
|
@@ -3626,18 +4200,42 @@ var extractCommand = new Command8("extract").description("Analyze source code an
|
|
|
3626
4200
|
allErrors.push("[behavioural] derived fact failed schema validation");
|
|
3627
4201
|
}
|
|
3628
4202
|
}
|
|
3629
|
-
process.stderr.write(
|
|
4203
|
+
process.stderr.write(chalk12.dim(
|
|
3630
4204
|
`Derived ${summary.assertions} behavioural assertion(s) \xB7 confidence ${summary.behaviouralConfidence} (${summary.assertedFacts}/${summary.surfaceFacts} surface facts)
|
|
3631
4205
|
`
|
|
3632
4206
|
));
|
|
3633
4207
|
}
|
|
3634
4208
|
allFacts.sort();
|
|
3635
|
-
const
|
|
4209
|
+
const manifestScanId = opts.scanId || "local";
|
|
4210
|
+
const manifestApplicationId = opts.applicationId || (source === "url" ? deriveRepoName(manifestRemoteUrl) : void 0) || path12.basename(rootDir);
|
|
4211
|
+
let manifestVcs;
|
|
4212
|
+
try {
|
|
4213
|
+
manifestVcs = await detectVcs2(rootDir);
|
|
4214
|
+
} catch {
|
|
4215
|
+
manifestVcs = void 0;
|
|
4216
|
+
}
|
|
4217
|
+
const manifest = buildScanManifest({
|
|
4218
|
+
// Prefer the redacted URL we cloned from; fall back to the local repo's
|
|
4219
|
+
// configured remote (also credential-redacted) for non-URL scans.
|
|
4220
|
+
remoteUrl: manifestRemoteUrl ?? (manifestVcs?.remoteUrl ? redactRemoteUrl(manifestVcs.remoteUrl) : void 0),
|
|
4221
|
+
sha: manifestVcs?.sha,
|
|
4222
|
+
shortSha: manifestVcs?.shortSha,
|
|
4223
|
+
branch: manifestVcs?.branch,
|
|
4224
|
+
ref: opts.ref,
|
|
4225
|
+
source,
|
|
4226
|
+
languages: runnableLanguages,
|
|
4227
|
+
scanId: manifestScanId,
|
|
4228
|
+
applicationId: manifestApplicationId,
|
|
4229
|
+
vibgrateVersion: VERSION,
|
|
4230
|
+
factLines: allFacts
|
|
4231
|
+
});
|
|
4232
|
+
const manifestLine = JSON.stringify(manifest);
|
|
4233
|
+
const allLines = [manifestLine, ...allPreamble, ...allFacts];
|
|
3636
4234
|
const ndjsonOutput = allLines.join("\n") + (allLines.length > 0 ? "\n" : "");
|
|
3637
4235
|
if (opts.out) {
|
|
3638
|
-
const outPath =
|
|
3639
|
-
await
|
|
3640
|
-
process.stderr.write(
|
|
4236
|
+
const outPath = path12.resolve(opts.out);
|
|
4237
|
+
await fs4.writeFile(outPath, ndjsonOutput, "utf-8");
|
|
4238
|
+
process.stderr.write(chalk12.green(`\u2714 Wrote ${allFacts.length} facts to ${outPath}
|
|
3641
4239
|
`));
|
|
3642
4240
|
} else {
|
|
3643
4241
|
process.stdout.write(ndjsonOutput);
|
|
@@ -3649,48 +4247,48 @@ var extractCommand = new Command8("extract").description("Analyze source code an
|
|
|
3649
4247
|
}
|
|
3650
4248
|
}
|
|
3651
4249
|
if (opts.verbose) {
|
|
3652
|
-
process.stderr.write(
|
|
3653
|
-
process.stderr.write(
|
|
4250
|
+
process.stderr.write(chalk12.dim("\n\u2500\u2500 Summary \u2500\u2500\n"));
|
|
4251
|
+
process.stderr.write(chalk12.dim(` Facts emitted : ${allFacts.length}
|
|
3654
4252
|
`));
|
|
3655
|
-
process.stderr.write(
|
|
4253
|
+
process.stderr.write(chalk12.dim(` Languages : ${runnableLanguages.join(", ")}
|
|
3656
4254
|
`));
|
|
3657
|
-
process.stderr.write(
|
|
4255
|
+
process.stderr.write(chalk12.dim(` Elapsed : ${elapsed}s
|
|
3658
4256
|
`));
|
|
3659
4257
|
if (allErrors.length > 0) {
|
|
3660
|
-
process.stderr.write(
|
|
4258
|
+
process.stderr.write(chalk12.dim(` Errors : ${allErrors.length}
|
|
3661
4259
|
`));
|
|
3662
4260
|
for (const err of allErrors.slice(0, 10)) {
|
|
3663
|
-
process.stderr.write(
|
|
4261
|
+
process.stderr.write(chalk12.dim(` ${err}
|
|
3664
4262
|
`));
|
|
3665
4263
|
}
|
|
3666
4264
|
if (allErrors.length > 10) {
|
|
3667
|
-
process.stderr.write(
|
|
4265
|
+
process.stderr.write(chalk12.dim(` ... and ${allErrors.length - 10} more
|
|
3668
4266
|
`));
|
|
3669
4267
|
}
|
|
3670
4268
|
}
|
|
3671
|
-
process.stderr.write(
|
|
4269
|
+
process.stderr.write(chalk12.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n"));
|
|
3672
4270
|
}
|
|
3673
4271
|
if (hasTimeout) {
|
|
3674
|
-
process.stderr.write(
|
|
4272
|
+
process.stderr.write(chalk12.red(`Timeout exceeded (${timeoutMins} minutes)
|
|
3675
4273
|
`));
|
|
3676
4274
|
process.exit(EXIT_TIMEOUT);
|
|
3677
4275
|
}
|
|
3678
4276
|
if (hasSchemaFailure) {
|
|
3679
|
-
process.stderr.write(
|
|
4277
|
+
process.stderr.write(chalk12.red("Fact schema validation failures detected\n"));
|
|
3680
4278
|
process.exit(EXIT_SCHEMA_FAILURE);
|
|
3681
4279
|
}
|
|
3682
4280
|
if (hasParseFailure) {
|
|
3683
|
-
process.stderr.write(
|
|
4281
|
+
process.stderr.write(chalk12.red("Parsing failures detected\n"));
|
|
3684
4282
|
process.exit(EXIT_PARSE_FAILURE);
|
|
3685
4283
|
}
|
|
3686
4284
|
});
|
|
3687
4285
|
|
|
3688
4286
|
// src/commands/diff.ts
|
|
3689
|
-
import * as
|
|
3690
|
-
import * as
|
|
3691
|
-
import * as
|
|
3692
|
-
import { Command as
|
|
3693
|
-
import
|
|
4287
|
+
import * as path13 from "path";
|
|
4288
|
+
import * as fs5 from "fs/promises";
|
|
4289
|
+
import * as crypto4 from "crypto";
|
|
4290
|
+
import { Command as Command11 } from "commander";
|
|
4291
|
+
import chalk13 from "chalk";
|
|
3694
4292
|
var EXIT_INVALID_PATH = 2;
|
|
3695
4293
|
var EXIT_USAGE_ERROR2 = 5;
|
|
3696
4294
|
var VALID_FORMATS = /* @__PURE__ */ new Set(["ndjson", "json", "patch"]);
|
|
@@ -3706,14 +4304,14 @@ async function discoverFiles(root, includeGlobs, excludeGlobs) {
|
|
|
3706
4304
|
async function walk(dir, relBase) {
|
|
3707
4305
|
let entries;
|
|
3708
4306
|
try {
|
|
3709
|
-
entries = await
|
|
4307
|
+
entries = await fs5.readdir(dir, { withFileTypes: true });
|
|
3710
4308
|
} catch {
|
|
3711
4309
|
return;
|
|
3712
4310
|
}
|
|
3713
4311
|
for (const entry of entries) {
|
|
3714
4312
|
if (entry.name.startsWith(".") && SKIP_DIRS2.has(entry.name)) continue;
|
|
3715
|
-
const absPath =
|
|
3716
|
-
const relPath =
|
|
4313
|
+
const absPath = path13.join(dir, entry.name);
|
|
4314
|
+
const relPath = path13.join(relBase, entry.name);
|
|
3717
4315
|
if (entry.isDirectory()) {
|
|
3718
4316
|
if (!SKIP_DIRS2.has(entry.name)) {
|
|
3719
4317
|
await walk(absPath, relPath);
|
|
@@ -3884,7 +4482,7 @@ function lcsRatio(a, b) {
|
|
|
3884
4482
|
return 2 * lcsLen / (n + m);
|
|
3885
4483
|
}
|
|
3886
4484
|
function generateDeltaId(kind, filePath) {
|
|
3887
|
-
const hash =
|
|
4485
|
+
const hash = crypto4.createHash("sha256").update(`${kind}:${filePath}`).digest("hex").substring(0, 16);
|
|
3888
4486
|
return `hcs:delta:${hash}`;
|
|
3889
4487
|
}
|
|
3890
4488
|
function formatNdjson(deltas) {
|
|
@@ -3896,8 +4494,8 @@ function formatJson(deltas) {
|
|
|
3896
4494
|
function formatPatch(deltas, originalPath, generatedPath) {
|
|
3897
4495
|
const lines = [];
|
|
3898
4496
|
for (const delta of deltas) {
|
|
3899
|
-
const aFile = delta.originalPath ?
|
|
3900
|
-
const bFile = delta.generatedPath ?
|
|
4497
|
+
const aFile = delta.originalPath ? path13.join("a", delta.originalPath) : "/dev/null";
|
|
4498
|
+
const bFile = delta.generatedPath ? path13.join("b", delta.generatedPath) : "/dev/null";
|
|
3901
4499
|
if (delta.changeKind === "renamed" && delta.originalPath && delta.generatedPath) {
|
|
3902
4500
|
lines.push(`diff --vibgrate ${aFile} ${bFile}`);
|
|
3903
4501
|
lines.push(`similarity index ${Math.round((delta.similarity ?? 0) * 100)}%`);
|
|
@@ -3918,38 +4516,38 @@ function formatPatch(deltas, originalPath, generatedPath) {
|
|
|
3918
4516
|
}
|
|
3919
4517
|
return lines.join("\n");
|
|
3920
4518
|
}
|
|
3921
|
-
var diffCommand = new
|
|
3922
|
-
const origDir =
|
|
3923
|
-
const genDir =
|
|
4519
|
+
var diffCommand = new Command11("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) => {
|
|
4520
|
+
const origDir = path13.resolve(originalPath);
|
|
4521
|
+
const genDir = path13.resolve(generatedPath);
|
|
3924
4522
|
for (const [label, dir] of [["original_path", origDir], ["generated_path", genDir]]) {
|
|
3925
4523
|
if (!await pathExists(dir)) {
|
|
3926
|
-
process.stderr.write(
|
|
4524
|
+
process.stderr.write(chalk13.red(`${label} does not exist: ${dir}
|
|
3927
4525
|
`));
|
|
3928
4526
|
process.exit(EXIT_INVALID_PATH);
|
|
3929
4527
|
}
|
|
3930
|
-
let
|
|
4528
|
+
let stat4;
|
|
3931
4529
|
try {
|
|
3932
|
-
|
|
4530
|
+
stat4 = await fs5.stat(dir);
|
|
3933
4531
|
} catch {
|
|
3934
|
-
process.stderr.write(
|
|
4532
|
+
process.stderr.write(chalk13.red(`Cannot read ${label}: ${dir}
|
|
3935
4533
|
`));
|
|
3936
4534
|
process.exit(EXIT_INVALID_PATH);
|
|
3937
4535
|
}
|
|
3938
|
-
if (!
|
|
3939
|
-
process.stderr.write(
|
|
4536
|
+
if (!stat4.isDirectory()) {
|
|
4537
|
+
process.stderr.write(chalk13.red(`${label} must be a directory: ${dir}
|
|
3940
4538
|
`));
|
|
3941
4539
|
process.exit(EXIT_INVALID_PATH);
|
|
3942
4540
|
}
|
|
3943
4541
|
}
|
|
3944
4542
|
const format = opts.format.toLowerCase();
|
|
3945
4543
|
if (!VALID_FORMATS.has(format)) {
|
|
3946
|
-
process.stderr.write(
|
|
4544
|
+
process.stderr.write(chalk13.red(`Invalid format: "${opts.format}". Allowed: ndjson, json, patch
|
|
3947
4545
|
`));
|
|
3948
4546
|
process.exit(EXIT_USAGE_ERROR2);
|
|
3949
4547
|
}
|
|
3950
4548
|
const contextLines = parseInt(opts.context, 10);
|
|
3951
4549
|
if (isNaN(contextLines) || contextLines < 0) {
|
|
3952
|
-
process.stderr.write(
|
|
4550
|
+
process.stderr.write(chalk13.red("--context must be >= 0\n"));
|
|
3953
4551
|
process.exit(EXIT_USAGE_ERROR2);
|
|
3954
4552
|
}
|
|
3955
4553
|
const includeGlobs = opts.include ? opts.include.split(",").map((g) => g.trim()).filter(Boolean) : void 0;
|
|
@@ -3961,9 +4559,9 @@ var diffCommand = new Command9("diff").description("Compare two directory trees
|
|
|
3961
4559
|
const origMap = new Map(origFiles.map((f) => [f.relPath, f]));
|
|
3962
4560
|
const genMap = new Map(genFiles.map((f) => [f.relPath, f]));
|
|
3963
4561
|
if (opts.verbose) {
|
|
3964
|
-
process.stderr.write(
|
|
4562
|
+
process.stderr.write(chalk13.dim(`Original: ${origFiles.length} files
|
|
3965
4563
|
`));
|
|
3966
|
-
process.stderr.write(
|
|
4564
|
+
process.stderr.write(chalk13.dim(`Generated: ${genFiles.length} files
|
|
3967
4565
|
`));
|
|
3968
4566
|
}
|
|
3969
4567
|
const deltas = [];
|
|
@@ -3974,8 +4572,8 @@ var diffCommand = new Command9("diff").description("Compare two directory trees
|
|
|
3974
4572
|
if (genFile) {
|
|
3975
4573
|
matchedOrig.add(relPath);
|
|
3976
4574
|
matchedGen.add(relPath);
|
|
3977
|
-
const origContent = await
|
|
3978
|
-
const genContent = await
|
|
4575
|
+
const origContent = await fs5.readFile(origFile.absPath, "utf-8");
|
|
4576
|
+
const genContent = await fs5.readFile(genFile.absPath, "utf-8");
|
|
3979
4577
|
const origCompare = opts.ignoreWhitespace ? origContent.replace(/\s+/g, " ").trim() : origContent;
|
|
3980
4578
|
const genCompare = opts.ignoreWhitespace ? genContent.replace(/\s+/g, " ").trim() : genContent;
|
|
3981
4579
|
if (origCompare !== genCompare) {
|
|
@@ -3997,7 +4595,7 @@ var diffCommand = new Command9("diff").description("Compare two directory trees
|
|
|
3997
4595
|
}
|
|
3998
4596
|
deltas.push(delta);
|
|
3999
4597
|
if (opts.verbose) {
|
|
4000
|
-
process.stderr.write(
|
|
4598
|
+
process.stderr.write(chalk13.dim(` modified: ${relPath} (+${diffResult.insertions} -${diffResult.deletions})
|
|
4001
4599
|
`));
|
|
4002
4600
|
}
|
|
4003
4601
|
}
|
|
@@ -4014,13 +4612,13 @@ var diffCommand = new Command9("diff").description("Compare two directory trees
|
|
|
4014
4612
|
await Promise.all([
|
|
4015
4613
|
...unmatchedOrig.map(async (f) => {
|
|
4016
4614
|
try {
|
|
4017
|
-
origContents.set(f.relPath, await
|
|
4615
|
+
origContents.set(f.relPath, await fs5.readFile(f.absPath, "utf-8"));
|
|
4018
4616
|
} catch {
|
|
4019
4617
|
}
|
|
4020
4618
|
}),
|
|
4021
4619
|
...unmatchedGen.map(async (f) => {
|
|
4022
4620
|
try {
|
|
4023
|
-
genContents.set(f.relPath, await
|
|
4621
|
+
genContents.set(f.relPath, await fs5.readFile(f.absPath, "utf-8"));
|
|
4024
4622
|
} catch {
|
|
4025
4623
|
}
|
|
4026
4624
|
})
|
|
@@ -4028,7 +4626,7 @@ var diffCommand = new Command9("diff").description("Compare two directory trees
|
|
|
4028
4626
|
const pairs = [];
|
|
4029
4627
|
for (const [origPath, origContent] of origContents) {
|
|
4030
4628
|
for (const [genPath, genContent] of genContents) {
|
|
4031
|
-
if (
|
|
4629
|
+
if (path13.extname(origPath) !== path13.extname(genPath)) continue;
|
|
4032
4630
|
const similarity = computeSimilarity(origContent, genContent);
|
|
4033
4631
|
if (similarity >= RENAME_THRESHOLD) {
|
|
4034
4632
|
pairs.push({ origPath, genPath, similarity });
|
|
@@ -4061,7 +4659,7 @@ var diffCommand = new Command9("diff").description("Compare two directory trees
|
|
|
4061
4659
|
}
|
|
4062
4660
|
deltas.push(delta);
|
|
4063
4661
|
if (opts.verbose) {
|
|
4064
|
-
process.stderr.write(
|
|
4662
|
+
process.stderr.write(chalk13.dim(
|
|
4065
4663
|
` renamed: ${pair.origPath} \u2192 ${pair.genPath} (${(pair.similarity * 100).toFixed(0)}% similar)
|
|
4066
4664
|
`
|
|
4067
4665
|
));
|
|
@@ -4078,7 +4676,7 @@ var diffCommand = new Command9("diff").description("Compare two directory trees
|
|
|
4078
4676
|
};
|
|
4079
4677
|
if (opts.stats) {
|
|
4080
4678
|
try {
|
|
4081
|
-
const content = await
|
|
4679
|
+
const content = await fs5.readFile(file.absPath, "utf-8");
|
|
4082
4680
|
delta.stats = { insertions: 0, deletions: content.split("\n").length };
|
|
4083
4681
|
} catch {
|
|
4084
4682
|
delta.stats = { insertions: 0, deletions: 0 };
|
|
@@ -4086,7 +4684,7 @@ var diffCommand = new Command9("diff").description("Compare two directory trees
|
|
|
4086
4684
|
}
|
|
4087
4685
|
deltas.push(delta);
|
|
4088
4686
|
if (opts.verbose) {
|
|
4089
|
-
process.stderr.write(
|
|
4687
|
+
process.stderr.write(chalk13.dim(` removed: ${file.relPath}
|
|
4090
4688
|
`));
|
|
4091
4689
|
}
|
|
4092
4690
|
}
|
|
@@ -4100,7 +4698,7 @@ var diffCommand = new Command9("diff").description("Compare two directory trees
|
|
|
4100
4698
|
};
|
|
4101
4699
|
if (opts.stats) {
|
|
4102
4700
|
try {
|
|
4103
|
-
const content = await
|
|
4701
|
+
const content = await fs5.readFile(file.absPath, "utf-8");
|
|
4104
4702
|
delta.stats = { insertions: content.split("\n").length, deletions: 0 };
|
|
4105
4703
|
} catch {
|
|
4106
4704
|
delta.stats = { insertions: 0, deletions: 0 };
|
|
@@ -4108,7 +4706,7 @@ var diffCommand = new Command9("diff").description("Compare two directory trees
|
|
|
4108
4706
|
}
|
|
4109
4707
|
deltas.push(delta);
|
|
4110
4708
|
if (opts.verbose) {
|
|
4111
|
-
process.stderr.write(
|
|
4709
|
+
process.stderr.write(chalk13.dim(` added: ${file.relPath}
|
|
4112
4710
|
`));
|
|
4113
4711
|
}
|
|
4114
4712
|
}
|
|
@@ -4129,16 +4727,16 @@ var diffCommand = new Command9("diff").description("Compare two directory trees
|
|
|
4129
4727
|
output = formatPatch(deltas, originalPath, generatedPath);
|
|
4130
4728
|
break;
|
|
4131
4729
|
}
|
|
4132
|
-
const outPath =
|
|
4133
|
-
await
|
|
4730
|
+
const outPath = path13.resolve(opts.out);
|
|
4731
|
+
await fs5.writeFile(outPath, output, "utf-8");
|
|
4134
4732
|
const added = deltas.filter((d) => d.changeKind === "added").length;
|
|
4135
4733
|
const removed = deltas.filter((d) => d.changeKind === "removed").length;
|
|
4136
4734
|
const modified = deltas.filter((d) => d.changeKind === "modified").length;
|
|
4137
4735
|
const renamed = deltas.filter((d) => d.changeKind === "renamed").length;
|
|
4138
4736
|
process.stderr.write(
|
|
4139
|
-
|
|
4737
|
+
chalk13.green(`\u2714 `) + `${deltas.length} changes: ` + chalk13.green(`+${added} added`) + ", " + chalk13.red(`-${removed} removed`) + ", " + chalk13.yellow(`~${modified} modified`) + ", " + chalk13.blue(`\u2192${renamed} renamed`) + "\n"
|
|
4140
4738
|
);
|
|
4141
|
-
process.stderr.write(
|
|
4739
|
+
process.stderr.write(chalk13.dim(` Written to ${outPath}
|
|
4142
4740
|
`));
|
|
4143
4741
|
if (opts.verbose && opts.stats) {
|
|
4144
4742
|
let totalIns = 0;
|
|
@@ -4149,193 +4747,193 @@ var diffCommand = new Command9("diff").description("Compare two directory trees
|
|
|
4149
4747
|
totalDel += d.stats.deletions;
|
|
4150
4748
|
}
|
|
4151
4749
|
}
|
|
4152
|
-
process.stderr.write(
|
|
4750
|
+
process.stderr.write(chalk13.dim(` Total: +${totalIns} insertions, -${totalDel} deletions
|
|
4153
4751
|
`));
|
|
4154
4752
|
}
|
|
4155
4753
|
});
|
|
4156
4754
|
|
|
4157
4755
|
// src/commands/help.ts
|
|
4158
|
-
import { Command as
|
|
4159
|
-
import
|
|
4756
|
+
import { Command as Command12 } from "commander";
|
|
4757
|
+
import chalk14 from "chalk";
|
|
4160
4758
|
var HELP_URL = "https://vibgrate.com/help";
|
|
4161
4759
|
function printFooter() {
|
|
4162
4760
|
console.log("");
|
|
4163
|
-
console.log(
|
|
4761
|
+
console.log(chalk14.dim(`See ${HELP_URL} for more guidance`));
|
|
4164
4762
|
}
|
|
4165
4763
|
var detailedHelp = {
|
|
4166
4764
|
scan: () => {
|
|
4167
4765
|
console.log("");
|
|
4168
|
-
console.log(
|
|
4766
|
+
console.log(chalk14.bold.underline("vibgrate scan") + chalk14.dim(" \u2014 Scan a project for upgrade drift"));
|
|
4169
4767
|
console.log("");
|
|
4170
|
-
console.log(
|
|
4768
|
+
console.log(chalk14.bold("Usage:"));
|
|
4171
4769
|
console.log(" vibgrate scan [path] [options]");
|
|
4172
4770
|
console.log("");
|
|
4173
|
-
console.log(
|
|
4174
|
-
console.log(` ${
|
|
4771
|
+
console.log(chalk14.bold("Arguments:"));
|
|
4772
|
+
console.log(` ${chalk14.cyan("[path]")} Path to scan (default: current directory)`);
|
|
4175
4773
|
console.log("");
|
|
4176
|
-
console.log(
|
|
4177
|
-
console.log(` ${
|
|
4178
|
-
console.log(` ${
|
|
4774
|
+
console.log(chalk14.bold("Output options:"));
|
|
4775
|
+
console.log(` ${chalk14.cyan("--format <format>")} Output format: ${chalk14.white("text")} | json | sarif | md (default: text)`);
|
|
4776
|
+
console.log(` ${chalk14.cyan("--out <file>")} Write output to a file instead of stdout`);
|
|
4179
4777
|
console.log("");
|
|
4180
|
-
console.log(
|
|
4181
|
-
console.log(` ${
|
|
4182
|
-
console.log(` ${
|
|
4183
|
-
console.log(` ${
|
|
4184
|
-
console.log(` ${
|
|
4778
|
+
console.log(chalk14.bold("Baseline & gating:"));
|
|
4779
|
+
console.log(` ${chalk14.cyan("--baseline <file>")} Compare results against a saved baseline`);
|
|
4780
|
+
console.log(` ${chalk14.cyan("--drift-budget <score>")} Fail if drift score exceeds this value (0\u2013100)`);
|
|
4781
|
+
console.log(` ${chalk14.cyan("--drift-worsening <percent>")} Fail if drift worsens by more than % since baseline`);
|
|
4782
|
+
console.log(` ${chalk14.cyan("--fail-on <level>")} Fail exit code on warn or error findings`);
|
|
4185
4783
|
console.log("");
|
|
4186
|
-
console.log(
|
|
4187
|
-
console.log(` ${
|
|
4188
|
-
console.log(` ${
|
|
4784
|
+
console.log(chalk14.bold("Performance:"));
|
|
4785
|
+
console.log(` ${chalk14.cyan("--concurrency <n>")} Max concurrent registry calls (default: 8)`);
|
|
4786
|
+
console.log(` ${chalk14.cyan("--changed-only")} Only scan files changed since last git commit`);
|
|
4189
4787
|
console.log("");
|
|
4190
|
-
console.log(
|
|
4191
|
-
console.log(` ${
|
|
4192
|
-
console.log(` ${
|
|
4193
|
-
console.log(` ${
|
|
4194
|
-
console.log(` ${
|
|
4788
|
+
console.log(chalk14.bold("Privacy & offline:"));
|
|
4789
|
+
console.log(` ${chalk14.cyan("--offline")} Run without any network calls; skip result upload`);
|
|
4790
|
+
console.log(` ${chalk14.cyan("--package-manifest <file>")} Use a local package-version manifest (JSON or ZIP) for offline mode`);
|
|
4791
|
+
console.log(` ${chalk14.cyan("--no-local-artifacts")} Do not write .vibgrate JSON artifacts to disk`);
|
|
4792
|
+
console.log(` ${chalk14.cyan("--max-privacy")} Strongest privacy mode: minimal scanners + no local artifacts`);
|
|
4195
4793
|
console.log("");
|
|
4196
|
-
console.log(
|
|
4197
|
-
console.log(` ${
|
|
4198
|
-
console.log(` ${
|
|
4794
|
+
console.log(chalk14.bold("Tooling:"));
|
|
4795
|
+
console.log(` ${chalk14.cyan("--install-tools")} Auto-install missing security scanners via Homebrew`);
|
|
4796
|
+
console.log(` ${chalk14.cyan("--ui-purpose")} Enable UI purpose evidence extraction (slower)`);
|
|
4199
4797
|
console.log("");
|
|
4200
|
-
console.log(
|
|
4201
|
-
console.log(` ${
|
|
4202
|
-
console.log(` ${
|
|
4203
|
-
console.log(` ${
|
|
4204
|
-
console.log(` ${
|
|
4798
|
+
console.log(chalk14.bold("Uploading results:"));
|
|
4799
|
+
console.log(` ${chalk14.cyan("--push")} Auto-push results to Vibgrate API after scan`);
|
|
4800
|
+
console.log(` ${chalk14.cyan("--dsn <dsn>")} DSN token for push (or set VIBGRATE_DSN env var)`);
|
|
4801
|
+
console.log(` ${chalk14.cyan("--region <region>")} Data residency region: us | eu (default: us)`);
|
|
4802
|
+
console.log(` ${chalk14.cyan("--strict")} Fail if the upload to Vibgrate API fails`);
|
|
4205
4803
|
console.log("");
|
|
4206
|
-
console.log(
|
|
4207
|
-
console.log(` ${
|
|
4804
|
+
console.log(chalk14.bold("Examples:"));
|
|
4805
|
+
console.log(` ${chalk14.dim("# Scan the current directory and display a text report")}`);
|
|
4208
4806
|
console.log(" vibgrate scan .");
|
|
4209
4807
|
console.log("");
|
|
4210
|
-
console.log(` ${
|
|
4808
|
+
console.log(` ${chalk14.dim("# Scan, fail if drift score > 40, and write SARIF for GitHub Actions")}`);
|
|
4211
4809
|
console.log(" vibgrate scan . --drift-budget 40 --format sarif --out drift.sarif");
|
|
4212
4810
|
console.log("");
|
|
4213
|
-
console.log(` ${
|
|
4811
|
+
console.log(` ${chalk14.dim("# Scan and automatically upload results via a DSN")}`);
|
|
4214
4812
|
console.log(" vibgrate scan . --push --dsn $VIBGRATE_DSN");
|
|
4215
4813
|
console.log("");
|
|
4216
|
-
console.log(` ${
|
|
4814
|
+
console.log(` ${chalk14.dim("# Offline scan using a pre-downloaded package manifest")}`);
|
|
4217
4815
|
console.log(" vibgrate scan . --offline --package-manifest ./manifest.zip");
|
|
4218
4816
|
},
|
|
4219
4817
|
init: () => {
|
|
4220
4818
|
console.log("");
|
|
4221
|
-
console.log(
|
|
4819
|
+
console.log(chalk14.bold.underline("vibgrate init") + chalk14.dim(" \u2014 Initialise vibgrate in a project directory"));
|
|
4222
4820
|
console.log("");
|
|
4223
|
-
console.log(
|
|
4821
|
+
console.log(chalk14.bold("Usage:"));
|
|
4224
4822
|
console.log(" vibgrate init [path] [options]");
|
|
4225
4823
|
console.log("");
|
|
4226
|
-
console.log(
|
|
4227
|
-
console.log(` ${
|
|
4824
|
+
console.log(chalk14.bold("Arguments:"));
|
|
4825
|
+
console.log(` ${chalk14.cyan("[path]")} Directory to initialise (default: current directory)`);
|
|
4228
4826
|
console.log("");
|
|
4229
|
-
console.log(
|
|
4230
|
-
console.log(` ${
|
|
4231
|
-
console.log(` ${
|
|
4827
|
+
console.log(chalk14.bold("Options:"));
|
|
4828
|
+
console.log(` ${chalk14.cyan("--baseline")} Create an initial drift baseline after init`);
|
|
4829
|
+
console.log(` ${chalk14.cyan("--yes")} Skip all confirmation prompts`);
|
|
4232
4830
|
console.log("");
|
|
4233
|
-
console.log(
|
|
4831
|
+
console.log(chalk14.bold("What it does:"));
|
|
4234
4832
|
console.log(" \u2022 Creates a .vibgrate/ directory");
|
|
4235
4833
|
console.log(" \u2022 Writes a vibgrate.config.ts starter config");
|
|
4236
4834
|
console.log(" \u2022 Optionally runs an initial baseline scan (--baseline)");
|
|
4237
4835
|
console.log("");
|
|
4238
|
-
console.log(
|
|
4836
|
+
console.log(chalk14.bold("Examples:"));
|
|
4239
4837
|
console.log(" vibgrate init");
|
|
4240
4838
|
console.log(" vibgrate init ./my-project --baseline");
|
|
4241
4839
|
},
|
|
4242
4840
|
baseline: () => {
|
|
4243
4841
|
console.log("");
|
|
4244
|
-
console.log(
|
|
4842
|
+
console.log(chalk14.bold.underline("vibgrate baseline") + chalk14.dim(" \u2014 Save a drift baseline snapshot"));
|
|
4245
4843
|
console.log("");
|
|
4246
|
-
console.log(
|
|
4844
|
+
console.log(chalk14.bold("Usage:"));
|
|
4247
4845
|
console.log(" vibgrate baseline [path]");
|
|
4248
4846
|
console.log("");
|
|
4249
|
-
console.log(
|
|
4250
|
-
console.log(` ${
|
|
4847
|
+
console.log(chalk14.bold("Arguments:"));
|
|
4848
|
+
console.log(` ${chalk14.cyan("[path]")} Path to baseline (default: current directory)`);
|
|
4251
4849
|
console.log("");
|
|
4252
|
-
console.log(
|
|
4850
|
+
console.log(chalk14.bold("What it does:"));
|
|
4253
4851
|
console.log(" Runs a full scan and saves the result as .vibgrate/baseline.json.");
|
|
4254
4852
|
console.log(" Future scans can compare against this file using --baseline.");
|
|
4255
4853
|
console.log("");
|
|
4256
|
-
console.log(
|
|
4854
|
+
console.log(chalk14.bold("Examples:"));
|
|
4257
4855
|
console.log(" vibgrate baseline .");
|
|
4258
4856
|
console.log(" vibgrate scan . --baseline .vibgrate/baseline.json --drift-worsening 10");
|
|
4259
4857
|
},
|
|
4260
4858
|
report: () => {
|
|
4261
4859
|
console.log("");
|
|
4262
|
-
console.log(
|
|
4860
|
+
console.log(chalk14.bold.underline("vibgrate report") + chalk14.dim(" \u2014 Generate a report from a saved scan artifact"));
|
|
4263
4861
|
console.log("");
|
|
4264
|
-
console.log(
|
|
4862
|
+
console.log(chalk14.bold("Usage:"));
|
|
4265
4863
|
console.log(" vibgrate report [options]");
|
|
4266
4864
|
console.log("");
|
|
4267
|
-
console.log(
|
|
4268
|
-
console.log(` ${
|
|
4269
|
-
console.log(` ${
|
|
4865
|
+
console.log(chalk14.bold("Options:"));
|
|
4866
|
+
console.log(` ${chalk14.cyan("--in <file>")} Input artifact file (default: .vibgrate/scan_result.json)`);
|
|
4867
|
+
console.log(` ${chalk14.cyan("--format <format>")} Output format: ${chalk14.white("text")} | md | json (default: text)`);
|
|
4270
4868
|
console.log("");
|
|
4271
|
-
console.log(
|
|
4869
|
+
console.log(chalk14.bold("Examples:"));
|
|
4272
4870
|
console.log(" vibgrate report");
|
|
4273
4871
|
console.log(" vibgrate report --format md > DRIFT-REPORT.md");
|
|
4274
4872
|
console.log(" vibgrate report --in ./ci/scan_result.json --format json");
|
|
4275
4873
|
},
|
|
4276
4874
|
sbom: () => {
|
|
4277
4875
|
console.log("");
|
|
4278
|
-
console.log(
|
|
4876
|
+
console.log(chalk14.bold.underline("vibgrate sbom") + chalk14.dim(" \u2014 Export a Software Bill of Materials from a scan artifact"));
|
|
4279
4877
|
console.log("");
|
|
4280
|
-
console.log(
|
|
4878
|
+
console.log(chalk14.bold("Usage:"));
|
|
4281
4879
|
console.log(" vibgrate sbom [options]");
|
|
4282
4880
|
console.log("");
|
|
4283
|
-
console.log(
|
|
4284
|
-
console.log(` ${
|
|
4285
|
-
console.log(` ${
|
|
4286
|
-
console.log(` ${
|
|
4881
|
+
console.log(chalk14.bold("Options:"));
|
|
4882
|
+
console.log(` ${chalk14.cyan("--in <file>")} Input artifact (default: .vibgrate/scan_result.json)`);
|
|
4883
|
+
console.log(` ${chalk14.cyan("--format <format>")} SBOM format: ${chalk14.white("cyclonedx")} | spdx (default: cyclonedx)`);
|
|
4884
|
+
console.log(` ${chalk14.cyan("--out <file>")} Write SBOM to file instead of stdout`);
|
|
4287
4885
|
console.log("");
|
|
4288
|
-
console.log(
|
|
4886
|
+
console.log(chalk14.bold("Examples:"));
|
|
4289
4887
|
console.log(" vibgrate sbom --format cyclonedx --out sbom.json");
|
|
4290
4888
|
console.log(" vibgrate sbom --format spdx --out sbom.spdx.json");
|
|
4291
4889
|
},
|
|
4292
4890
|
push: () => {
|
|
4293
4891
|
console.log("");
|
|
4294
|
-
console.log(
|
|
4892
|
+
console.log(chalk14.bold.underline("vibgrate push") + chalk14.dim(" \u2014 Upload a scan artifact to the Vibgrate API"));
|
|
4295
4893
|
console.log("");
|
|
4296
|
-
console.log(
|
|
4894
|
+
console.log(chalk14.bold("Usage:"));
|
|
4297
4895
|
console.log(" vibgrate push [options]");
|
|
4298
4896
|
console.log("");
|
|
4299
|
-
console.log(
|
|
4300
|
-
console.log(` ${
|
|
4301
|
-
console.log(` ${
|
|
4302
|
-
console.log(` ${
|
|
4303
|
-
console.log(` ${
|
|
4897
|
+
console.log(chalk14.bold("Options:"));
|
|
4898
|
+
console.log(` ${chalk14.cyan("--dsn <dsn>")} DSN token (or set VIBGRATE_DSN env var)`);
|
|
4899
|
+
console.log(` ${chalk14.cyan("--file <file>")} Artifact to upload (default: .vibgrate/scan_result.json)`);
|
|
4900
|
+
console.log(` ${chalk14.cyan("--region <region>")} Override data residency region: us | eu`);
|
|
4901
|
+
console.log(` ${chalk14.cyan("--strict")} Fail with non-zero exit code on upload error`);
|
|
4304
4902
|
console.log("");
|
|
4305
|
-
console.log(
|
|
4903
|
+
console.log(chalk14.bold("Examples:"));
|
|
4306
4904
|
console.log(" vibgrate push --dsn $VIBGRATE_DSN");
|
|
4307
4905
|
console.log(" vibgrate push --file ./ci/scan_result.json --strict");
|
|
4308
4906
|
},
|
|
4309
4907
|
dsn: () => {
|
|
4310
4908
|
console.log("");
|
|
4311
|
-
console.log(
|
|
4909
|
+
console.log(chalk14.bold.underline("vibgrate dsn") + chalk14.dim(" \u2014 Manage DSN tokens for API authentication"));
|
|
4312
4910
|
console.log("");
|
|
4313
|
-
console.log(
|
|
4314
|
-
console.log(` ${
|
|
4911
|
+
console.log(chalk14.bold("Subcommands:"));
|
|
4912
|
+
console.log(` ${chalk14.cyan("vibgrate dsn create")} Generate a new DSN token`);
|
|
4315
4913
|
console.log("");
|
|
4316
|
-
console.log(
|
|
4317
|
-
console.log(` ${
|
|
4318
|
-
console.log(` ${
|
|
4319
|
-
console.log(` ${
|
|
4320
|
-
console.log(` ${
|
|
4914
|
+
console.log(chalk14.bold("dsn create options:"));
|
|
4915
|
+
console.log(` ${chalk14.cyan("--workspace <id>")} Workspace ID or "new" to auto-generate ${chalk14.red("(required)")}`);
|
|
4916
|
+
console.log(` ${chalk14.cyan("--region <region>")} Data residency region: us | eu (default: us)`);
|
|
4917
|
+
console.log(` ${chalk14.cyan("--ingest <url>")} Override ingest API URL`);
|
|
4918
|
+
console.log(` ${chalk14.cyan("--write <path>")} Write the DSN to a file (add to .gitignore!)`);
|
|
4321
4919
|
console.log("");
|
|
4322
|
-
console.log(
|
|
4920
|
+
console.log(chalk14.bold("Examples:"));
|
|
4323
4921
|
console.log(" vibgrate dsn create --workspace new");
|
|
4324
4922
|
console.log(" vibgrate dsn create --workspace abc123");
|
|
4325
4923
|
console.log(" vibgrate dsn create --workspace new --region eu --write .vibgrate/.dsn");
|
|
4326
4924
|
},
|
|
4327
4925
|
update: () => {
|
|
4328
4926
|
console.log("");
|
|
4329
|
-
console.log(
|
|
4927
|
+
console.log(chalk14.bold.underline("vibgrate update") + chalk14.dim(" \u2014 Update the vibgrate CLI to the latest version"));
|
|
4330
4928
|
console.log("");
|
|
4331
|
-
console.log(
|
|
4929
|
+
console.log(chalk14.bold("Usage:"));
|
|
4332
4930
|
console.log(" vibgrate update [options]");
|
|
4333
4931
|
console.log("");
|
|
4334
|
-
console.log(
|
|
4335
|
-
console.log(` ${
|
|
4336
|
-
console.log(` ${
|
|
4932
|
+
console.log(chalk14.bold("Options:"));
|
|
4933
|
+
console.log(` ${chalk14.cyan("--check")} Check for a newer version without installing`);
|
|
4934
|
+
console.log(` ${chalk14.cyan("--pm <manager>")} Force a package manager: npm | pnpm | yarn | bun`);
|
|
4337
4935
|
console.log("");
|
|
4338
|
-
console.log(
|
|
4936
|
+
console.log(chalk14.bold("Examples:"));
|
|
4339
4937
|
console.log(" vibgrate update");
|
|
4340
4938
|
console.log(" vibgrate update --check");
|
|
4341
4939
|
console.log(" vibgrate update --pm pnpm");
|
|
@@ -4343,40 +4941,42 @@ var detailedHelp = {
|
|
|
4343
4941
|
};
|
|
4344
4942
|
function printSummaryHelp() {
|
|
4345
4943
|
console.log("");
|
|
4346
|
-
console.log(
|
|
4944
|
+
console.log(chalk14.bold("vibgrate") + chalk14.dim(" \u2014 Continuous Drift Intelligence"));
|
|
4347
4945
|
console.log("");
|
|
4348
|
-
console.log(
|
|
4946
|
+
console.log(chalk14.bold("Usage:"));
|
|
4349
4947
|
console.log(" vibgrate <command> [options]");
|
|
4350
4948
|
console.log(" vibgrate help [command] Show detailed help for a command");
|
|
4351
4949
|
console.log("");
|
|
4352
|
-
console.log(
|
|
4353
|
-
console.log(` ${
|
|
4950
|
+
console.log(chalk14.bold("Getting started:"));
|
|
4951
|
+
console.log(` ${chalk14.cyan("init")} Initialise vibgrate in a project (creates config & .vibgrate/ dir)`);
|
|
4952
|
+
console.log(` ${chalk14.cyan("login")} Sign the CLI into your workspace via the browser`);
|
|
4953
|
+
console.log(` ${chalk14.cyan("logout")} Clear stored login credentials`);
|
|
4354
4954
|
console.log("");
|
|
4355
|
-
console.log(
|
|
4356
|
-
console.log(` ${
|
|
4357
|
-
console.log(` ${
|
|
4955
|
+
console.log(chalk14.bold("Core scanning:"));
|
|
4956
|
+
console.log(` ${chalk14.cyan("scan")} Scan a project for upgrade drift and generate a report`);
|
|
4957
|
+
console.log(` ${chalk14.cyan("baseline")} Save a baseline snapshot to compare future scans against`);
|
|
4358
4958
|
console.log("");
|
|
4359
|
-
console.log(
|
|
4360
|
-
console.log(` ${
|
|
4361
|
-
console.log(` ${
|
|
4959
|
+
console.log(chalk14.bold("Reporting & export:"));
|
|
4960
|
+
console.log(` ${chalk14.cyan("report")} Re-generate a report from a previously saved scan artifact`);
|
|
4961
|
+
console.log(` ${chalk14.cyan("sbom")} Export a Software Bill of Materials (CycloneDX or SPDX)`);
|
|
4362
4962
|
console.log("");
|
|
4363
|
-
console.log(
|
|
4364
|
-
console.log(` ${
|
|
4365
|
-
console.log(` ${
|
|
4963
|
+
console.log(chalk14.bold("CI/CD integration:"));
|
|
4964
|
+
console.log(` ${chalk14.cyan("push")} Upload a scan artifact to the Vibgrate API`);
|
|
4965
|
+
console.log(` ${chalk14.cyan("dsn")} Create and manage DSN tokens for API authentication`);
|
|
4366
4966
|
console.log("");
|
|
4367
|
-
console.log(
|
|
4368
|
-
console.log(` ${
|
|
4967
|
+
console.log(chalk14.bold("Maintenance:"));
|
|
4968
|
+
console.log(` ${chalk14.cyan("update")} Update the vibgrate CLI to the latest version`);
|
|
4369
4969
|
console.log("");
|
|
4370
|
-
console.log(
|
|
4970
|
+
console.log(chalk14.dim("Run") + ` ${chalk14.cyan("vibgrate help <command>")} ` + chalk14.dim("for detailed options, e.g.") + ` ${chalk14.cyan("vibgrate help scan")}`);
|
|
4371
4971
|
}
|
|
4372
|
-
var helpCommand = new
|
|
4972
|
+
var helpCommand = new Command12("help").description("Show help for vibgrate commands").argument("[command]", "Command to show detailed help for").helpOption(false).action((cmd) => {
|
|
4373
4973
|
const name = cmd?.toLowerCase().trim();
|
|
4374
4974
|
if (name && detailedHelp[name]) {
|
|
4375
4975
|
detailedHelp[name]();
|
|
4376
4976
|
} else if (name) {
|
|
4377
4977
|
console.log("");
|
|
4378
|
-
console.log(
|
|
4379
|
-
console.log(
|
|
4978
|
+
console.log(chalk14.red(`Unknown command: ${name}`));
|
|
4979
|
+
console.log(chalk14.dim(`Available commands: ${Object.keys(detailedHelp).join(", ")}`));
|
|
4380
4980
|
printSummaryHelp();
|
|
4381
4981
|
} else {
|
|
4382
4982
|
printSummaryHelp();
|
|
@@ -4385,13 +4985,15 @@ var helpCommand = new Command10("help").description("Show help for vibgrate comm
|
|
|
4385
4985
|
});
|
|
4386
4986
|
|
|
4387
4987
|
// src/cli.ts
|
|
4388
|
-
var program = new
|
|
4988
|
+
var program = new Command13();
|
|
4389
4989
|
program.name("vibgrate").description("Continuous Drift Intelligence").version(VERSION).addHelpText("after", "\nSee https://vibgrate.com/help for more guidance");
|
|
4390
4990
|
program.addCommand(helpCommand);
|
|
4391
4991
|
program.addCommand(initCommand);
|
|
4392
4992
|
program.addCommand(scanCommand);
|
|
4393
4993
|
program.addCommand(baselineCommand);
|
|
4394
4994
|
program.addCommand(reportCommand);
|
|
4995
|
+
program.addCommand(loginCommand);
|
|
4996
|
+
program.addCommand(logoutCommand);
|
|
4395
4997
|
program.addCommand(dsnCommand);
|
|
4396
4998
|
program.addCommand(pushCommand);
|
|
4397
4999
|
program.addCommand(updateCommand);
|
|
@@ -4402,8 +5004,8 @@ function notifyIfUpdateAvailable() {
|
|
|
4402
5004
|
void checkForUpdate().then((update) => {
|
|
4403
5005
|
if (!update?.updateAvailable) return;
|
|
4404
5006
|
console.error("");
|
|
4405
|
-
console.error(
|
|
4406
|
-
console.error(
|
|
5007
|
+
console.error(chalk15.yellow(` Update available: ${update.current} \u2192 ${update.latest}`));
|
|
5008
|
+
console.error(chalk15.dim(' Run "vibgrate update" to install the latest version.'));
|
|
4407
5009
|
console.error("");
|
|
4408
5010
|
}).catch(() => {
|
|
4409
5011
|
});
|