@vibgrate/cli 2026.615.4 → 2026.617.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 chalk14 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,7 +60,7 @@ 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
65
|
import chalk3 from "chalk";
|
|
66
66
|
|
|
@@ -210,6 +210,49 @@ dsnCommand.command("create").description("Create a new DSN token").option("--ing
|
|
|
210
210
|
}
|
|
211
211
|
});
|
|
212
212
|
|
|
213
|
+
// src/credentials.ts
|
|
214
|
+
import * as os from "os";
|
|
215
|
+
import * as path3 from "path";
|
|
216
|
+
import * as fs from "fs";
|
|
217
|
+
function credentialsDir() {
|
|
218
|
+
return path3.join(os.homedir(), ".vibgrate");
|
|
219
|
+
}
|
|
220
|
+
function credentialsPath() {
|
|
221
|
+
return path3.join(credentialsDir(), "credentials.json");
|
|
222
|
+
}
|
|
223
|
+
function readStoredCredentials() {
|
|
224
|
+
try {
|
|
225
|
+
const raw = fs.readFileSync(credentialsPath(), "utf8");
|
|
226
|
+
const parsed = JSON.parse(raw);
|
|
227
|
+
return parsed && typeof parsed.dsn === "string" && parsed.dsn ? parsed : null;
|
|
228
|
+
} catch {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function writeStoredCredentials(creds) {
|
|
233
|
+
const dir = credentialsDir();
|
|
234
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
235
|
+
const file = credentialsPath();
|
|
236
|
+
fs.writeFileSync(file, JSON.stringify(creds, null, 2) + "\n", "utf8");
|
|
237
|
+
try {
|
|
238
|
+
fs.chmodSync(file, 384);
|
|
239
|
+
} catch {
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function clearStoredCredentials() {
|
|
243
|
+
try {
|
|
244
|
+
fs.rmSync(credentialsPath());
|
|
245
|
+
return true;
|
|
246
|
+
} catch {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function resolveDsn(explicitDsn) {
|
|
251
|
+
if (explicitDsn) return explicitDsn;
|
|
252
|
+
if (process.env.VIBGRATE_DSN) return process.env.VIBGRATE_DSN;
|
|
253
|
+
return readStoredCredentials()?.dsn ?? void 0;
|
|
254
|
+
}
|
|
255
|
+
|
|
213
256
|
// src/utils/ingest-id-output.ts
|
|
214
257
|
function emitIngestIdLine(ingestId, options) {
|
|
215
258
|
console.log(`VIBGRATE_INGEST_ID=${ingestId}`);
|
|
@@ -220,10 +263,10 @@ function emitIngestIdLine(ingestId, options) {
|
|
|
220
263
|
|
|
221
264
|
// src/commands/scan.ts
|
|
222
265
|
async function autoPush(artifact, rootDir, opts) {
|
|
223
|
-
const dsn = opts.dsn
|
|
266
|
+
const dsn = resolveDsn(opts.dsn);
|
|
224
267
|
if (!dsn) {
|
|
225
268
|
console.error(chalk3.red("No DSN provided for push."));
|
|
226
|
-
console.error(chalk3.dim("
|
|
269
|
+
console.error(chalk3.dim('Run "vibgrate login", set VIBGRATE_DSN, or use the --dsn flag.'));
|
|
227
270
|
if (opts.strict) process.exit(1);
|
|
228
271
|
return;
|
|
229
272
|
}
|
|
@@ -320,15 +363,15 @@ var scanCommand = new Command3("scan").description("Scan a project for upgrade d
|
|
|
320
363
|
collectExcludes,
|
|
321
364
|
[]
|
|
322
365
|
).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 =
|
|
366
|
+
const rootDir = path4.resolve(targetPath);
|
|
324
367
|
if (!await pathExists2(rootDir)) {
|
|
325
368
|
console.error(chalk3.red(`Path does not exist: ${rootDir}`));
|
|
326
369
|
process.exit(1);
|
|
327
370
|
}
|
|
328
|
-
const hasDsn = !!(opts.dsn
|
|
371
|
+
const hasDsn = !!resolveDsn(opts.dsn);
|
|
329
372
|
const willPush = !opts.offline && (opts.push || hasDsn);
|
|
330
373
|
if (willPush && hasDsn) {
|
|
331
|
-
const dsn = opts.dsn
|
|
374
|
+
const dsn = resolveDsn(opts.dsn);
|
|
332
375
|
const parsed = parseDsn(dsn);
|
|
333
376
|
if (parsed) {
|
|
334
377
|
const ingestHost = opts.region ? resolveIngestHost(opts.region) : parsed.host;
|
|
@@ -340,6 +383,15 @@ var scanCommand = new Command3("scan").description("Scan a project for upgrade d
|
|
|
340
383
|
repositoryName,
|
|
341
384
|
vcsSha: fingerprint.vcsSha
|
|
342
385
|
});
|
|
386
|
+
if (preflight.vm && !preflight.vm.allowed) {
|
|
387
|
+
console.error(chalk3.red(preflight.error ?? "VM meter usage exhausted"));
|
|
388
|
+
console.error(
|
|
389
|
+
chalk3.dim(
|
|
390
|
+
`VM minutes: ${preflight.vm.used}/${preflight.vm.limit} (${preflight.plan.label} plan) \u2014 enable overages or upgrade your plan.`
|
|
391
|
+
)
|
|
392
|
+
);
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
343
395
|
if (preflight.status === "error" || !preflight.scans.allowed) {
|
|
344
396
|
console.error(chalk3.red(preflight.error ?? "Scan ingestion not allowed for this workspace."));
|
|
345
397
|
console.error(
|
|
@@ -440,7 +492,7 @@ Failing fitness function: drift worsened by ${worseningPercent.toFixed(2)}% (thr
|
|
|
440
492
|
});
|
|
441
493
|
|
|
442
494
|
// src/commands/report.ts
|
|
443
|
-
import * as
|
|
495
|
+
import * as path5 from "path";
|
|
444
496
|
import { Command as Command4 } from "commander";
|
|
445
497
|
import chalk5 from "chalk";
|
|
446
498
|
|
|
@@ -1157,7 +1209,7 @@ function formatMarkdown(artifact) {
|
|
|
1157
1209
|
|
|
1158
1210
|
// src/commands/report.ts
|
|
1159
1211
|
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 =
|
|
1212
|
+
const artifactPath = path5.resolve(opts.in);
|
|
1161
1213
|
if (!await pathExists(artifactPath)) {
|
|
1162
1214
|
console.error(chalk5.red(`Artifact not found: ${artifactPath}`));
|
|
1163
1215
|
console.error(chalk5.dim('Run "vibgrate scan" first to generate a scan artifact.'));
|
|
@@ -1178,12 +1230,146 @@ var reportCommand = new Command4("report").description("Generate a drift report
|
|
|
1178
1230
|
}
|
|
1179
1231
|
});
|
|
1180
1232
|
|
|
1181
|
-
// src/commands/
|
|
1182
|
-
import * as crypto2 from "crypto";
|
|
1183
|
-
import * as path5 from "path";
|
|
1233
|
+
// src/commands/login.ts
|
|
1184
1234
|
import { Command as Command5 } from "commander";
|
|
1185
1235
|
import chalk6 from "chalk";
|
|
1186
1236
|
|
|
1237
|
+
// src/utils/open-url.ts
|
|
1238
|
+
import { spawn } from "child_process";
|
|
1239
|
+
function openUrl(url) {
|
|
1240
|
+
const platform = process.platform;
|
|
1241
|
+
let command;
|
|
1242
|
+
let args;
|
|
1243
|
+
if (platform === "darwin") {
|
|
1244
|
+
command = "open";
|
|
1245
|
+
args = [url];
|
|
1246
|
+
} else if (platform === "win32") {
|
|
1247
|
+
command = "cmd";
|
|
1248
|
+
args = ["/c", "start", "", url];
|
|
1249
|
+
} else {
|
|
1250
|
+
command = "xdg-open";
|
|
1251
|
+
args = [url];
|
|
1252
|
+
}
|
|
1253
|
+
try {
|
|
1254
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
1255
|
+
child.on("error", () => {
|
|
1256
|
+
});
|
|
1257
|
+
child.unref();
|
|
1258
|
+
return true;
|
|
1259
|
+
} catch {
|
|
1260
|
+
return false;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// src/commands/login.ts
|
|
1265
|
+
var delay = (ms) => new Promise((resolve10) => setTimeout(resolve10, ms));
|
|
1266
|
+
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) => {
|
|
1267
|
+
let ingestHost;
|
|
1268
|
+
try {
|
|
1269
|
+
ingestHost = resolveIngestHost(opts.region, opts.ingest);
|
|
1270
|
+
} catch (e) {
|
|
1271
|
+
console.error(chalk6.red(e instanceof Error ? e.message : String(e)));
|
|
1272
|
+
process.exit(1);
|
|
1273
|
+
}
|
|
1274
|
+
const base = `https://${ingestHost}/v1/auth/device`;
|
|
1275
|
+
let start;
|
|
1276
|
+
try {
|
|
1277
|
+
const res = await fetch(`${base}/start`, {
|
|
1278
|
+
method: "POST",
|
|
1279
|
+
headers: { "Content-Type": "application/json", Connection: "close" },
|
|
1280
|
+
body: "{}"
|
|
1281
|
+
});
|
|
1282
|
+
if (!res.ok) {
|
|
1283
|
+
console.error(chalk6.red(`Failed to start login (HTTP ${res.status}).`));
|
|
1284
|
+
process.exit(1);
|
|
1285
|
+
}
|
|
1286
|
+
start = await res.json();
|
|
1287
|
+
} catch (e) {
|
|
1288
|
+
console.error(chalk6.red(`Could not reach ${ingestHost}: ${e instanceof Error ? e.message : String(e)}`));
|
|
1289
|
+
process.exit(1);
|
|
1290
|
+
}
|
|
1291
|
+
console.log("");
|
|
1292
|
+
console.log("To finish signing in, open this URL and approve the request:");
|
|
1293
|
+
console.log("");
|
|
1294
|
+
console.log(" " + chalk6.cyan(start.verificationUri));
|
|
1295
|
+
console.log("");
|
|
1296
|
+
console.log(" Your code: " + chalk6.bold(start.userCode));
|
|
1297
|
+
console.log("");
|
|
1298
|
+
if (opts.browser) {
|
|
1299
|
+
const opened = openUrl(start.verificationUriComplete);
|
|
1300
|
+
if (opened) {
|
|
1301
|
+
console.log(chalk6.dim("Opening your browser\u2026 (if it does not open, use the URL above)"));
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
console.log(chalk6.dim("Waiting for approval\u2026"));
|
|
1305
|
+
const intervalMs = Math.max(2, start.interval || 5) * 1e3;
|
|
1306
|
+
const deadline = Date.now() + (start.expiresIn || 900) * 1e3;
|
|
1307
|
+
while (Date.now() < deadline) {
|
|
1308
|
+
await delay(intervalMs);
|
|
1309
|
+
let token;
|
|
1310
|
+
try {
|
|
1311
|
+
const res = await fetch(`${base}/token`, {
|
|
1312
|
+
method: "POST",
|
|
1313
|
+
headers: { "Content-Type": "application/json", Connection: "close" },
|
|
1314
|
+
body: JSON.stringify({ deviceCode: start.deviceCode })
|
|
1315
|
+
});
|
|
1316
|
+
token = await res.json();
|
|
1317
|
+
} catch {
|
|
1318
|
+
continue;
|
|
1319
|
+
}
|
|
1320
|
+
if (token.status === "authorization_pending") continue;
|
|
1321
|
+
if (token.status === "complete" && token.dsn) {
|
|
1322
|
+
writeStoredCredentials({
|
|
1323
|
+
dsn: token.dsn,
|
|
1324
|
+
workspaceId: token.workspaceId,
|
|
1325
|
+
keyId: token.keyId,
|
|
1326
|
+
ingestHost: token.ingestHost ?? ingestHost,
|
|
1327
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1328
|
+
});
|
|
1329
|
+
console.log("");
|
|
1330
|
+
console.log(chalk6.green("\u2714") + " Logged in.");
|
|
1331
|
+
if (token.workspaceId) {
|
|
1332
|
+
console.log(" Workspace: " + chalk6.bold(token.workspaceId));
|
|
1333
|
+
}
|
|
1334
|
+
console.log(chalk6.dim(` Credentials saved to ${credentialsPath()}`));
|
|
1335
|
+
console.log(chalk6.dim(' You can now run "vibgrate scan --push".'));
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
if (token.status === "access_denied") {
|
|
1339
|
+
console.error(chalk6.red("\u2716 Login was denied in the browser."));
|
|
1340
|
+
process.exit(1);
|
|
1341
|
+
}
|
|
1342
|
+
if (token.status === "expired" || token.status === "invalid") {
|
|
1343
|
+
console.error(chalk6.red('\u2716 Login request expired. Run "vibgrate login" again.'));
|
|
1344
|
+
process.exit(1);
|
|
1345
|
+
}
|
|
1346
|
+
if (token.status === "error") {
|
|
1347
|
+
console.error(chalk6.red(`\u2716 ${token.error ?? "Login failed."}`));
|
|
1348
|
+
process.exit(1);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
console.error(chalk6.red('\u2716 Timed out waiting for approval. Run "vibgrate login" again.'));
|
|
1352
|
+
process.exit(1);
|
|
1353
|
+
});
|
|
1354
|
+
|
|
1355
|
+
// src/commands/logout.ts
|
|
1356
|
+
import { Command as Command6 } from "commander";
|
|
1357
|
+
import chalk7 from "chalk";
|
|
1358
|
+
var logoutCommand = new Command6("logout").description("Clear stored Vibgrate login credentials").action(() => {
|
|
1359
|
+
const cleared = clearStoredCredentials();
|
|
1360
|
+
if (cleared) {
|
|
1361
|
+
console.log(chalk7.green("\u2714") + " Logged out. Stored credentials removed.");
|
|
1362
|
+
} else {
|
|
1363
|
+
console.log(chalk7.dim(`No stored credentials found at ${credentialsPath()}.`));
|
|
1364
|
+
}
|
|
1365
|
+
});
|
|
1366
|
+
|
|
1367
|
+
// src/commands/push.ts
|
|
1368
|
+
import * as crypto2 from "crypto";
|
|
1369
|
+
import * as path6 from "path";
|
|
1370
|
+
import { Command as Command7 } from "commander";
|
|
1371
|
+
import chalk8 from "chalk";
|
|
1372
|
+
|
|
1187
1373
|
// src/utils/compact-artifact.ts
|
|
1188
1374
|
import * as zlib from "zlib";
|
|
1189
1375
|
import { promisify } from "util";
|
|
@@ -1454,25 +1640,25 @@ function parseDsn2(dsn) {
|
|
|
1454
1640
|
function computeHmac(body, secret) {
|
|
1455
1641
|
return crypto2.createHmac("sha256", secret).update(body).digest("base64");
|
|
1456
1642
|
}
|
|
1457
|
-
var pushCommand = new
|
|
1458
|
-
const dsn = opts.dsn
|
|
1643
|
+
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) => {
|
|
1644
|
+
const dsn = resolveDsn(opts.dsn);
|
|
1459
1645
|
if (!dsn) {
|
|
1460
|
-
console.error(
|
|
1461
|
-
console.error(
|
|
1646
|
+
console.error(chalk8.red("No DSN provided."));
|
|
1647
|
+
console.error(chalk8.dim('Run "vibgrate login", set VIBGRATE_DSN, or use the --dsn flag.'));
|
|
1462
1648
|
if (opts.strict) process.exit(1);
|
|
1463
1649
|
return;
|
|
1464
1650
|
}
|
|
1465
1651
|
const parsed = parseDsn2(dsn);
|
|
1466
1652
|
if (!parsed) {
|
|
1467
|
-
console.error(
|
|
1468
|
-
console.error(
|
|
1653
|
+
console.error(chalk8.red("Invalid DSN format."));
|
|
1654
|
+
console.error(chalk8.dim("Expected: vibgrate+https://<key_id>:<secret>@<host>/<workspace_id>"));
|
|
1469
1655
|
if (opts.strict) process.exit(1);
|
|
1470
1656
|
return;
|
|
1471
1657
|
}
|
|
1472
|
-
const filePath =
|
|
1658
|
+
const filePath = path6.resolve(opts.file);
|
|
1473
1659
|
if (!await pathExists(filePath)) {
|
|
1474
|
-
console.error(
|
|
1475
|
-
console.error(
|
|
1660
|
+
console.error(chalk8.red(`Scan artifact not found: ${filePath}`));
|
|
1661
|
+
console.error(chalk8.dim('Run "vibgrate scan" first.'));
|
|
1476
1662
|
if (opts.strict) process.exit(1);
|
|
1477
1663
|
return;
|
|
1478
1664
|
}
|
|
@@ -1484,7 +1670,7 @@ var pushCommand = new Command5("push").description("Push scan results to Vibgrat
|
|
|
1484
1670
|
try {
|
|
1485
1671
|
host = resolveIngestHost(opts.region);
|
|
1486
1672
|
} catch (e) {
|
|
1487
|
-
console.error(
|
|
1673
|
+
console.error(chalk8.red(e instanceof Error ? e.message : String(e)));
|
|
1488
1674
|
if (opts.strict) process.exit(1);
|
|
1489
1675
|
return;
|
|
1490
1676
|
}
|
|
@@ -1493,7 +1679,7 @@ var pushCommand = new Command5("push").description("Push scan results to Vibgrat
|
|
|
1493
1679
|
const originalSize = JSON.stringify(artifact).length;
|
|
1494
1680
|
const compressedSize = body.length;
|
|
1495
1681
|
const ratio = ((1 - compressedSize / originalSize) * 100).toFixed(0);
|
|
1496
|
-
console.log(
|
|
1682
|
+
console.log(chalk8.dim(`Uploading to ${host}... (${(compressedSize / 1024).toFixed(0)} KB, ${ratio}% smaller)`));
|
|
1497
1683
|
try {
|
|
1498
1684
|
const response = await fetch(url, {
|
|
1499
1685
|
method: "POST",
|
|
@@ -1512,36 +1698,36 @@ var pushCommand = new Command5("push").description("Push scan results to Vibgrat
|
|
|
1512
1698
|
throw new Error(`HTTP ${response.status}: ${text}`);
|
|
1513
1699
|
}
|
|
1514
1700
|
const result = await response.json();
|
|
1515
|
-
console.log(
|
|
1701
|
+
console.log(chalk8.green("\u2714") + ` Scan queued for processing (${result.ingestId ?? "ok"})`);
|
|
1516
1702
|
console.log();
|
|
1517
|
-
console.log(
|
|
1703
|
+
console.log(chalk8.dim("Processing continues in the background. Results available shortly."));
|
|
1518
1704
|
console.log();
|
|
1519
1705
|
if (result.ingestId) {
|
|
1520
1706
|
const dashHost = dashHostForIngestHost(host);
|
|
1521
1707
|
const reportUrl = `https://${dashHost}/${parsed.workspaceId}/scan/${result.ingestId}`;
|
|
1522
|
-
console.log(
|
|
1708
|
+
console.log(chalk8.dim("View report: ") + chalk8.underline(reportUrl));
|
|
1523
1709
|
}
|
|
1524
1710
|
} catch (e) {
|
|
1525
1711
|
const msg = e instanceof Error ? e.message : String(e);
|
|
1526
|
-
console.error(
|
|
1712
|
+
console.error(chalk8.red(`Upload failed: ${msg}`));
|
|
1527
1713
|
if (opts.strict) process.exit(1);
|
|
1528
1714
|
}
|
|
1529
1715
|
});
|
|
1530
1716
|
|
|
1531
1717
|
// src/commands/update.ts
|
|
1532
1718
|
import { execSync } from "child_process";
|
|
1533
|
-
import * as
|
|
1534
|
-
import { Command as
|
|
1535
|
-
import
|
|
1719
|
+
import * as path8 from "path";
|
|
1720
|
+
import { Command as Command8 } from "commander";
|
|
1721
|
+
import chalk9 from "chalk";
|
|
1536
1722
|
|
|
1537
1723
|
// src/utils/update-check.ts
|
|
1538
1724
|
var import_semver = __toESM(require_semver(), 1);
|
|
1539
|
-
import * as
|
|
1540
|
-
import * as
|
|
1541
|
-
import * as
|
|
1725
|
+
import * as fs2 from "fs/promises";
|
|
1726
|
+
import * as path7 from "path";
|
|
1727
|
+
import * as os2 from "os";
|
|
1542
1728
|
var REGISTRY_URL = "https://registry.npmjs.org/@vibgrate%2fcli/latest";
|
|
1543
|
-
var CACHE_DIR =
|
|
1544
|
-
var CACHE_FILE =
|
|
1729
|
+
var CACHE_DIR = path7.join(os2.homedir(), ".vibgrate");
|
|
1730
|
+
var CACHE_FILE = path7.join(CACHE_DIR, "update-check.json");
|
|
1545
1731
|
var CHECK_INTERVAL_MS = 12 * 60 * 60 * 1e3;
|
|
1546
1732
|
async function checkForUpdate() {
|
|
1547
1733
|
try {
|
|
@@ -1605,7 +1791,7 @@ async function fetchLatestVersion() {
|
|
|
1605
1791
|
}
|
|
1606
1792
|
async function readCache() {
|
|
1607
1793
|
try {
|
|
1608
|
-
const raw = await
|
|
1794
|
+
const raw = await fs2.readFile(CACHE_FILE, "utf-8");
|
|
1609
1795
|
const data = JSON.parse(raw);
|
|
1610
1796
|
if (data.latest && typeof data.checkedAt === "number") return data;
|
|
1611
1797
|
return null;
|
|
@@ -1615,8 +1801,8 @@ async function readCache() {
|
|
|
1615
1801
|
}
|
|
1616
1802
|
async function writeCache(data) {
|
|
1617
1803
|
try {
|
|
1618
|
-
await
|
|
1619
|
-
await
|
|
1804
|
+
await fs2.mkdir(CACHE_DIR, { recursive: true });
|
|
1805
|
+
await fs2.writeFile(CACHE_FILE, JSON.stringify(data), "utf-8");
|
|
1620
1806
|
} catch {
|
|
1621
1807
|
}
|
|
1622
1808
|
}
|
|
@@ -1649,9 +1835,9 @@ function getGlobalUpdateCommand(pm, pkg2, version) {
|
|
|
1649
1835
|
}
|
|
1650
1836
|
}
|
|
1651
1837
|
async function detectPackageManager(cwd) {
|
|
1652
|
-
if (await pathExists(
|
|
1653
|
-
if (await pathExists(
|
|
1654
|
-
if (await pathExists(
|
|
1838
|
+
if (await pathExists(path8.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
1839
|
+
if (await pathExists(path8.join(cwd, "bun.lockb"))) return "bun";
|
|
1840
|
+
if (await pathExists(path8.join(cwd, "yarn.lock"))) return "yarn";
|
|
1655
1841
|
return "npm";
|
|
1656
1842
|
}
|
|
1657
1843
|
function getInstallCommand(pm, pkg2, version, isDev) {
|
|
@@ -1670,7 +1856,7 @@ function getInstallCommand(pm, pkg2, version, isDev) {
|
|
|
1670
1856
|
}
|
|
1671
1857
|
async function isDevDependency(cwd) {
|
|
1672
1858
|
try {
|
|
1673
|
-
const pkgPath =
|
|
1859
|
+
const pkgPath = path8.join(cwd, "package.json");
|
|
1674
1860
|
const raw = await (await import("fs/promises")).readFile(pkgPath, "utf-8");
|
|
1675
1861
|
const pkg2 = JSON.parse(raw);
|
|
1676
1862
|
return Boolean(pkg2.devDependencies?.["@vibgrate/cli"]);
|
|
@@ -1678,22 +1864,22 @@ async function isDevDependency(cwd) {
|
|
|
1678
1864
|
return true;
|
|
1679
1865
|
}
|
|
1680
1866
|
}
|
|
1681
|
-
var updateCommand = new
|
|
1682
|
-
console.log(
|
|
1683
|
-
console.log(
|
|
1867
|
+
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) => {
|
|
1868
|
+
console.log(chalk9.dim(`Current version: ${VERSION}`));
|
|
1869
|
+
console.log(chalk9.dim("Checking npm registry..."));
|
|
1684
1870
|
const latest = await fetchLatestVersion();
|
|
1685
1871
|
if (!latest) {
|
|
1686
|
-
console.error(
|
|
1872
|
+
console.error(chalk9.red("Could not reach the npm registry. Check your network connection."));
|
|
1687
1873
|
process.exit(1);
|
|
1688
1874
|
}
|
|
1689
1875
|
const semver2 = await import("./semver-2FJFIYVN.js");
|
|
1690
1876
|
if (!semver2.gt(latest, VERSION)) {
|
|
1691
|
-
console.log(
|
|
1877
|
+
console.log(chalk9.green("\u2714") + ` You are on the latest version (${VERSION}).`);
|
|
1692
1878
|
return;
|
|
1693
1879
|
}
|
|
1694
|
-
console.log(
|
|
1880
|
+
console.log(chalk9.yellow(`Update available: ${VERSION} \u2192 ${latest}`));
|
|
1695
1881
|
if (opts.check) {
|
|
1696
|
-
console.log(
|
|
1882
|
+
console.log(chalk9.dim('Run "vibgrate update" to install.'));
|
|
1697
1883
|
return;
|
|
1698
1884
|
}
|
|
1699
1885
|
const cwd = process.cwd();
|
|
@@ -1703,26 +1889,26 @@ var updateCommand = new Command6("update").description("Update vibgrate to the l
|
|
|
1703
1889
|
let cmd;
|
|
1704
1890
|
if (isGlobal) {
|
|
1705
1891
|
cmd = getGlobalUpdateCommand(pm, "@vibgrate/cli", latest);
|
|
1706
|
-
console.log(
|
|
1892
|
+
console.log(chalk9.dim(`Updating global installation with ${pm}: ${cmd}`));
|
|
1707
1893
|
} else {
|
|
1708
1894
|
const isDev = await isDevDependency(cwd);
|
|
1709
1895
|
cmd = getInstallCommand(pm, "@vibgrate/cli", latest, isDev);
|
|
1710
|
-
console.log(
|
|
1896
|
+
console.log(chalk9.dim(`Using ${pm}: ${cmd}`));
|
|
1711
1897
|
}
|
|
1712
1898
|
try {
|
|
1713
1899
|
execSync(cmd, { cwd, stdio: "inherit" });
|
|
1714
|
-
console.log(
|
|
1900
|
+
console.log(chalk9.green("\u2714") + ` Updated to @vibgrate/cli@${latest}`);
|
|
1715
1901
|
} catch {
|
|
1716
|
-
console.error(
|
|
1902
|
+
console.error(chalk9.red(`Update failed. Run manually: ${cmd}`));
|
|
1717
1903
|
process.exit(1);
|
|
1718
1904
|
}
|
|
1719
1905
|
});
|
|
1720
1906
|
|
|
1721
1907
|
// src/commands/sbom.ts
|
|
1722
|
-
import * as
|
|
1908
|
+
import * as path9 from "path";
|
|
1723
1909
|
import { randomUUID } from "crypto";
|
|
1724
|
-
import { Command as
|
|
1725
|
-
import
|
|
1910
|
+
import { Command as Command9 } from "commander";
|
|
1911
|
+
import chalk10 from "chalk";
|
|
1726
1912
|
function flattenDependencies(artifact) {
|
|
1727
1913
|
const rows = [];
|
|
1728
1914
|
for (const project of artifact.projects) {
|
|
@@ -1860,50 +2046,50 @@ function formatDeltaText(base, current) {
|
|
|
1860
2046
|
return lines.join("\n");
|
|
1861
2047
|
}
|
|
1862
2048
|
async function readArtifactOrExit(filePath) {
|
|
1863
|
-
const absolutePath =
|
|
2049
|
+
const absolutePath = path9.resolve(filePath);
|
|
1864
2050
|
if (!await pathExists(absolutePath)) {
|
|
1865
|
-
console.error(
|
|
2051
|
+
console.error(chalk10.red(`Artifact not found: ${absolutePath}`));
|
|
1866
2052
|
process.exit(1);
|
|
1867
2053
|
}
|
|
1868
2054
|
return readJsonFile(absolutePath);
|
|
1869
2055
|
}
|
|
1870
|
-
var exportCommand = new
|
|
2056
|
+
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
2057
|
const artifact = await readArtifactOrExit(opts.in);
|
|
1872
2058
|
const format = opts.format.toLowerCase();
|
|
1873
2059
|
if (format !== "cyclonedx" && format !== "spdx") {
|
|
1874
|
-
console.error(
|
|
2060
|
+
console.error(chalk10.red("Invalid SBOM format. Use cyclonedx or spdx."));
|
|
1875
2061
|
process.exit(1);
|
|
1876
2062
|
}
|
|
1877
2063
|
const sbom = format === "cyclonedx" ? toCycloneDx(artifact) : toSpdx(artifact);
|
|
1878
2064
|
const body = JSON.stringify(sbom, null, 2);
|
|
1879
2065
|
if (opts.out) {
|
|
1880
|
-
await writeTextFile(
|
|
1881
|
-
console.log(
|
|
2066
|
+
await writeTextFile(path9.resolve(opts.out), body);
|
|
2067
|
+
console.log(chalk10.green("\u2714") + ` SBOM written to ${opts.out}`);
|
|
1882
2068
|
} else {
|
|
1883
2069
|
console.log(body);
|
|
1884
2070
|
}
|
|
1885
2071
|
});
|
|
1886
|
-
var deltaCommand = new
|
|
2072
|
+
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
2073
|
const base = await readArtifactOrExit(opts.from);
|
|
1888
2074
|
const current = await readArtifactOrExit(opts.to);
|
|
1889
2075
|
const report = formatDeltaText(base, current);
|
|
1890
2076
|
if (opts.out) {
|
|
1891
|
-
await writeTextFile(
|
|
1892
|
-
console.log(
|
|
2077
|
+
await writeTextFile(path9.resolve(opts.out), report);
|
|
2078
|
+
console.log(chalk10.green("\u2714") + ` SBOM delta report written to ${opts.out}`);
|
|
1893
2079
|
} else {
|
|
1894
2080
|
console.log(report);
|
|
1895
2081
|
}
|
|
1896
2082
|
});
|
|
1897
|
-
var sbomCommand = new
|
|
2083
|
+
var sbomCommand = new Command9("sbom").description("SBOM export and delta reports for dependency drift tracking").addCommand(exportCommand).addCommand(deltaCommand);
|
|
1898
2084
|
|
|
1899
2085
|
// 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
|
|
2086
|
+
import * as path12 from "path";
|
|
2087
|
+
import * as os4 from "os";
|
|
2088
|
+
import * as fs4 from "fs/promises";
|
|
2089
|
+
import { existsSync, rmSync as rmSync2 } from "fs";
|
|
2090
|
+
import { spawn as spawn3, spawnSync as spawnSync2 } from "child_process";
|
|
2091
|
+
import { Command as Command10 } from "commander";
|
|
2092
|
+
import chalk11 from "chalk";
|
|
1907
2093
|
|
|
1908
2094
|
// src/behavioural/derive.ts
|
|
1909
2095
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -2756,6 +2942,303 @@ function deriveBehaviouralFacts(facts, opts) {
|
|
|
2756
2942
|
return { envelopes, summary: { assertions: unique.length, surfaceFacts, assertedFacts, behaviouralConfidence } };
|
|
2757
2943
|
}
|
|
2758
2944
|
|
|
2945
|
+
// src/utils/vcs.ts
|
|
2946
|
+
import * as path10 from "path";
|
|
2947
|
+
import * as fs3 from "fs/promises";
|
|
2948
|
+
async function detectVcs2(rootDir) {
|
|
2949
|
+
try {
|
|
2950
|
+
return await detectGit(rootDir);
|
|
2951
|
+
} catch {
|
|
2952
|
+
return { type: "unknown" };
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
async function detectGit(rootDir) {
|
|
2956
|
+
const gitDir = await findGitDir(rootDir);
|
|
2957
|
+
if (!gitDir) {
|
|
2958
|
+
return { type: "unknown" };
|
|
2959
|
+
}
|
|
2960
|
+
const headPath = path10.join(gitDir, "HEAD");
|
|
2961
|
+
let headContent;
|
|
2962
|
+
try {
|
|
2963
|
+
headContent = (await fs3.readFile(headPath, "utf8")).trim();
|
|
2964
|
+
} catch {
|
|
2965
|
+
return { type: "unknown" };
|
|
2966
|
+
}
|
|
2967
|
+
let sha;
|
|
2968
|
+
let branch;
|
|
2969
|
+
if (headContent.startsWith("ref: ")) {
|
|
2970
|
+
const refPath = headContent.slice(5);
|
|
2971
|
+
branch = refPath.startsWith("refs/heads/") ? refPath.slice(11) : refPath;
|
|
2972
|
+
sha = await resolveRef(gitDir, refPath);
|
|
2973
|
+
} else if (/^[0-9a-f]{40}$/i.test(headContent)) {
|
|
2974
|
+
sha = headContent;
|
|
2975
|
+
}
|
|
2976
|
+
const remoteUrl = await readGitRemoteUrl(gitDir);
|
|
2977
|
+
return {
|
|
2978
|
+
type: "git",
|
|
2979
|
+
sha: sha ?? void 0,
|
|
2980
|
+
shortSha: sha ? sha.slice(0, 7) : void 0,
|
|
2981
|
+
branch: branch ?? void 0,
|
|
2982
|
+
remoteUrl
|
|
2983
|
+
};
|
|
2984
|
+
}
|
|
2985
|
+
async function findGitDir(startDir) {
|
|
2986
|
+
let dir = path10.resolve(startDir);
|
|
2987
|
+
const root = path10.parse(dir).root;
|
|
2988
|
+
while (dir !== root) {
|
|
2989
|
+
const gitPath = path10.join(dir, ".git");
|
|
2990
|
+
try {
|
|
2991
|
+
const stat4 = await fs3.stat(gitPath);
|
|
2992
|
+
if (stat4.isDirectory()) {
|
|
2993
|
+
return gitPath;
|
|
2994
|
+
}
|
|
2995
|
+
if (stat4.isFile()) {
|
|
2996
|
+
const content = (await fs3.readFile(gitPath, "utf8")).trim();
|
|
2997
|
+
if (content.startsWith("gitdir: ")) {
|
|
2998
|
+
const resolved = path10.resolve(dir, content.slice(8));
|
|
2999
|
+
return resolved;
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
} catch {
|
|
3003
|
+
}
|
|
3004
|
+
dir = path10.dirname(dir);
|
|
3005
|
+
}
|
|
3006
|
+
return null;
|
|
3007
|
+
}
|
|
3008
|
+
async function resolveRef(gitDir, refPath) {
|
|
3009
|
+
const loosePath = path10.join(gitDir, refPath);
|
|
3010
|
+
try {
|
|
3011
|
+
const sha = (await fs3.readFile(loosePath, "utf8")).trim();
|
|
3012
|
+
if (/^[0-9a-f]{40}$/i.test(sha)) {
|
|
3013
|
+
return sha;
|
|
3014
|
+
}
|
|
3015
|
+
} catch {
|
|
3016
|
+
}
|
|
3017
|
+
const packedPath = path10.join(gitDir, "packed-refs");
|
|
3018
|
+
try {
|
|
3019
|
+
const packed = await fs3.readFile(packedPath, "utf8");
|
|
3020
|
+
for (const line of packed.split("\n")) {
|
|
3021
|
+
if (line.startsWith("#") || line.startsWith("^")) continue;
|
|
3022
|
+
const parts = line.trim().split(" ");
|
|
3023
|
+
if (parts.length >= 2 && parts[1] === refPath) {
|
|
3024
|
+
return parts[0];
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
} catch {
|
|
3028
|
+
}
|
|
3029
|
+
return void 0;
|
|
3030
|
+
}
|
|
3031
|
+
async function readGitRemoteUrl(gitDir) {
|
|
3032
|
+
const configPath = await resolveGitConfigPath(gitDir);
|
|
3033
|
+
if (!configPath) return void 0;
|
|
3034
|
+
try {
|
|
3035
|
+
const config = await fs3.readFile(configPath, "utf8");
|
|
3036
|
+
const originBlock = config.match(/\[remote\s+"origin"\]([\s\S]*?)(?=\n\[|$)/);
|
|
3037
|
+
if (!originBlock) return void 0;
|
|
3038
|
+
const urlMatch = originBlock[1]?.match(/\n\s*url\s*=\s*(.+)\s*/);
|
|
3039
|
+
return urlMatch?.[1]?.trim();
|
|
3040
|
+
} catch {
|
|
3041
|
+
return void 0;
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
async function resolveGitConfigPath(gitDir) {
|
|
3045
|
+
const directConfig = path10.join(gitDir, "config");
|
|
3046
|
+
try {
|
|
3047
|
+
const stat4 = await fs3.stat(directConfig);
|
|
3048
|
+
if (stat4.isFile()) return directConfig;
|
|
3049
|
+
} catch {
|
|
3050
|
+
}
|
|
3051
|
+
const commonDirFile = path10.join(gitDir, "commondir");
|
|
3052
|
+
try {
|
|
3053
|
+
const commonDir = (await fs3.readFile(commonDirFile, "utf8")).trim();
|
|
3054
|
+
if (!commonDir) return void 0;
|
|
3055
|
+
const resolvedCommonDir = path10.resolve(gitDir, commonDir);
|
|
3056
|
+
const commonConfig = path10.join(resolvedCommonDir, "config");
|
|
3057
|
+
const stat4 = await fs3.stat(commonConfig);
|
|
3058
|
+
if (stat4.isFile()) return commonConfig;
|
|
3059
|
+
} catch {
|
|
3060
|
+
return void 0;
|
|
3061
|
+
}
|
|
3062
|
+
return void 0;
|
|
3063
|
+
}
|
|
3064
|
+
|
|
3065
|
+
// src/utils/clone.ts
|
|
3066
|
+
import * as path11 from "path";
|
|
3067
|
+
import * as os3 from "os";
|
|
3068
|
+
import { mkdtemp, rm } from "fs/promises";
|
|
3069
|
+
import { spawn as spawn2, spawnSync } from "child_process";
|
|
3070
|
+
var SCP_LIKE = /^[A-Za-z0-9._-]+@[A-Za-z0-9._-]+:.+$/;
|
|
3071
|
+
function looksLikeRepoUrl(arg) {
|
|
3072
|
+
if (!arg) return false;
|
|
3073
|
+
if (/^(https?|ssh|git):\/\//i.test(arg)) return true;
|
|
3074
|
+
if (SCP_LIKE.test(arg)) return true;
|
|
3075
|
+
return false;
|
|
3076
|
+
}
|
|
3077
|
+
function redactRemoteUrl(url) {
|
|
3078
|
+
if (!url) return url;
|
|
3079
|
+
if (SCP_LIKE.test(url) && !url.includes("://")) return url;
|
|
3080
|
+
try {
|
|
3081
|
+
const parsed = new URL(url);
|
|
3082
|
+
if (parsed.username || parsed.password) {
|
|
3083
|
+
parsed.username = "";
|
|
3084
|
+
parsed.password = "";
|
|
3085
|
+
}
|
|
3086
|
+
return parsed.toString();
|
|
3087
|
+
} catch {
|
|
3088
|
+
return url.replace(/^([a-z]+:\/\/)[^/@]+@/i, "$1");
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
var CloneError = class extends Error {
|
|
3092
|
+
constructor(message) {
|
|
3093
|
+
super(message);
|
|
3094
|
+
this.name = "CloneError";
|
|
3095
|
+
}
|
|
3096
|
+
};
|
|
3097
|
+
function isGitAvailable() {
|
|
3098
|
+
const result = spawnSync("git", ["--version"], { stdio: "ignore" });
|
|
3099
|
+
return !result.error && result.status === 0;
|
|
3100
|
+
}
|
|
3101
|
+
function gitInstallHint() {
|
|
3102
|
+
switch (process.platform) {
|
|
3103
|
+
case "darwin":
|
|
3104
|
+
return "Install git with: brew install git (or xcode-select --install).";
|
|
3105
|
+
case "win32":
|
|
3106
|
+
return "Install git with: winget install Git.Git";
|
|
3107
|
+
default:
|
|
3108
|
+
return "Install git via your distro package manager (e.g. apt install git).";
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
function runGit(args, opts) {
|
|
3112
|
+
return new Promise((resolve10, reject) => {
|
|
3113
|
+
const child = spawn2("git", args, {
|
|
3114
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
3115
|
+
// Never prompt for credentials on a private/forbidden repo — fail fast.
|
|
3116
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_ASKPASS: "echo" }
|
|
3117
|
+
});
|
|
3118
|
+
let stderr = "";
|
|
3119
|
+
let killed = false;
|
|
3120
|
+
const timer = setTimeout(() => {
|
|
3121
|
+
killed = true;
|
|
3122
|
+
child.kill("SIGKILL");
|
|
3123
|
+
}, opts.timeoutMs);
|
|
3124
|
+
child.stderr.on("data", (d) => {
|
|
3125
|
+
if (stderr.length < 8192) stderr += scrubCredentials(String(d));
|
|
3126
|
+
});
|
|
3127
|
+
child.on("error", (err) => {
|
|
3128
|
+
clearTimeout(timer);
|
|
3129
|
+
reject(new CloneError(`Failed to run git: ${err.message}`));
|
|
3130
|
+
});
|
|
3131
|
+
child.on("close", (code) => {
|
|
3132
|
+
clearTimeout(timer);
|
|
3133
|
+
if (killed) {
|
|
3134
|
+
reject(new CloneError(`git clone timed out for ${opts.redacted}`));
|
|
3135
|
+
return;
|
|
3136
|
+
}
|
|
3137
|
+
resolve10({ code: code ?? 0, stderr: stderr.trim() });
|
|
3138
|
+
});
|
|
3139
|
+
});
|
|
3140
|
+
}
|
|
3141
|
+
function scrubCredentials(text) {
|
|
3142
|
+
return text.replace(/([a-z]+:\/\/)[^/\s@]+@/gi, "$1");
|
|
3143
|
+
}
|
|
3144
|
+
async function cloneRepo(url, opts) {
|
|
3145
|
+
const redacted = redactRemoteUrl(url);
|
|
3146
|
+
if (!looksLikeRepoUrl(url)) {
|
|
3147
|
+
throw new CloneError(
|
|
3148
|
+
`Not a supported repository URL: ${redacted}. Use an http(s), ssh, git://, or scp-like (git@host:owner/repo.git) URL.`
|
|
3149
|
+
);
|
|
3150
|
+
}
|
|
3151
|
+
if (/^file:\/\//i.test(url)) {
|
|
3152
|
+
throw new CloneError("file:// URLs are not allowed for remote clone.");
|
|
3153
|
+
}
|
|
3154
|
+
if (!isGitAvailable()) {
|
|
3155
|
+
throw new CloneError(`git is required to clone ${redacted} but was not found on PATH. ${gitInstallHint()}`);
|
|
3156
|
+
}
|
|
3157
|
+
const dir = await mkdtemp(path11.join(os3.tmpdir(), "vibgrate-hcs-clone-"));
|
|
3158
|
+
const cleanup = async () => {
|
|
3159
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {
|
|
3160
|
+
});
|
|
3161
|
+
};
|
|
3162
|
+
try {
|
|
3163
|
+
if (opts.verbose) process.stderr.write(`Cloning ${redacted} \u2026
|
|
3164
|
+
`);
|
|
3165
|
+
const clone = await runGit(
|
|
3166
|
+
["clone", "--depth", "1", "--quiet", "--", url, dir],
|
|
3167
|
+
{ timeoutMs: opts.timeoutMs, redacted }
|
|
3168
|
+
);
|
|
3169
|
+
if (clone.code !== 0) {
|
|
3170
|
+
throw new CloneError(`git clone failed for ${redacted}: ${clone.stderr || `exit ${clone.code}`}`);
|
|
3171
|
+
}
|
|
3172
|
+
if (opts.ref) {
|
|
3173
|
+
if (opts.verbose) process.stderr.write(`Checking out ${opts.ref} \u2026
|
|
3174
|
+
`);
|
|
3175
|
+
const fetch2 = await runGit(
|
|
3176
|
+
["-C", dir, "fetch", "--depth", "1", "--quiet", "origin", opts.ref],
|
|
3177
|
+
{ timeoutMs: opts.timeoutMs, redacted }
|
|
3178
|
+
);
|
|
3179
|
+
const checkoutTarget = fetch2.code === 0 ? "FETCH_HEAD" : opts.ref;
|
|
3180
|
+
const checkout = await runGit(
|
|
3181
|
+
["-C", dir, "checkout", "--quiet", "--detach", checkoutTarget],
|
|
3182
|
+
{ timeoutMs: opts.timeoutMs, redacted }
|
|
3183
|
+
);
|
|
3184
|
+
if (checkout.code !== 0) {
|
|
3185
|
+
throw new CloneError(
|
|
3186
|
+
`Could not check out ref "${opts.ref}" from ${redacted}: ${checkout.stderr || `exit ${checkout.code}`}`
|
|
3187
|
+
);
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
return { dir, cleanup };
|
|
3191
|
+
} catch (err) {
|
|
3192
|
+
await cleanup();
|
|
3193
|
+
throw err instanceof CloneError ? err : new CloneError(String(err));
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
|
|
3197
|
+
// src/utils/scan-manifest.ts
|
|
3198
|
+
import * as crypto3 from "crypto";
|
|
3199
|
+
function computeFactContentHash(factLines) {
|
|
3200
|
+
const hash = crypto3.createHash("sha256");
|
|
3201
|
+
for (const line of factLines) {
|
|
3202
|
+
hash.update(line);
|
|
3203
|
+
hash.update("\n");
|
|
3204
|
+
}
|
|
3205
|
+
return `sha256:${hash.digest("hex")}`;
|
|
3206
|
+
}
|
|
3207
|
+
function deriveRepoName(remoteUrl) {
|
|
3208
|
+
if (!remoteUrl) return void 0;
|
|
3209
|
+
const withoutScheme = remoteUrl.replace(/^[a-z]+:\/\//i, "").replace(/^[^/@]+@/, "");
|
|
3210
|
+
const pathPart = withoutScheme.replace(/^[^/:]+[/:]/, "");
|
|
3211
|
+
const segments = pathPart.split("/").filter(Boolean);
|
|
3212
|
+
if (segments.length === 0) return void 0;
|
|
3213
|
+
const repo = segments[segments.length - 1].replace(/\.git$/i, "");
|
|
3214
|
+
const owner = segments.length >= 2 ? segments[segments.length - 2] : void 0;
|
|
3215
|
+
return owner ? `${owner}/${repo}` : repo;
|
|
3216
|
+
}
|
|
3217
|
+
function buildScanManifest(input) {
|
|
3218
|
+
return {
|
|
3219
|
+
factType: "ScanManifest",
|
|
3220
|
+
manifestVersion: "1.0",
|
|
3221
|
+
emittedAt: input.emittedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
3222
|
+
repository: {
|
|
3223
|
+
remoteUrl: input.remoteUrl,
|
|
3224
|
+
name: input.repositoryName ?? deriveRepoName(input.remoteUrl)
|
|
3225
|
+
},
|
|
3226
|
+
vcs: {
|
|
3227
|
+
sha: input.sha,
|
|
3228
|
+
shortSha: input.shortSha,
|
|
3229
|
+
branch: input.branch,
|
|
3230
|
+
ref: input.ref
|
|
3231
|
+
},
|
|
3232
|
+
source: input.source,
|
|
3233
|
+
languages: [...input.languages].sort(),
|
|
3234
|
+
scanId: input.scanId,
|
|
3235
|
+
applicationId: input.applicationId,
|
|
3236
|
+
vibgrateVersion: input.vibgrateVersion,
|
|
3237
|
+
factCount: input.factLines.length,
|
|
3238
|
+
contentHash: computeFactContentHash(input.factLines)
|
|
3239
|
+
};
|
|
3240
|
+
}
|
|
3241
|
+
|
|
2759
3242
|
// src/commands/extract.ts
|
|
2760
3243
|
var EXIT_SUCCESS = 0;
|
|
2761
3244
|
var EXIT_SCHEMA_FAILURE = 1;
|
|
@@ -2840,14 +3323,14 @@ async function detectLanguages(rootDir, includeTests) {
|
|
|
2840
3323
|
async function walk(dir, relBase) {
|
|
2841
3324
|
let entries;
|
|
2842
3325
|
try {
|
|
2843
|
-
entries = await
|
|
3326
|
+
entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
2844
3327
|
} catch {
|
|
2845
3328
|
return;
|
|
2846
3329
|
}
|
|
2847
3330
|
for (const entry of entries) {
|
|
2848
3331
|
if (entry.name.startsWith(".")) continue;
|
|
2849
|
-
const absPath =
|
|
2850
|
-
const relPath =
|
|
3332
|
+
const absPath = path12.join(dir, entry.name);
|
|
3333
|
+
const relPath = path12.join(relBase, entry.name);
|
|
2851
3334
|
if (entry.isDirectory()) {
|
|
2852
3335
|
if (!SKIP_DIRS.has(entry.name)) {
|
|
2853
3336
|
await walk(absPath, relPath);
|
|
@@ -2856,7 +3339,7 @@ async function detectLanguages(rootDir, includeTests) {
|
|
|
2856
3339
|
}
|
|
2857
3340
|
if (!entry.isFile()) continue;
|
|
2858
3341
|
if (!includeTests && isTestPath(relPath)) continue;
|
|
2859
|
-
const ext =
|
|
3342
|
+
const ext = path12.extname(entry.name).toLowerCase();
|
|
2860
3343
|
for (const [lang, exts] of Object.entries(LANGUAGE_EXTENSIONS)) {
|
|
2861
3344
|
if (exts.has(ext)) {
|
|
2862
3345
|
counts.set(lang, (counts.get(lang) ?? 0) + 1);
|
|
@@ -2882,6 +3365,15 @@ function validateFactLine(line) {
|
|
|
2882
3365
|
}
|
|
2883
3366
|
return { valid: true };
|
|
2884
3367
|
}
|
|
3368
|
+
if (obj.factType === "ScanManifest") {
|
|
3369
|
+
if (typeof obj.manifestVersion !== "string") {
|
|
3370
|
+
return { valid: false, error: "ScanManifest missing manifestVersion" };
|
|
3371
|
+
}
|
|
3372
|
+
if (typeof obj.emittedAt !== "string") {
|
|
3373
|
+
return { valid: false, error: "ScanManifest missing emittedAt" };
|
|
3374
|
+
}
|
|
3375
|
+
return { valid: true };
|
|
3376
|
+
}
|
|
2885
3377
|
if (typeof obj.factId !== "string" || !obj.factId) {
|
|
2886
3378
|
return { valid: false, error: "Missing or invalid factId" };
|
|
2887
3379
|
}
|
|
@@ -2923,7 +3415,7 @@ function splitHcsOutput(lines) {
|
|
|
2923
3415
|
continue;
|
|
2924
3416
|
}
|
|
2925
3417
|
const obj = parsed;
|
|
2926
|
-
if (obj.factType === "Models" || obj.factType === "References") {
|
|
3418
|
+
if (obj.factType === "Models" || obj.factType === "References" || obj.factType === "ScanManifest") {
|
|
2927
3419
|
preamble.push(line);
|
|
2928
3420
|
} else {
|
|
2929
3421
|
facts.push(line);
|
|
@@ -2932,10 +3424,10 @@ function splitHcsOutput(lines) {
|
|
|
2932
3424
|
return { preamble, facts };
|
|
2933
3425
|
}
|
|
2934
3426
|
function resolveHcsWorkerBin() {
|
|
2935
|
-
const base = import.meta.dirname ??
|
|
2936
|
-
const bundledPath =
|
|
3427
|
+
const base = import.meta.dirname ?? path12.dirname(new URL(import.meta.url).pathname);
|
|
3428
|
+
const bundledPath = path12.resolve(base, "..", "dist", "hcs-worker.js");
|
|
2937
3429
|
if (existsSync(bundledPath)) return bundledPath;
|
|
2938
|
-
const siblingPath =
|
|
3430
|
+
const siblingPath = path12.resolve(base, "hcs-worker.js");
|
|
2939
3431
|
if (existsSync(siblingPath)) return siblingPath;
|
|
2940
3432
|
try {
|
|
2941
3433
|
const resolved = import.meta.resolve("@vibgrate/hcs-node-worker");
|
|
@@ -2943,9 +3435,9 @@ function resolveHcsWorkerBin() {
|
|
|
2943
3435
|
if (existsSync(resolvedPath)) return resolvedPath;
|
|
2944
3436
|
} catch {
|
|
2945
3437
|
}
|
|
2946
|
-
const monorepoPath =
|
|
3438
|
+
const monorepoPath = path12.resolve(base, "..", "..", "..", "vibgrate-hcs", "node", "dist", "main.js");
|
|
2947
3439
|
if (existsSync(monorepoPath)) return monorepoPath;
|
|
2948
|
-
const devPath =
|
|
3440
|
+
const devPath = path12.resolve(base, "..", "..", "..", "vibgrate-hcs", "node", "src", "main.ts");
|
|
2949
3441
|
if (existsSync(devPath)) return devPath;
|
|
2950
3442
|
throw new Error(
|
|
2951
3443
|
'Cannot locate HCS worker. Run "pnpm build" in the CLI package to bundle the worker into dist/.'
|
|
@@ -3024,7 +3516,7 @@ var NATIVE_RUNTIME_REQUIREMENTS = {
|
|
|
3024
3516
|
function commandExistsOnPath(command) {
|
|
3025
3517
|
const probeArgs = process.platform === "win32" ? ["/c", command, "--version"] : ["--version"];
|
|
3026
3518
|
const probeCmd = process.platform === "win32" ? "cmd" : command;
|
|
3027
|
-
const result =
|
|
3519
|
+
const result = spawnSync2(probeCmd, probeArgs, { stdio: "ignore" });
|
|
3028
3520
|
return !result.error && result.status === 0;
|
|
3029
3521
|
}
|
|
3030
3522
|
function buildNativeInstallHint(language) {
|
|
@@ -3048,12 +3540,12 @@ async function runNodeWorker(rootDir, language, opts) {
|
|
|
3048
3540
|
args.push("--cobol-copybook-paths", opts.copybookPaths);
|
|
3049
3541
|
}
|
|
3050
3542
|
}
|
|
3051
|
-
return new Promise((
|
|
3543
|
+
return new Promise((resolve10) => {
|
|
3052
3544
|
const facts = [];
|
|
3053
3545
|
const errors = [];
|
|
3054
3546
|
let stdoutBuf = "";
|
|
3055
3547
|
let killed = false;
|
|
3056
|
-
const child =
|
|
3548
|
+
const child = spawn3("node", ["--enable-source-maps", workerBin, ...args], {
|
|
3057
3549
|
cwd: rootDir,
|
|
3058
3550
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3059
3551
|
env: { ...process.env, NODE_OPTIONS: "--max-old-space-size=4096" }
|
|
@@ -3085,7 +3577,7 @@ async function runNodeWorker(rootDir, language, opts) {
|
|
|
3085
3577
|
continue;
|
|
3086
3578
|
}
|
|
3087
3579
|
if (opts.verbose) {
|
|
3088
|
-
process.stderr.write(
|
|
3580
|
+
process.stderr.write(chalk11.dim(`[${language}] ${trimmed}
|
|
3089
3581
|
`));
|
|
3090
3582
|
}
|
|
3091
3583
|
if (trimmed.startsWith("[error]")) {
|
|
@@ -3099,15 +3591,15 @@ async function runNodeWorker(rootDir, language, opts) {
|
|
|
3099
3591
|
facts.push(stdoutBuf.trim());
|
|
3100
3592
|
}
|
|
3101
3593
|
if (killed) {
|
|
3102
|
-
|
|
3594
|
+
resolve10({ language, facts, errors: ["Worker killed: timeout exceeded"], exitCode: EXIT_TIMEOUT });
|
|
3103
3595
|
} else {
|
|
3104
|
-
|
|
3596
|
+
resolve10({ language, facts, errors, exitCode: code ?? 0 });
|
|
3105
3597
|
}
|
|
3106
3598
|
});
|
|
3107
3599
|
child.on("error", (err) => {
|
|
3108
3600
|
clearTimeout(timer);
|
|
3109
3601
|
errors.push(`Failed to spawn worker: ${err.message}`);
|
|
3110
|
-
|
|
3602
|
+
resolve10({ language, facts, errors, exitCode: EXIT_PARSE_FAILURE });
|
|
3111
3603
|
});
|
|
3112
3604
|
});
|
|
3113
3605
|
}
|
|
@@ -3127,7 +3619,7 @@ function resolveDotnetPublishedWorker(workersDir) {
|
|
|
3127
3619
|
};
|
|
3128
3620
|
const candidates = candidatesByPlatform[process.platform] ?? [];
|
|
3129
3621
|
for (const filename of candidates) {
|
|
3130
|
-
const fullPath =
|
|
3622
|
+
const fullPath = path12.join(workersDir, filename);
|
|
3131
3623
|
if (existsSync(fullPath)) {
|
|
3132
3624
|
return fullPath;
|
|
3133
3625
|
}
|
|
@@ -3135,30 +3627,30 @@ function resolveDotnetPublishedWorker(workersDir) {
|
|
|
3135
3627
|
return null;
|
|
3136
3628
|
}
|
|
3137
3629
|
function resolveNativeWorker(language, projectDir) {
|
|
3138
|
-
const base = import.meta.dirname ??
|
|
3139
|
-
const hcsFromBundle =
|
|
3140
|
-
const hcsFromSrc =
|
|
3630
|
+
const base = import.meta.dirname ?? path12.dirname(new URL(import.meta.url).pathname);
|
|
3631
|
+
const hcsFromBundle = path12.resolve(base, "..", "..", "vibgrate-hcs");
|
|
3632
|
+
const hcsFromSrc = path12.resolve(base, "..", "..", "..", "vibgrate-hcs");
|
|
3141
3633
|
const hcsRoot = existsSync(hcsFromBundle) ? hcsFromBundle : hcsFromSrc;
|
|
3142
|
-
const workersDir =
|
|
3634
|
+
const workersDir = path12.resolve(base, "workers");
|
|
3143
3635
|
switch (language) {
|
|
3144
3636
|
case "go": {
|
|
3145
|
-
const bin =
|
|
3637
|
+
const bin = path12.join(workersDir, process.platform === "win32" ? "vibgrate-hcs-go.exe" : "vibgrate-hcs-go");
|
|
3146
3638
|
if (existsSync(bin)) {
|
|
3147
3639
|
return { cmd: bin, args: ["--project", projectDir, "--output", "ndjson"] };
|
|
3148
3640
|
}
|
|
3149
|
-
const src =
|
|
3150
|
-
if (existsSync(
|
|
3641
|
+
const src = path12.join(hcsRoot, "go");
|
|
3642
|
+
if (existsSync(path12.join(src, "main.go"))) {
|
|
3151
3643
|
return { cmd: "go", args: ["run", ".", "--project", projectDir, "--output", "ndjson"], cwd: src };
|
|
3152
3644
|
}
|
|
3153
3645
|
return null;
|
|
3154
3646
|
}
|
|
3155
3647
|
case "python": {
|
|
3156
|
-
const bin =
|
|
3648
|
+
const bin = path12.join(workersDir, "vibgrate-hcs-python");
|
|
3157
3649
|
if (existsSync(bin)) {
|
|
3158
3650
|
return { cmd: bin, args: ["--project", projectDir, "--output", "ndjson"] };
|
|
3159
3651
|
}
|
|
3160
|
-
const src =
|
|
3161
|
-
if (existsSync(
|
|
3652
|
+
const src = path12.join(hcsRoot, "python");
|
|
3653
|
+
if (existsSync(path12.join(src, "pyproject.toml"))) {
|
|
3162
3654
|
return {
|
|
3163
3655
|
cmd: "python3",
|
|
3164
3656
|
args: ["-m", "vibgrate_hcs_python.main", "--project", projectDir, "--output", "ndjson"],
|
|
@@ -3168,13 +3660,13 @@ function resolveNativeWorker(language, projectDir) {
|
|
|
3168
3660
|
return null;
|
|
3169
3661
|
}
|
|
3170
3662
|
case "java": {
|
|
3171
|
-
const jar =
|
|
3663
|
+
const jar = path12.join(workersDir, "vibgrate-hcs-jvm.jar");
|
|
3172
3664
|
if (existsSync(jar)) {
|
|
3173
3665
|
return { cmd: "java", args: ["-jar", jar, "--project", projectDir, "--output", "ndjson"] };
|
|
3174
3666
|
}
|
|
3175
|
-
const src =
|
|
3176
|
-
if (existsSync(
|
|
3177
|
-
const gradlew =
|
|
3667
|
+
const src = path12.join(hcsRoot, "jvm");
|
|
3668
|
+
if (existsSync(path12.join(src, "build.gradle.kts"))) {
|
|
3669
|
+
const gradlew = path12.join(src, process.platform === "win32" ? "gradlew.bat" : "gradlew");
|
|
3178
3670
|
const launcher = existsSync(gradlew) ? gradlew : "gradle";
|
|
3179
3671
|
return {
|
|
3180
3672
|
cmd: launcher,
|
|
@@ -3190,11 +3682,11 @@ function resolveNativeWorker(language, projectDir) {
|
|
|
3190
3682
|
if (publishedWorker) {
|
|
3191
3683
|
return { cmd: publishedWorker, args: ["--project", projectDir, "--output", "ndjson"] };
|
|
3192
3684
|
}
|
|
3193
|
-
const dll =
|
|
3685
|
+
const dll = path12.join(workersDir, "VibgrateHcsWorker.dll");
|
|
3194
3686
|
if (existsSync(dll)) {
|
|
3195
3687
|
return { cmd: "dotnet", args: [dll, "--project", projectDir, "--output", "ndjson"] };
|
|
3196
3688
|
}
|
|
3197
|
-
const csproj =
|
|
3689
|
+
const csproj = path12.join(hcsRoot, "dotnet", "src", "VibgrateHcsWorker", "VibgrateHcsWorker.csproj");
|
|
3198
3690
|
if (existsSync(csproj)) {
|
|
3199
3691
|
return {
|
|
3200
3692
|
cmd: "dotnet",
|
|
@@ -3220,19 +3712,19 @@ async function runNativeWorker(rootDir, language, opts) {
|
|
|
3220
3712
|
exitCode: EXIT_PARSE_FAILURE
|
|
3221
3713
|
};
|
|
3222
3714
|
}
|
|
3223
|
-
return new Promise((
|
|
3715
|
+
return new Promise((resolve10) => {
|
|
3224
3716
|
const facts = [];
|
|
3225
3717
|
const errors = [];
|
|
3226
3718
|
let stdoutBuf = "";
|
|
3227
3719
|
let killed = false;
|
|
3228
3720
|
if (opts.verbose) {
|
|
3229
|
-
process.stderr.write(
|
|
3721
|
+
process.stderr.write(chalk11.dim(`[${language}] Spawning: ${spec.cmd} ${spec.args.join(" ")}
|
|
3230
3722
|
`));
|
|
3231
3723
|
}
|
|
3232
3724
|
const runtimeReq = NATIVE_RUNTIME_REQUIREMENTS[language];
|
|
3233
3725
|
const usesPathCommand = runtimeReq && spec.cmd === runtimeReq.command;
|
|
3234
3726
|
if (usesPathCommand && !commandExistsOnPath(spec.cmd)) {
|
|
3235
|
-
|
|
3727
|
+
resolve10({
|
|
3236
3728
|
language,
|
|
3237
3729
|
facts: [],
|
|
3238
3730
|
errors: [
|
|
@@ -3244,7 +3736,7 @@ async function runNativeWorker(rootDir, language, opts) {
|
|
|
3244
3736
|
});
|
|
3245
3737
|
return;
|
|
3246
3738
|
}
|
|
3247
|
-
const child =
|
|
3739
|
+
const child = spawn3(spec.cmd, spec.args, {
|
|
3248
3740
|
cwd: spec.cwd ?? rootDir,
|
|
3249
3741
|
stdio: ["ignore", "pipe", "pipe"]
|
|
3250
3742
|
});
|
|
@@ -3267,7 +3759,7 @@ async function runNativeWorker(rootDir, language, opts) {
|
|
|
3267
3759
|
const trimmed = line.trim();
|
|
3268
3760
|
if (!trimmed) continue;
|
|
3269
3761
|
if (opts.verbose) {
|
|
3270
|
-
process.stderr.write(
|
|
3762
|
+
process.stderr.write(chalk11.dim(`[${language}] ${trimmed}
|
|
3271
3763
|
`));
|
|
3272
3764
|
}
|
|
3273
3765
|
if (trimmed.startsWith("[error]")) {
|
|
@@ -3279,9 +3771,9 @@ async function runNativeWorker(rootDir, language, opts) {
|
|
|
3279
3771
|
clearTimeout(timer);
|
|
3280
3772
|
if (stdoutBuf.trim()) facts.push(stdoutBuf.trim());
|
|
3281
3773
|
if (killed) {
|
|
3282
|
-
|
|
3774
|
+
resolve10({ language, facts, errors: ["Worker killed: timeout exceeded"], exitCode: EXIT_TIMEOUT });
|
|
3283
3775
|
} else {
|
|
3284
|
-
|
|
3776
|
+
resolve10({ language, facts, errors, exitCode: code ?? 0 });
|
|
3285
3777
|
}
|
|
3286
3778
|
});
|
|
3287
3779
|
child.on("error", (err) => {
|
|
@@ -3290,21 +3782,21 @@ async function runNativeWorker(rootDir, language, opts) {
|
|
|
3290
3782
|
errors.push(
|
|
3291
3783
|
isNotFound ? `[error] '${spec.cmd}' not found. ${buildNativeInstallHint(language)}` : `[error] Failed to spawn native worker: ${err.message}`
|
|
3292
3784
|
);
|
|
3293
|
-
|
|
3785
|
+
resolve10({ language, facts, errors, exitCode: EXIT_PARSE_FAILURE });
|
|
3294
3786
|
});
|
|
3295
3787
|
});
|
|
3296
3788
|
}
|
|
3297
3789
|
async function pushFacts(facts, dsn, verbose) {
|
|
3298
3790
|
const parsed = parseDsn2(dsn);
|
|
3299
3791
|
if (!parsed) {
|
|
3300
|
-
process.stderr.write(
|
|
3301
|
-
process.stderr.write(
|
|
3792
|
+
process.stderr.write(chalk11.red("Invalid DSN format.\n"));
|
|
3793
|
+
process.stderr.write(chalk11.dim("Expected: vibgrate+https://<key_id>:<secret>@<host>/<workspace_id>\n"));
|
|
3302
3794
|
return false;
|
|
3303
3795
|
}
|
|
3304
3796
|
const body = facts.join("\n") + "\n";
|
|
3305
3797
|
const url = `${parsed.scheme}://${parsed.host}/v1/ingest/hcs`;
|
|
3306
3798
|
if (verbose) {
|
|
3307
|
-
process.stderr.write(
|
|
3799
|
+
process.stderr.write(chalk11.dim(`Pushing ${facts.length} facts to ${parsed.host}...
|
|
3308
3800
|
`));
|
|
3309
3801
|
}
|
|
3310
3802
|
try {
|
|
@@ -3322,23 +3814,23 @@ async function pushFacts(facts, dsn, verbose) {
|
|
|
3322
3814
|
});
|
|
3323
3815
|
if (!response.ok) {
|
|
3324
3816
|
const text = await response.text().catch(() => "");
|
|
3325
|
-
process.stderr.write(
|
|
3817
|
+
process.stderr.write(chalk11.red(`Push failed: HTTP ${response.status} ${text}
|
|
3326
3818
|
`));
|
|
3327
3819
|
return false;
|
|
3328
3820
|
}
|
|
3329
3821
|
if (verbose) {
|
|
3330
|
-
process.stderr.write(
|
|
3822
|
+
process.stderr.write(chalk11.green(`\u2714 Pushed ${facts.length} facts successfully
|
|
3331
3823
|
`));
|
|
3332
3824
|
}
|
|
3333
3825
|
return true;
|
|
3334
3826
|
} catch (err) {
|
|
3335
|
-
process.stderr.write(
|
|
3827
|
+
process.stderr.write(chalk11.red(`Push failed: ${err instanceof Error ? err.message : String(err)}
|
|
3336
3828
|
`));
|
|
3337
3829
|
return false;
|
|
3338
3830
|
}
|
|
3339
3831
|
}
|
|
3340
3832
|
async function loadFeedback(filePath) {
|
|
3341
|
-
const content = await
|
|
3833
|
+
const content = await fs4.readFile(filePath, "utf-8");
|
|
3342
3834
|
const lines = content.split("\n").filter((l) => l.trim());
|
|
3343
3835
|
for (const line of lines) {
|
|
3344
3836
|
try {
|
|
@@ -3399,21 +3891,21 @@ var ProgressTracker = class {
|
|
|
3399
3891
|
const parts = [];
|
|
3400
3892
|
for (const [lang, st] of this.langStatus) {
|
|
3401
3893
|
if (st.done) {
|
|
3402
|
-
parts.push(
|
|
3894
|
+
parts.push(chalk11.green(`${lang} \u2713`));
|
|
3403
3895
|
} else if (st.phase === "waiting") {
|
|
3404
|
-
parts.push(
|
|
3896
|
+
parts.push(chalk11.dim(`${lang} \xB7`));
|
|
3405
3897
|
} else if (st.phase === "discovering") {
|
|
3406
|
-
parts.push(
|
|
3898
|
+
parts.push(chalk11.yellow(`${lang} \u2026`));
|
|
3407
3899
|
} else if (st.phase === "scanning" && st.fileCount > 0) {
|
|
3408
3900
|
const pct = Math.round(st.fileIndex / st.fileCount * 100);
|
|
3409
|
-
parts.push(
|
|
3901
|
+
parts.push(chalk11.cyan(`${lang} ${st.fileIndex}/${st.fileCount} ${pct}%`));
|
|
3410
3902
|
} else if (st.phase === "extracting") {
|
|
3411
|
-
parts.push(
|
|
3903
|
+
parts.push(chalk11.cyan(`${lang} extracting`));
|
|
3412
3904
|
} else {
|
|
3413
|
-
parts.push(
|
|
3905
|
+
parts.push(chalk11.dim(`${lang} ${st.phase}`));
|
|
3414
3906
|
}
|
|
3415
3907
|
}
|
|
3416
|
-
const line = ` ${parts.join(" ")} ${
|
|
3908
|
+
const line = ` ${parts.join(" ")} ${chalk11.dim(`${this.totalFacts} facts`)}`;
|
|
3417
3909
|
const padding = Math.max(0, this.lastLineLen - line.length);
|
|
3418
3910
|
process.stderr.write(`\r${line}${" ".repeat(padding)}`);
|
|
3419
3911
|
this.lastLineLen = line.length;
|
|
@@ -3425,58 +3917,95 @@ var ProgressTracker = class {
|
|
|
3425
3917
|
}
|
|
3426
3918
|
}
|
|
3427
3919
|
};
|
|
3428
|
-
var extractCommand = new
|
|
3429
|
-
const
|
|
3430
|
-
|
|
3431
|
-
|
|
3920
|
+
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) => {
|
|
3921
|
+
const urlArg = opts.repoUrl ?? (looksLikeRepoUrl(targetPath) ? targetPath : void 0);
|
|
3922
|
+
const source = urlArg ? "url" : "local";
|
|
3923
|
+
let rootDir;
|
|
3924
|
+
let manifestRemoteUrl;
|
|
3925
|
+
if (urlArg) {
|
|
3926
|
+
const cloneTimeoutMins = parseInt(opts.cloneTimeoutMins, 10);
|
|
3927
|
+
if (isNaN(cloneTimeoutMins) || cloneTimeoutMins < 1) {
|
|
3928
|
+
process.stderr.write(chalk11.red("--clone-timeout-mins must be >= 1\n"));
|
|
3929
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
3930
|
+
}
|
|
3931
|
+
manifestRemoteUrl = redactRemoteUrl(urlArg);
|
|
3932
|
+
try {
|
|
3933
|
+
const cloned = await cloneRepo(urlArg, {
|
|
3934
|
+
ref: opts.ref,
|
|
3935
|
+
timeoutMs: cloneTimeoutMins * 60 * 1e3,
|
|
3936
|
+
verbose: opts.verbose ?? false
|
|
3937
|
+
});
|
|
3938
|
+
rootDir = cloned.dir;
|
|
3939
|
+
if (opts.keepClone) {
|
|
3940
|
+
process.stderr.write(chalk11.dim(`Clone retained at: ${cloned.dir}
|
|
3432
3941
|
`));
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3942
|
+
} else {
|
|
3943
|
+
process.once("exit", () => {
|
|
3944
|
+
try {
|
|
3945
|
+
rmSync2(cloned.dir, { recursive: true, force: true });
|
|
3946
|
+
} catch {
|
|
3947
|
+
}
|
|
3948
|
+
});
|
|
3949
|
+
}
|
|
3950
|
+
} catch (err) {
|
|
3951
|
+
const msg = err instanceof CloneError ? err.message : err instanceof Error ? err.message : String(err);
|
|
3952
|
+
process.stderr.write(chalk11.red(`Clone failed: ${msg}
|
|
3440
3953
|
`));
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3954
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
3955
|
+
}
|
|
3956
|
+
} else {
|
|
3957
|
+
rootDir = path12.resolve(targetPath);
|
|
3958
|
+
if (!await pathExists(rootDir)) {
|
|
3959
|
+
process.stderr.write(chalk11.red(`Path does not exist: ${rootDir}
|
|
3445
3960
|
`));
|
|
3446
|
-
|
|
3961
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
3962
|
+
}
|
|
3963
|
+
let stat4;
|
|
3964
|
+
try {
|
|
3965
|
+
stat4 = await fs4.stat(rootDir);
|
|
3966
|
+
} catch {
|
|
3967
|
+
process.stderr.write(chalk11.red(`Cannot read path: ${rootDir}
|
|
3968
|
+
`));
|
|
3969
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
3970
|
+
}
|
|
3971
|
+
if (!stat4.isDirectory()) {
|
|
3972
|
+
process.stderr.write(chalk11.red(`Path must be a directory: ${rootDir}
|
|
3973
|
+
`));
|
|
3974
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
3975
|
+
}
|
|
3447
3976
|
}
|
|
3448
3977
|
const dsn = opts.dsn || process.env.VIBGRATE_DSN;
|
|
3449
3978
|
if (opts.push && !dsn) {
|
|
3450
|
-
process.stderr.write(
|
|
3979
|
+
process.stderr.write(chalk11.red("--push requires --dsn or VIBGRATE_DSN environment variable\n"));
|
|
3451
3980
|
process.exit(EXIT_USAGE_ERROR);
|
|
3452
3981
|
}
|
|
3453
3982
|
if (dsn && !parseDsn2(dsn)) {
|
|
3454
|
-
process.stderr.write(
|
|
3455
|
-
process.stderr.write(
|
|
3983
|
+
process.stderr.write(chalk11.red("Invalid DSN format.\n"));
|
|
3984
|
+
process.stderr.write(chalk11.dim("Expected: vibgrate+https://<key_id>:<secret>@<host>/<workspace_id>\n"));
|
|
3456
3985
|
process.exit(EXIT_USAGE_ERROR);
|
|
3457
3986
|
}
|
|
3458
|
-
const concurrency = opts.concurrency ? parseInt(opts.concurrency, 10) :
|
|
3987
|
+
const concurrency = opts.concurrency ? parseInt(opts.concurrency, 10) : os4.cpus().length;
|
|
3459
3988
|
if (isNaN(concurrency) || concurrency < 1) {
|
|
3460
|
-
process.stderr.write(
|
|
3989
|
+
process.stderr.write(chalk11.red("--concurrency must be >= 1\n"));
|
|
3461
3990
|
process.exit(EXIT_USAGE_ERROR);
|
|
3462
3991
|
}
|
|
3463
3992
|
const timeoutMins = parseInt(opts.timeoutMins, 10);
|
|
3464
3993
|
if (isNaN(timeoutMins) || timeoutMins < 1) {
|
|
3465
|
-
process.stderr.write(
|
|
3994
|
+
process.stderr.write(chalk11.red("--timeout-mins must be >= 1\n"));
|
|
3466
3995
|
process.exit(EXIT_USAGE_ERROR);
|
|
3467
3996
|
}
|
|
3468
3997
|
const timeoutMs = timeoutMins * 60 * 1e3;
|
|
3469
3998
|
if (opts.out) {
|
|
3470
|
-
const outDir =
|
|
3999
|
+
const outDir = path12.dirname(path12.resolve(opts.out));
|
|
3471
4000
|
if (!await pathExists(outDir)) {
|
|
3472
|
-
process.stderr.write(
|
|
4001
|
+
process.stderr.write(chalk11.red(`Output directory does not exist: ${outDir}
|
|
3473
4002
|
`));
|
|
3474
4003
|
process.exit(EXIT_USAGE_ERROR);
|
|
3475
4004
|
}
|
|
3476
4005
|
try {
|
|
3477
|
-
const outStat = await
|
|
4006
|
+
const outStat = await fs4.stat(path12.resolve(opts.out));
|
|
3478
4007
|
if (outStat.isDirectory()) {
|
|
3479
|
-
process.stderr.write(
|
|
4008
|
+
process.stderr.write(chalk11.red(`--out cannot be a directory: ${opts.out}
|
|
3480
4009
|
`));
|
|
3481
4010
|
process.exit(EXIT_USAGE_ERROR);
|
|
3482
4011
|
}
|
|
@@ -3485,16 +4014,16 @@ var extractCommand = new Command8("extract").description("Analyze source code an
|
|
|
3485
4014
|
}
|
|
3486
4015
|
let feedbackLines;
|
|
3487
4016
|
if (opts.feedback) {
|
|
3488
|
-
const feedbackPath =
|
|
4017
|
+
const feedbackPath = path12.resolve(opts.feedback);
|
|
3489
4018
|
if (!await pathExists(feedbackPath)) {
|
|
3490
|
-
process.stderr.write(
|
|
4019
|
+
process.stderr.write(chalk11.red(`Feedback file not found: ${feedbackPath}
|
|
3491
4020
|
`));
|
|
3492
4021
|
process.exit(EXIT_USAGE_ERROR);
|
|
3493
4022
|
}
|
|
3494
4023
|
try {
|
|
3495
4024
|
feedbackLines = await loadFeedback(feedbackPath);
|
|
3496
4025
|
} catch (err) {
|
|
3497
|
-
process.stderr.write(
|
|
4026
|
+
process.stderr.write(chalk11.red(`Invalid feedback file: ${err instanceof Error ? err.message : String(err)}
|
|
3498
4027
|
`));
|
|
3499
4028
|
process.exit(EXIT_USAGE_ERROR);
|
|
3500
4029
|
}
|
|
@@ -3504,9 +4033,9 @@ var extractCommand = new Command8("extract").description("Analyze source code an
|
|
|
3504
4033
|
targetLanguages = opts.language.split(",").map((l) => normalizeLanguageToken(l)).filter(Boolean);
|
|
3505
4034
|
for (const lang of targetLanguages) {
|
|
3506
4035
|
if (!SUPPORTED_LANGUAGES.has(lang)) {
|
|
3507
|
-
process.stderr.write(
|
|
4036
|
+
process.stderr.write(chalk11.red(`Unknown language: "${lang}"
|
|
3508
4037
|
`));
|
|
3509
|
-
process.stderr.write(
|
|
4038
|
+
process.stderr.write(chalk11.dim(`Supported: ${[...SUPPORTED_LANGUAGES].sort().join(", ")}
|
|
3510
4039
|
`));
|
|
3511
4040
|
process.exit(EXIT_USAGE_ERROR);
|
|
3512
4041
|
}
|
|
@@ -3514,14 +4043,14 @@ var extractCommand = new Command8("extract").description("Analyze source code an
|
|
|
3514
4043
|
} else {
|
|
3515
4044
|
const detected = await detectLanguages(rootDir, opts.includeTests ?? false);
|
|
3516
4045
|
if (detected.length === 0) {
|
|
3517
|
-
process.stderr.write(
|
|
4046
|
+
process.stderr.write(chalk11.yellow("No supported source files detected.\n"));
|
|
3518
4047
|
process.exit(EXIT_SUCCESS);
|
|
3519
4048
|
}
|
|
3520
4049
|
targetLanguages = detected.map((d) => d.language);
|
|
3521
4050
|
if (opts.verbose) {
|
|
3522
|
-
process.stderr.write(
|
|
4051
|
+
process.stderr.write(chalk11.dim("Detected languages:\n"));
|
|
3523
4052
|
for (const d of detected) {
|
|
3524
|
-
process.stderr.write(
|
|
4053
|
+
process.stderr.write(chalk11.dim(` ${d.language}: ${d.fileCount} files
|
|
3525
4054
|
`));
|
|
3526
4055
|
}
|
|
3527
4056
|
}
|
|
@@ -3529,20 +4058,20 @@ var extractCommand = new Command8("extract").description("Analyze source code an
|
|
|
3529
4058
|
const runnableLanguages = targetLanguages.filter((l) => ALL_WORKER_LANGS.has(l));
|
|
3530
4059
|
const unknownWorkerLangs = targetLanguages.filter((l) => !ALL_WORKER_LANGS.has(l));
|
|
3531
4060
|
if (unknownWorkerLangs.length > 0 && opts.verbose) {
|
|
3532
|
-
process.stderr.write(
|
|
4061
|
+
process.stderr.write(chalk11.dim(
|
|
3533
4062
|
`No worker registered for: ${unknownWorkerLangs.join(", ")} \u2014 skipping.
|
|
3534
4063
|
`
|
|
3535
4064
|
));
|
|
3536
4065
|
}
|
|
3537
4066
|
if (runnableLanguages.length === 0) {
|
|
3538
|
-
process.stderr.write(
|
|
4067
|
+
process.stderr.write(chalk11.yellow("No languages with available HCS workers found.\n"));
|
|
3539
4068
|
process.exit(EXIT_SUCCESS);
|
|
3540
4069
|
}
|
|
3541
4070
|
const startTime = Date.now();
|
|
3542
4071
|
const globalDeadline = startTime + timeoutMs;
|
|
3543
4072
|
process.stderr.write(
|
|
3544
|
-
|
|
3545
|
-
`) +
|
|
4073
|
+
chalk11.bold(`Extracting HCS facts from ${rootDir}
|
|
4074
|
+
`) + chalk11.dim(`Languages: ${runnableLanguages.join(", ")} Concurrency: ${concurrency} Timeout: ${timeoutMins}m
|
|
3546
4075
|
`)
|
|
3547
4076
|
);
|
|
3548
4077
|
const progress = new ProgressTracker(runnableLanguages);
|
|
@@ -3589,7 +4118,7 @@ var extractCommand = new Command8("extract").description("Analyze source code an
|
|
|
3589
4118
|
hasSchemaFailure = true;
|
|
3590
4119
|
allErrors.push(`[${language}] Schema validation: ${validation.error}`);
|
|
3591
4120
|
if (opts.verbose) {
|
|
3592
|
-
process.stderr.write(
|
|
4121
|
+
process.stderr.write(chalk11.red(`[${language}] Invalid fact: ${validation.error}
|
|
3593
4122
|
`));
|
|
3594
4123
|
}
|
|
3595
4124
|
}
|
|
@@ -3615,7 +4144,7 @@ var extractCommand = new Command8("extract").description("Analyze source code an
|
|
|
3615
4144
|
progress.finish(elapsed);
|
|
3616
4145
|
if (opts.derive) {
|
|
3617
4146
|
const scanId = opts.scanId || "local";
|
|
3618
|
-
const applicationId = opts.applicationId ||
|
|
4147
|
+
const applicationId = opts.applicationId || path12.basename(rootDir);
|
|
3619
4148
|
const { envelopes, summary } = deriveBehaviouralFacts(derivedInputs, { scanId, applicationId });
|
|
3620
4149
|
for (const env of envelopes) {
|
|
3621
4150
|
const line = JSON.stringify(env);
|
|
@@ -3626,18 +4155,42 @@ var extractCommand = new Command8("extract").description("Analyze source code an
|
|
|
3626
4155
|
allErrors.push("[behavioural] derived fact failed schema validation");
|
|
3627
4156
|
}
|
|
3628
4157
|
}
|
|
3629
|
-
process.stderr.write(
|
|
4158
|
+
process.stderr.write(chalk11.dim(
|
|
3630
4159
|
`Derived ${summary.assertions} behavioural assertion(s) \xB7 confidence ${summary.behaviouralConfidence} (${summary.assertedFacts}/${summary.surfaceFacts} surface facts)
|
|
3631
4160
|
`
|
|
3632
4161
|
));
|
|
3633
4162
|
}
|
|
3634
4163
|
allFacts.sort();
|
|
3635
|
-
const
|
|
4164
|
+
const manifestScanId = opts.scanId || "local";
|
|
4165
|
+
const manifestApplicationId = opts.applicationId || (source === "url" ? deriveRepoName(manifestRemoteUrl) : void 0) || path12.basename(rootDir);
|
|
4166
|
+
let manifestVcs;
|
|
4167
|
+
try {
|
|
4168
|
+
manifestVcs = await detectVcs2(rootDir);
|
|
4169
|
+
} catch {
|
|
4170
|
+
manifestVcs = void 0;
|
|
4171
|
+
}
|
|
4172
|
+
const manifest = buildScanManifest({
|
|
4173
|
+
// Prefer the redacted URL we cloned from; fall back to the local repo's
|
|
4174
|
+
// configured remote (also credential-redacted) for non-URL scans.
|
|
4175
|
+
remoteUrl: manifestRemoteUrl ?? (manifestVcs?.remoteUrl ? redactRemoteUrl(manifestVcs.remoteUrl) : void 0),
|
|
4176
|
+
sha: manifestVcs?.sha,
|
|
4177
|
+
shortSha: manifestVcs?.shortSha,
|
|
4178
|
+
branch: manifestVcs?.branch,
|
|
4179
|
+
ref: opts.ref,
|
|
4180
|
+
source,
|
|
4181
|
+
languages: runnableLanguages,
|
|
4182
|
+
scanId: manifestScanId,
|
|
4183
|
+
applicationId: manifestApplicationId,
|
|
4184
|
+
vibgrateVersion: VERSION,
|
|
4185
|
+
factLines: allFacts
|
|
4186
|
+
});
|
|
4187
|
+
const manifestLine = JSON.stringify(manifest);
|
|
4188
|
+
const allLines = [manifestLine, ...allPreamble, ...allFacts];
|
|
3636
4189
|
const ndjsonOutput = allLines.join("\n") + (allLines.length > 0 ? "\n" : "");
|
|
3637
4190
|
if (opts.out) {
|
|
3638
|
-
const outPath =
|
|
3639
|
-
await
|
|
3640
|
-
process.stderr.write(
|
|
4191
|
+
const outPath = path12.resolve(opts.out);
|
|
4192
|
+
await fs4.writeFile(outPath, ndjsonOutput, "utf-8");
|
|
4193
|
+
process.stderr.write(chalk11.green(`\u2714 Wrote ${allFacts.length} facts to ${outPath}
|
|
3641
4194
|
`));
|
|
3642
4195
|
} else {
|
|
3643
4196
|
process.stdout.write(ndjsonOutput);
|
|
@@ -3649,48 +4202,48 @@ var extractCommand = new Command8("extract").description("Analyze source code an
|
|
|
3649
4202
|
}
|
|
3650
4203
|
}
|
|
3651
4204
|
if (opts.verbose) {
|
|
3652
|
-
process.stderr.write(
|
|
3653
|
-
process.stderr.write(
|
|
4205
|
+
process.stderr.write(chalk11.dim("\n\u2500\u2500 Summary \u2500\u2500\n"));
|
|
4206
|
+
process.stderr.write(chalk11.dim(` Facts emitted : ${allFacts.length}
|
|
3654
4207
|
`));
|
|
3655
|
-
process.stderr.write(
|
|
4208
|
+
process.stderr.write(chalk11.dim(` Languages : ${runnableLanguages.join(", ")}
|
|
3656
4209
|
`));
|
|
3657
|
-
process.stderr.write(
|
|
4210
|
+
process.stderr.write(chalk11.dim(` Elapsed : ${elapsed}s
|
|
3658
4211
|
`));
|
|
3659
4212
|
if (allErrors.length > 0) {
|
|
3660
|
-
process.stderr.write(
|
|
4213
|
+
process.stderr.write(chalk11.dim(` Errors : ${allErrors.length}
|
|
3661
4214
|
`));
|
|
3662
4215
|
for (const err of allErrors.slice(0, 10)) {
|
|
3663
|
-
process.stderr.write(
|
|
4216
|
+
process.stderr.write(chalk11.dim(` ${err}
|
|
3664
4217
|
`));
|
|
3665
4218
|
}
|
|
3666
4219
|
if (allErrors.length > 10) {
|
|
3667
|
-
process.stderr.write(
|
|
4220
|
+
process.stderr.write(chalk11.dim(` ... and ${allErrors.length - 10} more
|
|
3668
4221
|
`));
|
|
3669
4222
|
}
|
|
3670
4223
|
}
|
|
3671
|
-
process.stderr.write(
|
|
4224
|
+
process.stderr.write(chalk11.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n"));
|
|
3672
4225
|
}
|
|
3673
4226
|
if (hasTimeout) {
|
|
3674
|
-
process.stderr.write(
|
|
4227
|
+
process.stderr.write(chalk11.red(`Timeout exceeded (${timeoutMins} minutes)
|
|
3675
4228
|
`));
|
|
3676
4229
|
process.exit(EXIT_TIMEOUT);
|
|
3677
4230
|
}
|
|
3678
4231
|
if (hasSchemaFailure) {
|
|
3679
|
-
process.stderr.write(
|
|
4232
|
+
process.stderr.write(chalk11.red("Fact schema validation failures detected\n"));
|
|
3680
4233
|
process.exit(EXIT_SCHEMA_FAILURE);
|
|
3681
4234
|
}
|
|
3682
4235
|
if (hasParseFailure) {
|
|
3683
|
-
process.stderr.write(
|
|
4236
|
+
process.stderr.write(chalk11.red("Parsing failures detected\n"));
|
|
3684
4237
|
process.exit(EXIT_PARSE_FAILURE);
|
|
3685
4238
|
}
|
|
3686
4239
|
});
|
|
3687
4240
|
|
|
3688
4241
|
// src/commands/diff.ts
|
|
3689
|
-
import * as
|
|
3690
|
-
import * as
|
|
3691
|
-
import * as
|
|
3692
|
-
import { Command as
|
|
3693
|
-
import
|
|
4242
|
+
import * as path13 from "path";
|
|
4243
|
+
import * as fs5 from "fs/promises";
|
|
4244
|
+
import * as crypto4 from "crypto";
|
|
4245
|
+
import { Command as Command11 } from "commander";
|
|
4246
|
+
import chalk12 from "chalk";
|
|
3694
4247
|
var EXIT_INVALID_PATH = 2;
|
|
3695
4248
|
var EXIT_USAGE_ERROR2 = 5;
|
|
3696
4249
|
var VALID_FORMATS = /* @__PURE__ */ new Set(["ndjson", "json", "patch"]);
|
|
@@ -3706,14 +4259,14 @@ async function discoverFiles(root, includeGlobs, excludeGlobs) {
|
|
|
3706
4259
|
async function walk(dir, relBase) {
|
|
3707
4260
|
let entries;
|
|
3708
4261
|
try {
|
|
3709
|
-
entries = await
|
|
4262
|
+
entries = await fs5.readdir(dir, { withFileTypes: true });
|
|
3710
4263
|
} catch {
|
|
3711
4264
|
return;
|
|
3712
4265
|
}
|
|
3713
4266
|
for (const entry of entries) {
|
|
3714
4267
|
if (entry.name.startsWith(".") && SKIP_DIRS2.has(entry.name)) continue;
|
|
3715
|
-
const absPath =
|
|
3716
|
-
const relPath =
|
|
4268
|
+
const absPath = path13.join(dir, entry.name);
|
|
4269
|
+
const relPath = path13.join(relBase, entry.name);
|
|
3717
4270
|
if (entry.isDirectory()) {
|
|
3718
4271
|
if (!SKIP_DIRS2.has(entry.name)) {
|
|
3719
4272
|
await walk(absPath, relPath);
|
|
@@ -3884,7 +4437,7 @@ function lcsRatio(a, b) {
|
|
|
3884
4437
|
return 2 * lcsLen / (n + m);
|
|
3885
4438
|
}
|
|
3886
4439
|
function generateDeltaId(kind, filePath) {
|
|
3887
|
-
const hash =
|
|
4440
|
+
const hash = crypto4.createHash("sha256").update(`${kind}:${filePath}`).digest("hex").substring(0, 16);
|
|
3888
4441
|
return `hcs:delta:${hash}`;
|
|
3889
4442
|
}
|
|
3890
4443
|
function formatNdjson(deltas) {
|
|
@@ -3896,8 +4449,8 @@ function formatJson(deltas) {
|
|
|
3896
4449
|
function formatPatch(deltas, originalPath, generatedPath) {
|
|
3897
4450
|
const lines = [];
|
|
3898
4451
|
for (const delta of deltas) {
|
|
3899
|
-
const aFile = delta.originalPath ?
|
|
3900
|
-
const bFile = delta.generatedPath ?
|
|
4452
|
+
const aFile = delta.originalPath ? path13.join("a", delta.originalPath) : "/dev/null";
|
|
4453
|
+
const bFile = delta.generatedPath ? path13.join("b", delta.generatedPath) : "/dev/null";
|
|
3901
4454
|
if (delta.changeKind === "renamed" && delta.originalPath && delta.generatedPath) {
|
|
3902
4455
|
lines.push(`diff --vibgrate ${aFile} ${bFile}`);
|
|
3903
4456
|
lines.push(`similarity index ${Math.round((delta.similarity ?? 0) * 100)}%`);
|
|
@@ -3918,38 +4471,38 @@ function formatPatch(deltas, originalPath, generatedPath) {
|
|
|
3918
4471
|
}
|
|
3919
4472
|
return lines.join("\n");
|
|
3920
4473
|
}
|
|
3921
|
-
var diffCommand = new
|
|
3922
|
-
const origDir =
|
|
3923
|
-
const genDir =
|
|
4474
|
+
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) => {
|
|
4475
|
+
const origDir = path13.resolve(originalPath);
|
|
4476
|
+
const genDir = path13.resolve(generatedPath);
|
|
3924
4477
|
for (const [label, dir] of [["original_path", origDir], ["generated_path", genDir]]) {
|
|
3925
4478
|
if (!await pathExists(dir)) {
|
|
3926
|
-
process.stderr.write(
|
|
4479
|
+
process.stderr.write(chalk12.red(`${label} does not exist: ${dir}
|
|
3927
4480
|
`));
|
|
3928
4481
|
process.exit(EXIT_INVALID_PATH);
|
|
3929
4482
|
}
|
|
3930
|
-
let
|
|
4483
|
+
let stat4;
|
|
3931
4484
|
try {
|
|
3932
|
-
|
|
4485
|
+
stat4 = await fs5.stat(dir);
|
|
3933
4486
|
} catch {
|
|
3934
|
-
process.stderr.write(
|
|
4487
|
+
process.stderr.write(chalk12.red(`Cannot read ${label}: ${dir}
|
|
3935
4488
|
`));
|
|
3936
4489
|
process.exit(EXIT_INVALID_PATH);
|
|
3937
4490
|
}
|
|
3938
|
-
if (!
|
|
3939
|
-
process.stderr.write(
|
|
4491
|
+
if (!stat4.isDirectory()) {
|
|
4492
|
+
process.stderr.write(chalk12.red(`${label} must be a directory: ${dir}
|
|
3940
4493
|
`));
|
|
3941
4494
|
process.exit(EXIT_INVALID_PATH);
|
|
3942
4495
|
}
|
|
3943
4496
|
}
|
|
3944
4497
|
const format = opts.format.toLowerCase();
|
|
3945
4498
|
if (!VALID_FORMATS.has(format)) {
|
|
3946
|
-
process.stderr.write(
|
|
4499
|
+
process.stderr.write(chalk12.red(`Invalid format: "${opts.format}". Allowed: ndjson, json, patch
|
|
3947
4500
|
`));
|
|
3948
4501
|
process.exit(EXIT_USAGE_ERROR2);
|
|
3949
4502
|
}
|
|
3950
4503
|
const contextLines = parseInt(opts.context, 10);
|
|
3951
4504
|
if (isNaN(contextLines) || contextLines < 0) {
|
|
3952
|
-
process.stderr.write(
|
|
4505
|
+
process.stderr.write(chalk12.red("--context must be >= 0\n"));
|
|
3953
4506
|
process.exit(EXIT_USAGE_ERROR2);
|
|
3954
4507
|
}
|
|
3955
4508
|
const includeGlobs = opts.include ? opts.include.split(",").map((g) => g.trim()).filter(Boolean) : void 0;
|
|
@@ -3961,9 +4514,9 @@ var diffCommand = new Command9("diff").description("Compare two directory trees
|
|
|
3961
4514
|
const origMap = new Map(origFiles.map((f) => [f.relPath, f]));
|
|
3962
4515
|
const genMap = new Map(genFiles.map((f) => [f.relPath, f]));
|
|
3963
4516
|
if (opts.verbose) {
|
|
3964
|
-
process.stderr.write(
|
|
4517
|
+
process.stderr.write(chalk12.dim(`Original: ${origFiles.length} files
|
|
3965
4518
|
`));
|
|
3966
|
-
process.stderr.write(
|
|
4519
|
+
process.stderr.write(chalk12.dim(`Generated: ${genFiles.length} files
|
|
3967
4520
|
`));
|
|
3968
4521
|
}
|
|
3969
4522
|
const deltas = [];
|
|
@@ -3974,8 +4527,8 @@ var diffCommand = new Command9("diff").description("Compare two directory trees
|
|
|
3974
4527
|
if (genFile) {
|
|
3975
4528
|
matchedOrig.add(relPath);
|
|
3976
4529
|
matchedGen.add(relPath);
|
|
3977
|
-
const origContent = await
|
|
3978
|
-
const genContent = await
|
|
4530
|
+
const origContent = await fs5.readFile(origFile.absPath, "utf-8");
|
|
4531
|
+
const genContent = await fs5.readFile(genFile.absPath, "utf-8");
|
|
3979
4532
|
const origCompare = opts.ignoreWhitespace ? origContent.replace(/\s+/g, " ").trim() : origContent;
|
|
3980
4533
|
const genCompare = opts.ignoreWhitespace ? genContent.replace(/\s+/g, " ").trim() : genContent;
|
|
3981
4534
|
if (origCompare !== genCompare) {
|
|
@@ -3997,7 +4550,7 @@ var diffCommand = new Command9("diff").description("Compare two directory trees
|
|
|
3997
4550
|
}
|
|
3998
4551
|
deltas.push(delta);
|
|
3999
4552
|
if (opts.verbose) {
|
|
4000
|
-
process.stderr.write(
|
|
4553
|
+
process.stderr.write(chalk12.dim(` modified: ${relPath} (+${diffResult.insertions} -${diffResult.deletions})
|
|
4001
4554
|
`));
|
|
4002
4555
|
}
|
|
4003
4556
|
}
|
|
@@ -4014,13 +4567,13 @@ var diffCommand = new Command9("diff").description("Compare two directory trees
|
|
|
4014
4567
|
await Promise.all([
|
|
4015
4568
|
...unmatchedOrig.map(async (f) => {
|
|
4016
4569
|
try {
|
|
4017
|
-
origContents.set(f.relPath, await
|
|
4570
|
+
origContents.set(f.relPath, await fs5.readFile(f.absPath, "utf-8"));
|
|
4018
4571
|
} catch {
|
|
4019
4572
|
}
|
|
4020
4573
|
}),
|
|
4021
4574
|
...unmatchedGen.map(async (f) => {
|
|
4022
4575
|
try {
|
|
4023
|
-
genContents.set(f.relPath, await
|
|
4576
|
+
genContents.set(f.relPath, await fs5.readFile(f.absPath, "utf-8"));
|
|
4024
4577
|
} catch {
|
|
4025
4578
|
}
|
|
4026
4579
|
})
|
|
@@ -4028,7 +4581,7 @@ var diffCommand = new Command9("diff").description("Compare two directory trees
|
|
|
4028
4581
|
const pairs = [];
|
|
4029
4582
|
for (const [origPath, origContent] of origContents) {
|
|
4030
4583
|
for (const [genPath, genContent] of genContents) {
|
|
4031
|
-
if (
|
|
4584
|
+
if (path13.extname(origPath) !== path13.extname(genPath)) continue;
|
|
4032
4585
|
const similarity = computeSimilarity(origContent, genContent);
|
|
4033
4586
|
if (similarity >= RENAME_THRESHOLD) {
|
|
4034
4587
|
pairs.push({ origPath, genPath, similarity });
|
|
@@ -4061,7 +4614,7 @@ var diffCommand = new Command9("diff").description("Compare two directory trees
|
|
|
4061
4614
|
}
|
|
4062
4615
|
deltas.push(delta);
|
|
4063
4616
|
if (opts.verbose) {
|
|
4064
|
-
process.stderr.write(
|
|
4617
|
+
process.stderr.write(chalk12.dim(
|
|
4065
4618
|
` renamed: ${pair.origPath} \u2192 ${pair.genPath} (${(pair.similarity * 100).toFixed(0)}% similar)
|
|
4066
4619
|
`
|
|
4067
4620
|
));
|
|
@@ -4078,7 +4631,7 @@ var diffCommand = new Command9("diff").description("Compare two directory trees
|
|
|
4078
4631
|
};
|
|
4079
4632
|
if (opts.stats) {
|
|
4080
4633
|
try {
|
|
4081
|
-
const content = await
|
|
4634
|
+
const content = await fs5.readFile(file.absPath, "utf-8");
|
|
4082
4635
|
delta.stats = { insertions: 0, deletions: content.split("\n").length };
|
|
4083
4636
|
} catch {
|
|
4084
4637
|
delta.stats = { insertions: 0, deletions: 0 };
|
|
@@ -4086,7 +4639,7 @@ var diffCommand = new Command9("diff").description("Compare two directory trees
|
|
|
4086
4639
|
}
|
|
4087
4640
|
deltas.push(delta);
|
|
4088
4641
|
if (opts.verbose) {
|
|
4089
|
-
process.stderr.write(
|
|
4642
|
+
process.stderr.write(chalk12.dim(` removed: ${file.relPath}
|
|
4090
4643
|
`));
|
|
4091
4644
|
}
|
|
4092
4645
|
}
|
|
@@ -4100,7 +4653,7 @@ var diffCommand = new Command9("diff").description("Compare two directory trees
|
|
|
4100
4653
|
};
|
|
4101
4654
|
if (opts.stats) {
|
|
4102
4655
|
try {
|
|
4103
|
-
const content = await
|
|
4656
|
+
const content = await fs5.readFile(file.absPath, "utf-8");
|
|
4104
4657
|
delta.stats = { insertions: content.split("\n").length, deletions: 0 };
|
|
4105
4658
|
} catch {
|
|
4106
4659
|
delta.stats = { insertions: 0, deletions: 0 };
|
|
@@ -4108,7 +4661,7 @@ var diffCommand = new Command9("diff").description("Compare two directory trees
|
|
|
4108
4661
|
}
|
|
4109
4662
|
deltas.push(delta);
|
|
4110
4663
|
if (opts.verbose) {
|
|
4111
|
-
process.stderr.write(
|
|
4664
|
+
process.stderr.write(chalk12.dim(` added: ${file.relPath}
|
|
4112
4665
|
`));
|
|
4113
4666
|
}
|
|
4114
4667
|
}
|
|
@@ -4129,16 +4682,16 @@ var diffCommand = new Command9("diff").description("Compare two directory trees
|
|
|
4129
4682
|
output = formatPatch(deltas, originalPath, generatedPath);
|
|
4130
4683
|
break;
|
|
4131
4684
|
}
|
|
4132
|
-
const outPath =
|
|
4133
|
-
await
|
|
4685
|
+
const outPath = path13.resolve(opts.out);
|
|
4686
|
+
await fs5.writeFile(outPath, output, "utf-8");
|
|
4134
4687
|
const added = deltas.filter((d) => d.changeKind === "added").length;
|
|
4135
4688
|
const removed = deltas.filter((d) => d.changeKind === "removed").length;
|
|
4136
4689
|
const modified = deltas.filter((d) => d.changeKind === "modified").length;
|
|
4137
4690
|
const renamed = deltas.filter((d) => d.changeKind === "renamed").length;
|
|
4138
4691
|
process.stderr.write(
|
|
4139
|
-
|
|
4692
|
+
chalk12.green(`\u2714 `) + `${deltas.length} changes: ` + chalk12.green(`+${added} added`) + ", " + chalk12.red(`-${removed} removed`) + ", " + chalk12.yellow(`~${modified} modified`) + ", " + chalk12.blue(`\u2192${renamed} renamed`) + "\n"
|
|
4140
4693
|
);
|
|
4141
|
-
process.stderr.write(
|
|
4694
|
+
process.stderr.write(chalk12.dim(` Written to ${outPath}
|
|
4142
4695
|
`));
|
|
4143
4696
|
if (opts.verbose && opts.stats) {
|
|
4144
4697
|
let totalIns = 0;
|
|
@@ -4149,193 +4702,193 @@ var diffCommand = new Command9("diff").description("Compare two directory trees
|
|
|
4149
4702
|
totalDel += d.stats.deletions;
|
|
4150
4703
|
}
|
|
4151
4704
|
}
|
|
4152
|
-
process.stderr.write(
|
|
4705
|
+
process.stderr.write(chalk12.dim(` Total: +${totalIns} insertions, -${totalDel} deletions
|
|
4153
4706
|
`));
|
|
4154
4707
|
}
|
|
4155
4708
|
});
|
|
4156
4709
|
|
|
4157
4710
|
// src/commands/help.ts
|
|
4158
|
-
import { Command as
|
|
4159
|
-
import
|
|
4711
|
+
import { Command as Command12 } from "commander";
|
|
4712
|
+
import chalk13 from "chalk";
|
|
4160
4713
|
var HELP_URL = "https://vibgrate.com/help";
|
|
4161
4714
|
function printFooter() {
|
|
4162
4715
|
console.log("");
|
|
4163
|
-
console.log(
|
|
4716
|
+
console.log(chalk13.dim(`See ${HELP_URL} for more guidance`));
|
|
4164
4717
|
}
|
|
4165
4718
|
var detailedHelp = {
|
|
4166
4719
|
scan: () => {
|
|
4167
4720
|
console.log("");
|
|
4168
|
-
console.log(
|
|
4721
|
+
console.log(chalk13.bold.underline("vibgrate scan") + chalk13.dim(" \u2014 Scan a project for upgrade drift"));
|
|
4169
4722
|
console.log("");
|
|
4170
|
-
console.log(
|
|
4723
|
+
console.log(chalk13.bold("Usage:"));
|
|
4171
4724
|
console.log(" vibgrate scan [path] [options]");
|
|
4172
4725
|
console.log("");
|
|
4173
|
-
console.log(
|
|
4174
|
-
console.log(` ${
|
|
4726
|
+
console.log(chalk13.bold("Arguments:"));
|
|
4727
|
+
console.log(` ${chalk13.cyan("[path]")} Path to scan (default: current directory)`);
|
|
4175
4728
|
console.log("");
|
|
4176
|
-
console.log(
|
|
4177
|
-
console.log(` ${
|
|
4178
|
-
console.log(` ${
|
|
4729
|
+
console.log(chalk13.bold("Output options:"));
|
|
4730
|
+
console.log(` ${chalk13.cyan("--format <format>")} Output format: ${chalk13.white("text")} | json | sarif | md (default: text)`);
|
|
4731
|
+
console.log(` ${chalk13.cyan("--out <file>")} Write output to a file instead of stdout`);
|
|
4179
4732
|
console.log("");
|
|
4180
|
-
console.log(
|
|
4181
|
-
console.log(` ${
|
|
4182
|
-
console.log(` ${
|
|
4183
|
-
console.log(` ${
|
|
4184
|
-
console.log(` ${
|
|
4733
|
+
console.log(chalk13.bold("Baseline & gating:"));
|
|
4734
|
+
console.log(` ${chalk13.cyan("--baseline <file>")} Compare results against a saved baseline`);
|
|
4735
|
+
console.log(` ${chalk13.cyan("--drift-budget <score>")} Fail if drift score exceeds this value (0\u2013100)`);
|
|
4736
|
+
console.log(` ${chalk13.cyan("--drift-worsening <percent>")} Fail if drift worsens by more than % since baseline`);
|
|
4737
|
+
console.log(` ${chalk13.cyan("--fail-on <level>")} Fail exit code on warn or error findings`);
|
|
4185
4738
|
console.log("");
|
|
4186
|
-
console.log(
|
|
4187
|
-
console.log(` ${
|
|
4188
|
-
console.log(` ${
|
|
4739
|
+
console.log(chalk13.bold("Performance:"));
|
|
4740
|
+
console.log(` ${chalk13.cyan("--concurrency <n>")} Max concurrent registry calls (default: 8)`);
|
|
4741
|
+
console.log(` ${chalk13.cyan("--changed-only")} Only scan files changed since last git commit`);
|
|
4189
4742
|
console.log("");
|
|
4190
|
-
console.log(
|
|
4191
|
-
console.log(` ${
|
|
4192
|
-
console.log(` ${
|
|
4193
|
-
console.log(` ${
|
|
4194
|
-
console.log(` ${
|
|
4743
|
+
console.log(chalk13.bold("Privacy & offline:"));
|
|
4744
|
+
console.log(` ${chalk13.cyan("--offline")} Run without any network calls; skip result upload`);
|
|
4745
|
+
console.log(` ${chalk13.cyan("--package-manifest <file>")} Use a local package-version manifest (JSON or ZIP) for offline mode`);
|
|
4746
|
+
console.log(` ${chalk13.cyan("--no-local-artifacts")} Do not write .vibgrate JSON artifacts to disk`);
|
|
4747
|
+
console.log(` ${chalk13.cyan("--max-privacy")} Strongest privacy mode: minimal scanners + no local artifacts`);
|
|
4195
4748
|
console.log("");
|
|
4196
|
-
console.log(
|
|
4197
|
-
console.log(` ${
|
|
4198
|
-
console.log(` ${
|
|
4749
|
+
console.log(chalk13.bold("Tooling:"));
|
|
4750
|
+
console.log(` ${chalk13.cyan("--install-tools")} Auto-install missing security scanners via Homebrew`);
|
|
4751
|
+
console.log(` ${chalk13.cyan("--ui-purpose")} Enable UI purpose evidence extraction (slower)`);
|
|
4199
4752
|
console.log("");
|
|
4200
|
-
console.log(
|
|
4201
|
-
console.log(` ${
|
|
4202
|
-
console.log(` ${
|
|
4203
|
-
console.log(` ${
|
|
4204
|
-
console.log(` ${
|
|
4753
|
+
console.log(chalk13.bold("Uploading results:"));
|
|
4754
|
+
console.log(` ${chalk13.cyan("--push")} Auto-push results to Vibgrate API after scan`);
|
|
4755
|
+
console.log(` ${chalk13.cyan("--dsn <dsn>")} DSN token for push (or set VIBGRATE_DSN env var)`);
|
|
4756
|
+
console.log(` ${chalk13.cyan("--region <region>")} Data residency region: us | eu (default: us)`);
|
|
4757
|
+
console.log(` ${chalk13.cyan("--strict")} Fail if the upload to Vibgrate API fails`);
|
|
4205
4758
|
console.log("");
|
|
4206
|
-
console.log(
|
|
4207
|
-
console.log(` ${
|
|
4759
|
+
console.log(chalk13.bold("Examples:"));
|
|
4760
|
+
console.log(` ${chalk13.dim("# Scan the current directory and display a text report")}`);
|
|
4208
4761
|
console.log(" vibgrate scan .");
|
|
4209
4762
|
console.log("");
|
|
4210
|
-
console.log(` ${
|
|
4763
|
+
console.log(` ${chalk13.dim("# Scan, fail if drift score > 40, and write SARIF for GitHub Actions")}`);
|
|
4211
4764
|
console.log(" vibgrate scan . --drift-budget 40 --format sarif --out drift.sarif");
|
|
4212
4765
|
console.log("");
|
|
4213
|
-
console.log(` ${
|
|
4766
|
+
console.log(` ${chalk13.dim("# Scan and automatically upload results via a DSN")}`);
|
|
4214
4767
|
console.log(" vibgrate scan . --push --dsn $VIBGRATE_DSN");
|
|
4215
4768
|
console.log("");
|
|
4216
|
-
console.log(` ${
|
|
4769
|
+
console.log(` ${chalk13.dim("# Offline scan using a pre-downloaded package manifest")}`);
|
|
4217
4770
|
console.log(" vibgrate scan . --offline --package-manifest ./manifest.zip");
|
|
4218
4771
|
},
|
|
4219
4772
|
init: () => {
|
|
4220
4773
|
console.log("");
|
|
4221
|
-
console.log(
|
|
4774
|
+
console.log(chalk13.bold.underline("vibgrate init") + chalk13.dim(" \u2014 Initialise vibgrate in a project directory"));
|
|
4222
4775
|
console.log("");
|
|
4223
|
-
console.log(
|
|
4776
|
+
console.log(chalk13.bold("Usage:"));
|
|
4224
4777
|
console.log(" vibgrate init [path] [options]");
|
|
4225
4778
|
console.log("");
|
|
4226
|
-
console.log(
|
|
4227
|
-
console.log(` ${
|
|
4779
|
+
console.log(chalk13.bold("Arguments:"));
|
|
4780
|
+
console.log(` ${chalk13.cyan("[path]")} Directory to initialise (default: current directory)`);
|
|
4228
4781
|
console.log("");
|
|
4229
|
-
console.log(
|
|
4230
|
-
console.log(` ${
|
|
4231
|
-
console.log(` ${
|
|
4782
|
+
console.log(chalk13.bold("Options:"));
|
|
4783
|
+
console.log(` ${chalk13.cyan("--baseline")} Create an initial drift baseline after init`);
|
|
4784
|
+
console.log(` ${chalk13.cyan("--yes")} Skip all confirmation prompts`);
|
|
4232
4785
|
console.log("");
|
|
4233
|
-
console.log(
|
|
4786
|
+
console.log(chalk13.bold("What it does:"));
|
|
4234
4787
|
console.log(" \u2022 Creates a .vibgrate/ directory");
|
|
4235
4788
|
console.log(" \u2022 Writes a vibgrate.config.ts starter config");
|
|
4236
4789
|
console.log(" \u2022 Optionally runs an initial baseline scan (--baseline)");
|
|
4237
4790
|
console.log("");
|
|
4238
|
-
console.log(
|
|
4791
|
+
console.log(chalk13.bold("Examples:"));
|
|
4239
4792
|
console.log(" vibgrate init");
|
|
4240
4793
|
console.log(" vibgrate init ./my-project --baseline");
|
|
4241
4794
|
},
|
|
4242
4795
|
baseline: () => {
|
|
4243
4796
|
console.log("");
|
|
4244
|
-
console.log(
|
|
4797
|
+
console.log(chalk13.bold.underline("vibgrate baseline") + chalk13.dim(" \u2014 Save a drift baseline snapshot"));
|
|
4245
4798
|
console.log("");
|
|
4246
|
-
console.log(
|
|
4799
|
+
console.log(chalk13.bold("Usage:"));
|
|
4247
4800
|
console.log(" vibgrate baseline [path]");
|
|
4248
4801
|
console.log("");
|
|
4249
|
-
console.log(
|
|
4250
|
-
console.log(` ${
|
|
4802
|
+
console.log(chalk13.bold("Arguments:"));
|
|
4803
|
+
console.log(` ${chalk13.cyan("[path]")} Path to baseline (default: current directory)`);
|
|
4251
4804
|
console.log("");
|
|
4252
|
-
console.log(
|
|
4805
|
+
console.log(chalk13.bold("What it does:"));
|
|
4253
4806
|
console.log(" Runs a full scan and saves the result as .vibgrate/baseline.json.");
|
|
4254
4807
|
console.log(" Future scans can compare against this file using --baseline.");
|
|
4255
4808
|
console.log("");
|
|
4256
|
-
console.log(
|
|
4809
|
+
console.log(chalk13.bold("Examples:"));
|
|
4257
4810
|
console.log(" vibgrate baseline .");
|
|
4258
4811
|
console.log(" vibgrate scan . --baseline .vibgrate/baseline.json --drift-worsening 10");
|
|
4259
4812
|
},
|
|
4260
4813
|
report: () => {
|
|
4261
4814
|
console.log("");
|
|
4262
|
-
console.log(
|
|
4815
|
+
console.log(chalk13.bold.underline("vibgrate report") + chalk13.dim(" \u2014 Generate a report from a saved scan artifact"));
|
|
4263
4816
|
console.log("");
|
|
4264
|
-
console.log(
|
|
4817
|
+
console.log(chalk13.bold("Usage:"));
|
|
4265
4818
|
console.log(" vibgrate report [options]");
|
|
4266
4819
|
console.log("");
|
|
4267
|
-
console.log(
|
|
4268
|
-
console.log(` ${
|
|
4269
|
-
console.log(` ${
|
|
4820
|
+
console.log(chalk13.bold("Options:"));
|
|
4821
|
+
console.log(` ${chalk13.cyan("--in <file>")} Input artifact file (default: .vibgrate/scan_result.json)`);
|
|
4822
|
+
console.log(` ${chalk13.cyan("--format <format>")} Output format: ${chalk13.white("text")} | md | json (default: text)`);
|
|
4270
4823
|
console.log("");
|
|
4271
|
-
console.log(
|
|
4824
|
+
console.log(chalk13.bold("Examples:"));
|
|
4272
4825
|
console.log(" vibgrate report");
|
|
4273
4826
|
console.log(" vibgrate report --format md > DRIFT-REPORT.md");
|
|
4274
4827
|
console.log(" vibgrate report --in ./ci/scan_result.json --format json");
|
|
4275
4828
|
},
|
|
4276
4829
|
sbom: () => {
|
|
4277
4830
|
console.log("");
|
|
4278
|
-
console.log(
|
|
4831
|
+
console.log(chalk13.bold.underline("vibgrate sbom") + chalk13.dim(" \u2014 Export a Software Bill of Materials from a scan artifact"));
|
|
4279
4832
|
console.log("");
|
|
4280
|
-
console.log(
|
|
4833
|
+
console.log(chalk13.bold("Usage:"));
|
|
4281
4834
|
console.log(" vibgrate sbom [options]");
|
|
4282
4835
|
console.log("");
|
|
4283
|
-
console.log(
|
|
4284
|
-
console.log(` ${
|
|
4285
|
-
console.log(` ${
|
|
4286
|
-
console.log(` ${
|
|
4836
|
+
console.log(chalk13.bold("Options:"));
|
|
4837
|
+
console.log(` ${chalk13.cyan("--in <file>")} Input artifact (default: .vibgrate/scan_result.json)`);
|
|
4838
|
+
console.log(` ${chalk13.cyan("--format <format>")} SBOM format: ${chalk13.white("cyclonedx")} | spdx (default: cyclonedx)`);
|
|
4839
|
+
console.log(` ${chalk13.cyan("--out <file>")} Write SBOM to file instead of stdout`);
|
|
4287
4840
|
console.log("");
|
|
4288
|
-
console.log(
|
|
4841
|
+
console.log(chalk13.bold("Examples:"));
|
|
4289
4842
|
console.log(" vibgrate sbom --format cyclonedx --out sbom.json");
|
|
4290
4843
|
console.log(" vibgrate sbom --format spdx --out sbom.spdx.json");
|
|
4291
4844
|
},
|
|
4292
4845
|
push: () => {
|
|
4293
4846
|
console.log("");
|
|
4294
|
-
console.log(
|
|
4847
|
+
console.log(chalk13.bold.underline("vibgrate push") + chalk13.dim(" \u2014 Upload a scan artifact to the Vibgrate API"));
|
|
4295
4848
|
console.log("");
|
|
4296
|
-
console.log(
|
|
4849
|
+
console.log(chalk13.bold("Usage:"));
|
|
4297
4850
|
console.log(" vibgrate push [options]");
|
|
4298
4851
|
console.log("");
|
|
4299
|
-
console.log(
|
|
4300
|
-
console.log(` ${
|
|
4301
|
-
console.log(` ${
|
|
4302
|
-
console.log(` ${
|
|
4303
|
-
console.log(` ${
|
|
4852
|
+
console.log(chalk13.bold("Options:"));
|
|
4853
|
+
console.log(` ${chalk13.cyan("--dsn <dsn>")} DSN token (or set VIBGRATE_DSN env var)`);
|
|
4854
|
+
console.log(` ${chalk13.cyan("--file <file>")} Artifact to upload (default: .vibgrate/scan_result.json)`);
|
|
4855
|
+
console.log(` ${chalk13.cyan("--region <region>")} Override data residency region: us | eu`);
|
|
4856
|
+
console.log(` ${chalk13.cyan("--strict")} Fail with non-zero exit code on upload error`);
|
|
4304
4857
|
console.log("");
|
|
4305
|
-
console.log(
|
|
4858
|
+
console.log(chalk13.bold("Examples:"));
|
|
4306
4859
|
console.log(" vibgrate push --dsn $VIBGRATE_DSN");
|
|
4307
4860
|
console.log(" vibgrate push --file ./ci/scan_result.json --strict");
|
|
4308
4861
|
},
|
|
4309
4862
|
dsn: () => {
|
|
4310
4863
|
console.log("");
|
|
4311
|
-
console.log(
|
|
4864
|
+
console.log(chalk13.bold.underline("vibgrate dsn") + chalk13.dim(" \u2014 Manage DSN tokens for API authentication"));
|
|
4312
4865
|
console.log("");
|
|
4313
|
-
console.log(
|
|
4314
|
-
console.log(` ${
|
|
4866
|
+
console.log(chalk13.bold("Subcommands:"));
|
|
4867
|
+
console.log(` ${chalk13.cyan("vibgrate dsn create")} Generate a new DSN token`);
|
|
4315
4868
|
console.log("");
|
|
4316
|
-
console.log(
|
|
4317
|
-
console.log(` ${
|
|
4318
|
-
console.log(` ${
|
|
4319
|
-
console.log(` ${
|
|
4320
|
-
console.log(` ${
|
|
4869
|
+
console.log(chalk13.bold("dsn create options:"));
|
|
4870
|
+
console.log(` ${chalk13.cyan("--workspace <id>")} Workspace ID or "new" to auto-generate ${chalk13.red("(required)")}`);
|
|
4871
|
+
console.log(` ${chalk13.cyan("--region <region>")} Data residency region: us | eu (default: us)`);
|
|
4872
|
+
console.log(` ${chalk13.cyan("--ingest <url>")} Override ingest API URL`);
|
|
4873
|
+
console.log(` ${chalk13.cyan("--write <path>")} Write the DSN to a file (add to .gitignore!)`);
|
|
4321
4874
|
console.log("");
|
|
4322
|
-
console.log(
|
|
4875
|
+
console.log(chalk13.bold("Examples:"));
|
|
4323
4876
|
console.log(" vibgrate dsn create --workspace new");
|
|
4324
4877
|
console.log(" vibgrate dsn create --workspace abc123");
|
|
4325
4878
|
console.log(" vibgrate dsn create --workspace new --region eu --write .vibgrate/.dsn");
|
|
4326
4879
|
},
|
|
4327
4880
|
update: () => {
|
|
4328
4881
|
console.log("");
|
|
4329
|
-
console.log(
|
|
4882
|
+
console.log(chalk13.bold.underline("vibgrate update") + chalk13.dim(" \u2014 Update the vibgrate CLI to the latest version"));
|
|
4330
4883
|
console.log("");
|
|
4331
|
-
console.log(
|
|
4884
|
+
console.log(chalk13.bold("Usage:"));
|
|
4332
4885
|
console.log(" vibgrate update [options]");
|
|
4333
4886
|
console.log("");
|
|
4334
|
-
console.log(
|
|
4335
|
-
console.log(` ${
|
|
4336
|
-
console.log(` ${
|
|
4887
|
+
console.log(chalk13.bold("Options:"));
|
|
4888
|
+
console.log(` ${chalk13.cyan("--check")} Check for a newer version without installing`);
|
|
4889
|
+
console.log(` ${chalk13.cyan("--pm <manager>")} Force a package manager: npm | pnpm | yarn | bun`);
|
|
4337
4890
|
console.log("");
|
|
4338
|
-
console.log(
|
|
4891
|
+
console.log(chalk13.bold("Examples:"));
|
|
4339
4892
|
console.log(" vibgrate update");
|
|
4340
4893
|
console.log(" vibgrate update --check");
|
|
4341
4894
|
console.log(" vibgrate update --pm pnpm");
|
|
@@ -4343,40 +4896,42 @@ var detailedHelp = {
|
|
|
4343
4896
|
};
|
|
4344
4897
|
function printSummaryHelp() {
|
|
4345
4898
|
console.log("");
|
|
4346
|
-
console.log(
|
|
4899
|
+
console.log(chalk13.bold("vibgrate") + chalk13.dim(" \u2014 Continuous Drift Intelligence"));
|
|
4347
4900
|
console.log("");
|
|
4348
|
-
console.log(
|
|
4901
|
+
console.log(chalk13.bold("Usage:"));
|
|
4349
4902
|
console.log(" vibgrate <command> [options]");
|
|
4350
4903
|
console.log(" vibgrate help [command] Show detailed help for a command");
|
|
4351
4904
|
console.log("");
|
|
4352
|
-
console.log(
|
|
4353
|
-
console.log(` ${
|
|
4905
|
+
console.log(chalk13.bold("Getting started:"));
|
|
4906
|
+
console.log(` ${chalk13.cyan("init")} Initialise vibgrate in a project (creates config & .vibgrate/ dir)`);
|
|
4907
|
+
console.log(` ${chalk13.cyan("login")} Sign the CLI into your workspace via the browser`);
|
|
4908
|
+
console.log(` ${chalk13.cyan("logout")} Clear stored login credentials`);
|
|
4354
4909
|
console.log("");
|
|
4355
|
-
console.log(
|
|
4356
|
-
console.log(` ${
|
|
4357
|
-
console.log(` ${
|
|
4910
|
+
console.log(chalk13.bold("Core scanning:"));
|
|
4911
|
+
console.log(` ${chalk13.cyan("scan")} Scan a project for upgrade drift and generate a report`);
|
|
4912
|
+
console.log(` ${chalk13.cyan("baseline")} Save a baseline snapshot to compare future scans against`);
|
|
4358
4913
|
console.log("");
|
|
4359
|
-
console.log(
|
|
4360
|
-
console.log(` ${
|
|
4361
|
-
console.log(` ${
|
|
4914
|
+
console.log(chalk13.bold("Reporting & export:"));
|
|
4915
|
+
console.log(` ${chalk13.cyan("report")} Re-generate a report from a previously saved scan artifact`);
|
|
4916
|
+
console.log(` ${chalk13.cyan("sbom")} Export a Software Bill of Materials (CycloneDX or SPDX)`);
|
|
4362
4917
|
console.log("");
|
|
4363
|
-
console.log(
|
|
4364
|
-
console.log(` ${
|
|
4365
|
-
console.log(` ${
|
|
4918
|
+
console.log(chalk13.bold("CI/CD integration:"));
|
|
4919
|
+
console.log(` ${chalk13.cyan("push")} Upload a scan artifact to the Vibgrate API`);
|
|
4920
|
+
console.log(` ${chalk13.cyan("dsn")} Create and manage DSN tokens for API authentication`);
|
|
4366
4921
|
console.log("");
|
|
4367
|
-
console.log(
|
|
4368
|
-
console.log(` ${
|
|
4922
|
+
console.log(chalk13.bold("Maintenance:"));
|
|
4923
|
+
console.log(` ${chalk13.cyan("update")} Update the vibgrate CLI to the latest version`);
|
|
4369
4924
|
console.log("");
|
|
4370
|
-
console.log(
|
|
4925
|
+
console.log(chalk13.dim("Run") + ` ${chalk13.cyan("vibgrate help <command>")} ` + chalk13.dim("for detailed options, e.g.") + ` ${chalk13.cyan("vibgrate help scan")}`);
|
|
4371
4926
|
}
|
|
4372
|
-
var helpCommand = new
|
|
4927
|
+
var helpCommand = new Command12("help").description("Show help for vibgrate commands").argument("[command]", "Command to show detailed help for").helpOption(false).action((cmd) => {
|
|
4373
4928
|
const name = cmd?.toLowerCase().trim();
|
|
4374
4929
|
if (name && detailedHelp[name]) {
|
|
4375
4930
|
detailedHelp[name]();
|
|
4376
4931
|
} else if (name) {
|
|
4377
4932
|
console.log("");
|
|
4378
|
-
console.log(
|
|
4379
|
-
console.log(
|
|
4933
|
+
console.log(chalk13.red(`Unknown command: ${name}`));
|
|
4934
|
+
console.log(chalk13.dim(`Available commands: ${Object.keys(detailedHelp).join(", ")}`));
|
|
4380
4935
|
printSummaryHelp();
|
|
4381
4936
|
} else {
|
|
4382
4937
|
printSummaryHelp();
|
|
@@ -4385,13 +4940,15 @@ var helpCommand = new Command10("help").description("Show help for vibgrate comm
|
|
|
4385
4940
|
});
|
|
4386
4941
|
|
|
4387
4942
|
// src/cli.ts
|
|
4388
|
-
var program = new
|
|
4943
|
+
var program = new Command13();
|
|
4389
4944
|
program.name("vibgrate").description("Continuous Drift Intelligence").version(VERSION).addHelpText("after", "\nSee https://vibgrate.com/help for more guidance");
|
|
4390
4945
|
program.addCommand(helpCommand);
|
|
4391
4946
|
program.addCommand(initCommand);
|
|
4392
4947
|
program.addCommand(scanCommand);
|
|
4393
4948
|
program.addCommand(baselineCommand);
|
|
4394
4949
|
program.addCommand(reportCommand);
|
|
4950
|
+
program.addCommand(loginCommand);
|
|
4951
|
+
program.addCommand(logoutCommand);
|
|
4395
4952
|
program.addCommand(dsnCommand);
|
|
4396
4953
|
program.addCommand(pushCommand);
|
|
4397
4954
|
program.addCommand(updateCommand);
|
|
@@ -4402,8 +4959,8 @@ function notifyIfUpdateAvailable() {
|
|
|
4402
4959
|
void checkForUpdate().then((update) => {
|
|
4403
4960
|
if (!update?.updateAvailable) return;
|
|
4404
4961
|
console.error("");
|
|
4405
|
-
console.error(
|
|
4406
|
-
console.error(
|
|
4962
|
+
console.error(chalk14.yellow(` Update available: ${update.current} \u2192 ${update.latest}`));
|
|
4963
|
+
console.error(chalk14.dim(' Run "vibgrate update" to install the latest version.'));
|
|
4407
4964
|
console.error("");
|
|
4408
4965
|
}).catch(() => {
|
|
4409
4966
|
});
|