@vibgrate/cli 1.0.76 → 1.0.77
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
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
baselineCommand
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-DKSPLRJV.js";
|
|
5
5
|
import {
|
|
6
6
|
VERSION,
|
|
7
|
+
computeHmac,
|
|
7
8
|
dsnCommand,
|
|
8
9
|
formatMarkdown,
|
|
9
10
|
formatText,
|
|
11
|
+
parseDsn,
|
|
10
12
|
pushCommand,
|
|
11
13
|
scanCommand,
|
|
12
14
|
writeDefaultConfig
|
|
13
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-TKNDQ337.js";
|
|
14
16
|
import {
|
|
17
|
+
Semaphore,
|
|
15
18
|
ensureDir,
|
|
16
19
|
pathExists,
|
|
17
20
|
readJsonFile,
|
|
@@ -19,8 +22,8 @@ import {
|
|
|
19
22
|
} from "./chunk-JQHUH6A3.js";
|
|
20
23
|
|
|
21
24
|
// src/cli.ts
|
|
22
|
-
import { Command as
|
|
23
|
-
import
|
|
25
|
+
import { Command as Command8 } from "commander";
|
|
26
|
+
import chalk8 from "chalk";
|
|
24
27
|
|
|
25
28
|
// src/commands/init.ts
|
|
26
29
|
import * as path from "path";
|
|
@@ -39,7 +42,7 @@ var initCommand = new Command("init").description("Initialize vibgrate in a proj
|
|
|
39
42
|
console.log(chalk.green("\u2714") + ` Created ${chalk.bold("vibgrate.config.ts")}`);
|
|
40
43
|
}
|
|
41
44
|
if (opts.baseline) {
|
|
42
|
-
const { runBaseline } = await import("./baseline-
|
|
45
|
+
const { runBaseline } = await import("./baseline-NRKROYVK.js");
|
|
43
46
|
await runBaseline(rootDir);
|
|
44
47
|
}
|
|
45
48
|
console.log("");
|
|
@@ -409,188 +412,1270 @@ var deltaCommand = new Command4("delta").description("Show SBOM delta between tw
|
|
|
409
412
|
});
|
|
410
413
|
var sbomCommand = new Command4("sbom").description("SBOM export and delta reports for dependency drift tracking").addCommand(exportCommand).addCommand(deltaCommand);
|
|
411
414
|
|
|
412
|
-
// src/commands/
|
|
415
|
+
// src/commands/extract.ts
|
|
416
|
+
import * as path6 from "path";
|
|
417
|
+
import * as os2 from "os";
|
|
418
|
+
import * as fs2 from "fs/promises";
|
|
419
|
+
import { existsSync } from "fs";
|
|
420
|
+
import { spawn } from "child_process";
|
|
413
421
|
import { Command as Command5 } from "commander";
|
|
414
422
|
import chalk5 from "chalk";
|
|
423
|
+
var EXIT_SUCCESS = 0;
|
|
424
|
+
var EXIT_SCHEMA_FAILURE = 1;
|
|
425
|
+
var EXIT_PARSE_FAILURE = 2;
|
|
426
|
+
var EXIT_TIMEOUT = 3;
|
|
427
|
+
var EXIT_PUSH_FAILURE = 4;
|
|
428
|
+
var EXIT_USAGE_ERROR = 5;
|
|
429
|
+
var LANGUAGE_EXTENSIONS = {
|
|
430
|
+
typescript: /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts"]),
|
|
431
|
+
javascript: /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs"]),
|
|
432
|
+
swift: /* @__PURE__ */ new Set([".swift"]),
|
|
433
|
+
rust: /* @__PURE__ */ new Set([".rs"]),
|
|
434
|
+
ruby: /* @__PURE__ */ new Set([".rb", ".rake", ".gemspec"]),
|
|
435
|
+
php: /* @__PURE__ */ new Set([".php"]),
|
|
436
|
+
dart: /* @__PURE__ */ new Set([".dart"]),
|
|
437
|
+
scala: /* @__PURE__ */ new Set([".scala", ".sc"]),
|
|
438
|
+
cobol: /* @__PURE__ */ new Set([".cbl", ".cob", ".cpy", ".cob85", ".cobol"]),
|
|
439
|
+
vb6: /* @__PURE__ */ new Set([".vbp", ".bas", ".cls", ".frm", ".ctl", ".dsr"]),
|
|
440
|
+
go: /* @__PURE__ */ new Set([".go"]),
|
|
441
|
+
python: /* @__PURE__ */ new Set([".py"]),
|
|
442
|
+
java: /* @__PURE__ */ new Set([".java"]),
|
|
443
|
+
csharp: /* @__PURE__ */ new Set([".cs"])
|
|
444
|
+
};
|
|
445
|
+
var SUPPORTED_LANGUAGES = new Set(Object.keys(LANGUAGE_EXTENSIONS));
|
|
446
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
447
|
+
"node_modules",
|
|
448
|
+
".git",
|
|
449
|
+
".vibgrate",
|
|
450
|
+
".hg",
|
|
451
|
+
".svn",
|
|
452
|
+
"dist",
|
|
453
|
+
"build",
|
|
454
|
+
"out",
|
|
455
|
+
".next",
|
|
456
|
+
".nuxt",
|
|
457
|
+
".output",
|
|
458
|
+
"bin",
|
|
459
|
+
"obj",
|
|
460
|
+
".vs",
|
|
461
|
+
"target",
|
|
462
|
+
"vendor",
|
|
463
|
+
".dart_tool",
|
|
464
|
+
".build",
|
|
465
|
+
"DerivedData",
|
|
466
|
+
"Pods",
|
|
467
|
+
".swiftpm",
|
|
468
|
+
"Carthage",
|
|
469
|
+
".cargo",
|
|
470
|
+
".pub-cache",
|
|
471
|
+
".bloop",
|
|
472
|
+
".metals",
|
|
473
|
+
".idea",
|
|
474
|
+
"coverage",
|
|
475
|
+
"TestResults",
|
|
476
|
+
"__pycache__",
|
|
477
|
+
".tox",
|
|
478
|
+
".mypy_cache"
|
|
479
|
+
]);
|
|
480
|
+
var TEST_PATTERNS = [
|
|
481
|
+
/[/\\]test[/\\]/i,
|
|
482
|
+
/[/\\]tests[/\\]/i,
|
|
483
|
+
/[/\\]__tests__[/\\]/i,
|
|
484
|
+
/[/\\]spec[/\\]/i,
|
|
485
|
+
/\.spec\./i,
|
|
486
|
+
/\.test\./i,
|
|
487
|
+
/[/\\]test-/i
|
|
488
|
+
];
|
|
489
|
+
function isTestPath(relPath) {
|
|
490
|
+
return TEST_PATTERNS.some((p) => p.test(relPath));
|
|
491
|
+
}
|
|
492
|
+
async function detectLanguages(rootDir, includeTests) {
|
|
493
|
+
const counts = /* @__PURE__ */ new Map();
|
|
494
|
+
async function walk(dir, relBase) {
|
|
495
|
+
let entries;
|
|
496
|
+
try {
|
|
497
|
+
entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
498
|
+
} catch {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
for (const entry of entries) {
|
|
502
|
+
if (entry.name.startsWith(".")) continue;
|
|
503
|
+
const absPath = path6.join(dir, entry.name);
|
|
504
|
+
const relPath = path6.join(relBase, entry.name);
|
|
505
|
+
if (entry.isDirectory()) {
|
|
506
|
+
if (!SKIP_DIRS.has(entry.name)) {
|
|
507
|
+
await walk(absPath, relPath);
|
|
508
|
+
}
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
if (!entry.isFile()) continue;
|
|
512
|
+
if (!includeTests && isTestPath(relPath)) continue;
|
|
513
|
+
const ext = path6.extname(entry.name).toLowerCase();
|
|
514
|
+
for (const [lang, exts] of Object.entries(LANGUAGE_EXTENSIONS)) {
|
|
515
|
+
if (exts.has(ext)) {
|
|
516
|
+
counts.set(lang, (counts.get(lang) ?? 0) + 1);
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
await walk(rootDir, "");
|
|
523
|
+
return Array.from(counts.entries()).map(([language, fileCount]) => ({ language, fileCount })).sort((a, b) => b.fileCount - a.fileCount);
|
|
524
|
+
}
|
|
525
|
+
function validateFactLine(line) {
|
|
526
|
+
let parsed;
|
|
527
|
+
try {
|
|
528
|
+
parsed = JSON.parse(line);
|
|
529
|
+
} catch {
|
|
530
|
+
return { valid: false, error: `Invalid JSON: ${line.substring(0, 80)}...` };
|
|
531
|
+
}
|
|
532
|
+
const envelope = parsed;
|
|
533
|
+
if (typeof envelope.factId !== "string" || !envelope.factId) {
|
|
534
|
+
return { valid: false, error: "Missing or invalid factId" };
|
|
535
|
+
}
|
|
536
|
+
if (typeof envelope.factType !== "string" || !envelope.factType) {
|
|
537
|
+
return { valid: false, error: "Missing or invalid factType" };
|
|
538
|
+
}
|
|
539
|
+
if (typeof envelope.language !== "string") {
|
|
540
|
+
return { valid: false, error: "Missing or invalid language" };
|
|
541
|
+
}
|
|
542
|
+
if (typeof envelope.scanner !== "string") {
|
|
543
|
+
return { valid: false, error: "Missing or invalid scanner" };
|
|
544
|
+
}
|
|
545
|
+
if (typeof envelope.scannerVersion !== "string") {
|
|
546
|
+
return { valid: false, error: "Missing or invalid scannerVersion" };
|
|
547
|
+
}
|
|
548
|
+
if (typeof envelope.emittedAt !== "string") {
|
|
549
|
+
return { valid: false, error: "Missing or invalid emittedAt" };
|
|
550
|
+
}
|
|
551
|
+
if (envelope.payload === void 0 || envelope.payload === null) {
|
|
552
|
+
return { valid: false, error: "Missing payload" };
|
|
553
|
+
}
|
|
554
|
+
return { valid: true, fact: envelope };
|
|
555
|
+
}
|
|
556
|
+
function resolveHcsWorkerBin() {
|
|
557
|
+
try {
|
|
558
|
+
const resolved = import.meta.resolve("@vibgrate/hcs-node-worker");
|
|
559
|
+
const resolvedPath = resolved.startsWith("file://") ? new URL(resolved).pathname : resolved;
|
|
560
|
+
if (existsSync(resolvedPath)) return resolvedPath;
|
|
561
|
+
} catch {
|
|
562
|
+
}
|
|
563
|
+
const base = import.meta.dirname ?? path6.dirname(new URL(import.meta.url).pathname);
|
|
564
|
+
const monorepoPath = path6.resolve(base, "..", "..", "..", "vibgrate-hcs", "node", "dist", "main.js");
|
|
565
|
+
if (existsSync(monorepoPath)) return monorepoPath;
|
|
566
|
+
const devPath = path6.resolve(base, "..", "..", "..", "vibgrate-hcs", "node", "src", "main.ts");
|
|
567
|
+
if (existsSync(devPath)) return devPath;
|
|
568
|
+
throw new Error(
|
|
569
|
+
'Cannot locate @vibgrate/hcs-node-worker. Run "pnpm build" in packages/vibgrate-hcs/node or install @vibgrate/hcs-node-worker.'
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
var NODE_WORKER_AST_LANGS = /* @__PURE__ */ new Set(["typescript", "javascript"]);
|
|
573
|
+
var NODE_WORKER_TEXT_LANGS = /* @__PURE__ */ new Set([
|
|
574
|
+
"swift",
|
|
575
|
+
"vb6",
|
|
576
|
+
"rust",
|
|
577
|
+
"ruby",
|
|
578
|
+
"php",
|
|
579
|
+
"dart",
|
|
580
|
+
"scala",
|
|
581
|
+
"cobol"
|
|
582
|
+
]);
|
|
583
|
+
var NODE_WORKER_ALL_LANGS = /* @__PURE__ */ new Set([
|
|
584
|
+
...NODE_WORKER_AST_LANGS,
|
|
585
|
+
...NODE_WORKER_TEXT_LANGS
|
|
586
|
+
]);
|
|
587
|
+
var EXTERNAL_WORKER_LANGS = /* @__PURE__ */ new Set(["go", "python", "java", "csharp"]);
|
|
588
|
+
async function runNodeWorker(rootDir, language, opts) {
|
|
589
|
+
const workerBin = resolveHcsWorkerBin();
|
|
590
|
+
const args = [];
|
|
591
|
+
if (NODE_WORKER_AST_LANGS.has(language)) {
|
|
592
|
+
args.push("--project", rootDir);
|
|
593
|
+
} else {
|
|
594
|
+
args.push("--project", rootDir);
|
|
595
|
+
const langFlag = `--${language}-project`;
|
|
596
|
+
args.push(langFlag, rootDir);
|
|
597
|
+
if (language === "cobol" && opts.copybookPaths) {
|
|
598
|
+
args.push("--cobol-copybook-paths", opts.copybookPaths);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return new Promise((resolve6) => {
|
|
602
|
+
const facts = [];
|
|
603
|
+
const errors = [];
|
|
604
|
+
let stdoutBuf = "";
|
|
605
|
+
let killed = false;
|
|
606
|
+
const child = spawn("node", ["--enable-source-maps", workerBin, ...args], {
|
|
607
|
+
cwd: rootDir,
|
|
608
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
609
|
+
env: { ...process.env, NODE_OPTIONS: "--max-old-space-size=4096" }
|
|
610
|
+
});
|
|
611
|
+
const timer = setTimeout(() => {
|
|
612
|
+
killed = true;
|
|
613
|
+
child.kill("SIGKILL");
|
|
614
|
+
}, opts.timeoutMs);
|
|
615
|
+
child.stdout.on("data", (chunk) => {
|
|
616
|
+
stdoutBuf += chunk.toString();
|
|
617
|
+
const lines = stdoutBuf.split("\n");
|
|
618
|
+
stdoutBuf = lines.pop();
|
|
619
|
+
for (const line of lines) {
|
|
620
|
+
const trimmed = line.trim();
|
|
621
|
+
if (trimmed) facts.push(trimmed);
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
child.stderr.on("data", (chunk) => {
|
|
625
|
+
const text = chunk.toString();
|
|
626
|
+
for (const line of text.split("\n")) {
|
|
627
|
+
const trimmed = line.trim();
|
|
628
|
+
if (!trimmed) continue;
|
|
629
|
+
if (trimmed.startsWith("[progress] ")) {
|
|
630
|
+
try {
|
|
631
|
+
const event = JSON.parse(trimmed.slice(11));
|
|
632
|
+
opts.onProgress?.(event);
|
|
633
|
+
} catch {
|
|
634
|
+
}
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
if (opts.verbose) {
|
|
638
|
+
process.stderr.write(chalk5.dim(`[${language}] ${trimmed}
|
|
639
|
+
`));
|
|
640
|
+
}
|
|
641
|
+
if (trimmed.startsWith("[error]")) {
|
|
642
|
+
errors.push(trimmed);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
child.on("close", (code) => {
|
|
647
|
+
clearTimeout(timer);
|
|
648
|
+
if (stdoutBuf.trim()) {
|
|
649
|
+
facts.push(stdoutBuf.trim());
|
|
650
|
+
}
|
|
651
|
+
if (killed) {
|
|
652
|
+
resolve6({ language, facts, errors: ["Worker killed: timeout exceeded"], exitCode: EXIT_TIMEOUT });
|
|
653
|
+
} else {
|
|
654
|
+
resolve6({ language, facts, errors, exitCode: code ?? 0 });
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
child.on("error", (err) => {
|
|
658
|
+
clearTimeout(timer);
|
|
659
|
+
errors.push(`Failed to spawn worker: ${err.message}`);
|
|
660
|
+
resolve6({ language, facts, errors, exitCode: EXIT_PARSE_FAILURE });
|
|
661
|
+
});
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
async function pushFacts(facts, dsn, verbose) {
|
|
665
|
+
const parsed = parseDsn(dsn);
|
|
666
|
+
if (!parsed) {
|
|
667
|
+
process.stderr.write(chalk5.red("Invalid DSN format.\n"));
|
|
668
|
+
process.stderr.write(chalk5.dim("Expected: vibgrate+https://<key_id>:<secret>@<host>/<workspace_id>\n"));
|
|
669
|
+
return false;
|
|
670
|
+
}
|
|
671
|
+
const body = facts.join("\n") + "\n";
|
|
672
|
+
const url = `${parsed.scheme}://${parsed.host}/v1/ingest/hcs`;
|
|
673
|
+
if (verbose) {
|
|
674
|
+
process.stderr.write(chalk5.dim(`Pushing ${facts.length} facts to ${parsed.host}...
|
|
675
|
+
`));
|
|
676
|
+
}
|
|
677
|
+
try {
|
|
678
|
+
const hmac = computeHmac(body, parsed.secret);
|
|
679
|
+
const response = await fetch(url, {
|
|
680
|
+
method: "POST",
|
|
681
|
+
headers: {
|
|
682
|
+
"Content-Type": "application/x-ndjson",
|
|
683
|
+
"X-Vibgrate-Key": parsed.keyId,
|
|
684
|
+
"X-Vibgrate-Workspace": parsed.workspaceId,
|
|
685
|
+
"X-Vibgrate-Signature": hmac,
|
|
686
|
+
"Connection": "close"
|
|
687
|
+
},
|
|
688
|
+
body
|
|
689
|
+
});
|
|
690
|
+
if (!response.ok) {
|
|
691
|
+
const text = await response.text().catch(() => "");
|
|
692
|
+
process.stderr.write(chalk5.red(`Push failed: HTTP ${response.status} ${text}
|
|
693
|
+
`));
|
|
694
|
+
return false;
|
|
695
|
+
}
|
|
696
|
+
if (verbose) {
|
|
697
|
+
process.stderr.write(chalk5.green(`\u2714 Pushed ${facts.length} facts successfully
|
|
698
|
+
`));
|
|
699
|
+
}
|
|
700
|
+
return true;
|
|
701
|
+
} catch (err) {
|
|
702
|
+
process.stderr.write(chalk5.red(`Push failed: ${err instanceof Error ? err.message : String(err)}
|
|
703
|
+
`));
|
|
704
|
+
return false;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
async function loadFeedback(filePath) {
|
|
708
|
+
const content = await fs2.readFile(filePath, "utf-8");
|
|
709
|
+
const lines = content.split("\n").filter((l) => l.trim());
|
|
710
|
+
for (const line of lines) {
|
|
711
|
+
try {
|
|
712
|
+
JSON.parse(line);
|
|
713
|
+
} catch {
|
|
714
|
+
throw new Error(`Invalid NDJSON in feedback file at: ${line.substring(0, 60)}...`);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return lines;
|
|
718
|
+
}
|
|
719
|
+
var ProgressTracker = class {
|
|
720
|
+
constructor(languages) {
|
|
721
|
+
this.languages = languages;
|
|
722
|
+
this.isTTY = process.stderr.isTTY ?? false;
|
|
723
|
+
for (const lang of languages) {
|
|
724
|
+
this.langStatus.set(lang, {
|
|
725
|
+
phase: "waiting",
|
|
726
|
+
fileIndex: 0,
|
|
727
|
+
fileCount: 0,
|
|
728
|
+
done: false,
|
|
729
|
+
factCount: 0
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
isTTY;
|
|
734
|
+
langStatus = /* @__PURE__ */ new Map();
|
|
735
|
+
totalFacts = 0;
|
|
736
|
+
lastRender = 0;
|
|
737
|
+
throttleMs = 80;
|
|
738
|
+
// Cap redraws to ~12 fps
|
|
739
|
+
lastLineLen = 0;
|
|
740
|
+
/** Called by worker spawner on each [progress] event */
|
|
741
|
+
onProgress(language, event) {
|
|
742
|
+
const status = this.langStatus.get(language);
|
|
743
|
+
if (!status) return;
|
|
744
|
+
status.phase = event.phase;
|
|
745
|
+
if (event.fileCount !== void 0) status.fileCount = event.fileCount;
|
|
746
|
+
if (event.fileIndex !== void 0) status.fileIndex = event.fileIndex;
|
|
747
|
+
if (event.file) status.file = event.file;
|
|
748
|
+
if (event.phase === "done") status.done = true;
|
|
749
|
+
this.render();
|
|
750
|
+
}
|
|
751
|
+
/** Called when a new fact line is validated */
|
|
752
|
+
onFact() {
|
|
753
|
+
this.totalFacts++;
|
|
754
|
+
this.render();
|
|
755
|
+
}
|
|
756
|
+
/** Called per-language when facts are counted */
|
|
757
|
+
setLanguageFactCount(language, count) {
|
|
758
|
+
const status = this.langStatus.get(language);
|
|
759
|
+
if (status) status.factCount = count;
|
|
760
|
+
}
|
|
761
|
+
render() {
|
|
762
|
+
const now = Date.now();
|
|
763
|
+
if (now - this.lastRender < this.throttleMs) return;
|
|
764
|
+
this.lastRender = now;
|
|
765
|
+
if (!this.isTTY) return;
|
|
766
|
+
const parts = [];
|
|
767
|
+
for (const [lang, st] of this.langStatus) {
|
|
768
|
+
if (st.done) {
|
|
769
|
+
parts.push(chalk5.green(`${lang} \u2713`));
|
|
770
|
+
} else if (st.phase === "waiting") {
|
|
771
|
+
parts.push(chalk5.dim(`${lang} \xB7`));
|
|
772
|
+
} else if (st.phase === "discovering") {
|
|
773
|
+
parts.push(chalk5.yellow(`${lang} \u2026`));
|
|
774
|
+
} else if (st.phase === "scanning" && st.fileCount > 0) {
|
|
775
|
+
const pct = Math.round(st.fileIndex / st.fileCount * 100);
|
|
776
|
+
parts.push(chalk5.cyan(`${lang} ${st.fileIndex}/${st.fileCount} ${pct}%`));
|
|
777
|
+
} else if (st.phase === "extracting") {
|
|
778
|
+
parts.push(chalk5.cyan(`${lang} extracting`));
|
|
779
|
+
} else {
|
|
780
|
+
parts.push(chalk5.dim(`${lang} ${st.phase}`));
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
const line = ` ${parts.join(" ")} ${chalk5.dim(`${this.totalFacts} facts`)}`;
|
|
784
|
+
const padding = Math.max(0, this.lastLineLen - line.length);
|
|
785
|
+
process.stderr.write(`\r${line}${" ".repeat(padding)}`);
|
|
786
|
+
this.lastLineLen = line.length;
|
|
787
|
+
}
|
|
788
|
+
/** Clear the progress line and print final summary */
|
|
789
|
+
finish(elapsed) {
|
|
790
|
+
if (this.isTTY) {
|
|
791
|
+
process.stderr.write(`\r${" ".repeat(this.lastLineLen)}\r`);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
var extractCommand = new Command5("extract").description("Analyze source code and emit validated HCS facts (NDJSON)").argument("[path]", "Path to source directory", ".").option("-o, --out <file>", "Write NDJSON to file (default: stdout)").option("--language <langs>", "Comma-separated languages to analyze (default: auto-detect)").option("--include-tests", "Include test files in analysis").option("--push", "Stream validated facts to dashboard API").option("--dsn <dsn>", "DSN token for push (or use VIBGRATE_DSN env)").option("--concurrency <n>", "Number of parallel file workers").option("--timeout-mins <mins>", "Total analysis timeout in minutes", "60").option("--feedback <file>", "Load NDJSON diff artifact for refinement").option("--verbose", "Print worker stderr and summary statistics").action(async (targetPath, opts) => {
|
|
796
|
+
const rootDir = path6.resolve(targetPath);
|
|
797
|
+
if (!await pathExists(rootDir)) {
|
|
798
|
+
process.stderr.write(chalk5.red(`Path does not exist: ${rootDir}
|
|
799
|
+
`));
|
|
800
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
801
|
+
}
|
|
802
|
+
let stat3;
|
|
803
|
+
try {
|
|
804
|
+
stat3 = await fs2.stat(rootDir);
|
|
805
|
+
} catch {
|
|
806
|
+
process.stderr.write(chalk5.red(`Cannot read path: ${rootDir}
|
|
807
|
+
`));
|
|
808
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
809
|
+
}
|
|
810
|
+
if (!stat3.isDirectory()) {
|
|
811
|
+
process.stderr.write(chalk5.red(`Path must be a directory: ${rootDir}
|
|
812
|
+
`));
|
|
813
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
814
|
+
}
|
|
815
|
+
const dsn = opts.dsn || process.env.VIBGRATE_DSN;
|
|
816
|
+
if (opts.push && !dsn) {
|
|
817
|
+
process.stderr.write(chalk5.red("--push requires --dsn or VIBGRATE_DSN environment variable\n"));
|
|
818
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
819
|
+
}
|
|
820
|
+
if (dsn && !parseDsn(dsn)) {
|
|
821
|
+
process.stderr.write(chalk5.red("Invalid DSN format.\n"));
|
|
822
|
+
process.stderr.write(chalk5.dim("Expected: vibgrate+https://<key_id>:<secret>@<host>/<workspace_id>\n"));
|
|
823
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
824
|
+
}
|
|
825
|
+
const concurrency = opts.concurrency ? parseInt(opts.concurrency, 10) : os2.cpus().length;
|
|
826
|
+
if (isNaN(concurrency) || concurrency < 1) {
|
|
827
|
+
process.stderr.write(chalk5.red("--concurrency must be >= 1\n"));
|
|
828
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
829
|
+
}
|
|
830
|
+
const timeoutMins = parseInt(opts.timeoutMins, 10);
|
|
831
|
+
if (isNaN(timeoutMins) || timeoutMins < 1) {
|
|
832
|
+
process.stderr.write(chalk5.red("--timeout-mins must be >= 1\n"));
|
|
833
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
834
|
+
}
|
|
835
|
+
const timeoutMs = timeoutMins * 60 * 1e3;
|
|
836
|
+
if (opts.out) {
|
|
837
|
+
const outDir = path6.dirname(path6.resolve(opts.out));
|
|
838
|
+
if (!await pathExists(outDir)) {
|
|
839
|
+
process.stderr.write(chalk5.red(`Output directory does not exist: ${outDir}
|
|
840
|
+
`));
|
|
841
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
842
|
+
}
|
|
843
|
+
try {
|
|
844
|
+
const outStat = await fs2.stat(path6.resolve(opts.out));
|
|
845
|
+
if (outStat.isDirectory()) {
|
|
846
|
+
process.stderr.write(chalk5.red(`--out cannot be a directory: ${opts.out}
|
|
847
|
+
`));
|
|
848
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
849
|
+
}
|
|
850
|
+
} catch {
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
let feedbackLines;
|
|
854
|
+
if (opts.feedback) {
|
|
855
|
+
const feedbackPath = path6.resolve(opts.feedback);
|
|
856
|
+
if (!await pathExists(feedbackPath)) {
|
|
857
|
+
process.stderr.write(chalk5.red(`Feedback file not found: ${feedbackPath}
|
|
858
|
+
`));
|
|
859
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
860
|
+
}
|
|
861
|
+
try {
|
|
862
|
+
feedbackLines = await loadFeedback(feedbackPath);
|
|
863
|
+
} catch (err) {
|
|
864
|
+
process.stderr.write(chalk5.red(`Invalid feedback file: ${err instanceof Error ? err.message : String(err)}
|
|
865
|
+
`));
|
|
866
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
let targetLanguages;
|
|
870
|
+
if (opts.language) {
|
|
871
|
+
targetLanguages = opts.language.split(",").map((l) => l.trim().toLowerCase()).filter(Boolean);
|
|
872
|
+
for (const lang of targetLanguages) {
|
|
873
|
+
if (!SUPPORTED_LANGUAGES.has(lang)) {
|
|
874
|
+
process.stderr.write(chalk5.red(`Unknown language: "${lang}"
|
|
875
|
+
`));
|
|
876
|
+
process.stderr.write(chalk5.dim(`Supported: ${[...SUPPORTED_LANGUAGES].sort().join(", ")}
|
|
877
|
+
`));
|
|
878
|
+
process.exit(EXIT_USAGE_ERROR);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
} else {
|
|
882
|
+
const detected = await detectLanguages(rootDir, opts.includeTests ?? false);
|
|
883
|
+
if (detected.length === 0) {
|
|
884
|
+
process.stderr.write(chalk5.yellow("No supported source files detected.\n"));
|
|
885
|
+
process.exit(EXIT_SUCCESS);
|
|
886
|
+
}
|
|
887
|
+
targetLanguages = detected.map((d) => d.language);
|
|
888
|
+
if (opts.verbose) {
|
|
889
|
+
process.stderr.write(chalk5.dim("Detected languages:\n"));
|
|
890
|
+
for (const d of detected) {
|
|
891
|
+
process.stderr.write(chalk5.dim(` ${d.language}: ${d.fileCount} files
|
|
892
|
+
`));
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
const runnableLanguages = targetLanguages.filter((l) => NODE_WORKER_ALL_LANGS.has(l));
|
|
897
|
+
const skippedLanguages = targetLanguages.filter((l) => EXTERNAL_WORKER_LANGS.has(l));
|
|
898
|
+
const unknownWorkerLangs = targetLanguages.filter(
|
|
899
|
+
(l) => !NODE_WORKER_ALL_LANGS.has(l) && !EXTERNAL_WORKER_LANGS.has(l)
|
|
900
|
+
);
|
|
901
|
+
if (skippedLanguages.length > 0 && opts.verbose) {
|
|
902
|
+
process.stderr.write(chalk5.yellow(
|
|
903
|
+
`Skipping languages without built-in HCS worker: ${skippedLanguages.join(", ")}
|
|
904
|
+
` + chalk5.dim("These require dedicated external workers.\n")
|
|
905
|
+
));
|
|
906
|
+
}
|
|
907
|
+
if (runnableLanguages.length === 0) {
|
|
908
|
+
process.stderr.write(chalk5.yellow("No languages with available HCS workers found.\n"));
|
|
909
|
+
if (skippedLanguages.length > 0) {
|
|
910
|
+
process.stderr.write(chalk5.dim(
|
|
911
|
+
`Detected: ${skippedLanguages.join(", ")} \u2014 these require external workers not yet integrated.
|
|
912
|
+
`
|
|
913
|
+
));
|
|
914
|
+
}
|
|
915
|
+
process.exit(EXIT_SUCCESS);
|
|
916
|
+
}
|
|
917
|
+
const startTime = Date.now();
|
|
918
|
+
const globalDeadline = startTime + timeoutMs;
|
|
919
|
+
process.stderr.write(
|
|
920
|
+
chalk5.bold(`Extracting HCS facts from ${rootDir}
|
|
921
|
+
`) + chalk5.dim(`Languages: ${runnableLanguages.join(", ")} Concurrency: ${concurrency} Timeout: ${timeoutMins}m
|
|
922
|
+
`)
|
|
923
|
+
);
|
|
924
|
+
const progress = new ProgressTracker(runnableLanguages);
|
|
925
|
+
const sem = new Semaphore(concurrency);
|
|
926
|
+
const allFacts = [];
|
|
927
|
+
const allErrors = [];
|
|
928
|
+
let hasSchemaFailure = false;
|
|
929
|
+
let hasParseFailure = false;
|
|
930
|
+
let hasTimeout = false;
|
|
931
|
+
const workerPromises = runnableLanguages.map(
|
|
932
|
+
(language) => sem.run(async () => {
|
|
933
|
+
const remaining = globalDeadline - Date.now();
|
|
934
|
+
if (remaining <= 0) {
|
|
935
|
+
hasTimeout = true;
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
const result = await runNodeWorker(rootDir, language, {
|
|
939
|
+
includeTests: opts.includeTests ?? false,
|
|
940
|
+
verbose: opts.verbose ?? false,
|
|
941
|
+
timeoutMs: remaining,
|
|
942
|
+
onProgress: (event) => progress.onProgress(language, event)
|
|
943
|
+
});
|
|
944
|
+
if (result.exitCode === EXIT_TIMEOUT) {
|
|
945
|
+
hasTimeout = true;
|
|
946
|
+
}
|
|
947
|
+
let langFactCount = 0;
|
|
948
|
+
for (const line of result.facts) {
|
|
949
|
+
const validation = validateFactLine(line);
|
|
950
|
+
if (validation.valid) {
|
|
951
|
+
allFacts.push(line);
|
|
952
|
+
langFactCount++;
|
|
953
|
+
progress.onFact();
|
|
954
|
+
} else {
|
|
955
|
+
hasSchemaFailure = true;
|
|
956
|
+
allErrors.push(`[${language}] Schema validation: ${validation.error}`);
|
|
957
|
+
if (opts.verbose) {
|
|
958
|
+
process.stderr.write(chalk5.red(`[${language}] Invalid fact: ${validation.error}
|
|
959
|
+
`));
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
progress.setLanguageFactCount(language, langFactCount);
|
|
964
|
+
if (result.errors.length > 0) {
|
|
965
|
+
allErrors.push(...result.errors.map((e) => `[${language}] ${e}`));
|
|
966
|
+
}
|
|
967
|
+
if (result.exitCode !== 0 && result.exitCode !== EXIT_TIMEOUT) {
|
|
968
|
+
hasParseFailure = true;
|
|
969
|
+
}
|
|
970
|
+
})
|
|
971
|
+
);
|
|
972
|
+
await Promise.all(workerPromises);
|
|
973
|
+
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
974
|
+
progress.finish(elapsed);
|
|
975
|
+
allFacts.sort();
|
|
976
|
+
const ndjsonOutput = allFacts.map((f) => f).join("\n") + (allFacts.length > 0 ? "\n" : "");
|
|
977
|
+
if (opts.out) {
|
|
978
|
+
const outPath = path6.resolve(opts.out);
|
|
979
|
+
await fs2.writeFile(outPath, ndjsonOutput, "utf-8");
|
|
980
|
+
process.stderr.write(chalk5.green(`\u2714 Wrote ${allFacts.length} facts to ${outPath}
|
|
981
|
+
`));
|
|
982
|
+
} else {
|
|
983
|
+
process.stdout.write(ndjsonOutput);
|
|
984
|
+
}
|
|
985
|
+
if (opts.push && dsn && allFacts.length > 0) {
|
|
986
|
+
const pushOk = await pushFacts(allFacts, dsn, opts.verbose ?? false);
|
|
987
|
+
if (!pushOk) {
|
|
988
|
+
process.exit(EXIT_PUSH_FAILURE);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
if (opts.verbose) {
|
|
992
|
+
process.stderr.write(chalk5.dim("\n\u2500\u2500 Summary \u2500\u2500\n"));
|
|
993
|
+
process.stderr.write(chalk5.dim(` Facts emitted : ${allFacts.length}
|
|
994
|
+
`));
|
|
995
|
+
process.stderr.write(chalk5.dim(` Languages : ${runnableLanguages.join(", ")}
|
|
996
|
+
`));
|
|
997
|
+
process.stderr.write(chalk5.dim(` Elapsed : ${elapsed}s
|
|
998
|
+
`));
|
|
999
|
+
if (allErrors.length > 0) {
|
|
1000
|
+
process.stderr.write(chalk5.dim(` Errors : ${allErrors.length}
|
|
1001
|
+
`));
|
|
1002
|
+
for (const err of allErrors.slice(0, 10)) {
|
|
1003
|
+
process.stderr.write(chalk5.dim(` ${err}
|
|
1004
|
+
`));
|
|
1005
|
+
}
|
|
1006
|
+
if (allErrors.length > 10) {
|
|
1007
|
+
process.stderr.write(chalk5.dim(` ... and ${allErrors.length - 10} more
|
|
1008
|
+
`));
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
process.stderr.write(chalk5.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n"));
|
|
1012
|
+
}
|
|
1013
|
+
if (hasTimeout) {
|
|
1014
|
+
process.stderr.write(chalk5.red(`Timeout exceeded (${timeoutMins} minutes)
|
|
1015
|
+
`));
|
|
1016
|
+
process.exit(EXIT_TIMEOUT);
|
|
1017
|
+
}
|
|
1018
|
+
if (hasSchemaFailure) {
|
|
1019
|
+
process.stderr.write(chalk5.red("Fact schema validation failures detected\n"));
|
|
1020
|
+
process.exit(EXIT_SCHEMA_FAILURE);
|
|
1021
|
+
}
|
|
1022
|
+
if (hasParseFailure) {
|
|
1023
|
+
process.stderr.write(chalk5.red("Parsing failures detected\n"));
|
|
1024
|
+
process.exit(EXIT_PARSE_FAILURE);
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
// src/commands/diff.ts
|
|
1029
|
+
import * as path7 from "path";
|
|
1030
|
+
import * as fs3 from "fs/promises";
|
|
1031
|
+
import * as crypto from "crypto";
|
|
1032
|
+
import { Command as Command6 } from "commander";
|
|
1033
|
+
import chalk6 from "chalk";
|
|
1034
|
+
var EXIT_INVALID_PATH = 2;
|
|
1035
|
+
var EXIT_USAGE_ERROR2 = 5;
|
|
1036
|
+
var VALID_FORMATS = /* @__PURE__ */ new Set(["ndjson", "json", "patch"]);
|
|
1037
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
1038
|
+
".git",
|
|
1039
|
+
".hg",
|
|
1040
|
+
".svn",
|
|
1041
|
+
"node_modules",
|
|
1042
|
+
".vibgrate"
|
|
1043
|
+
]);
|
|
1044
|
+
async function discoverFiles(root, includeGlobs, excludeGlobs) {
|
|
1045
|
+
const result = [];
|
|
1046
|
+
async function walk(dir, relBase) {
|
|
1047
|
+
let entries;
|
|
1048
|
+
try {
|
|
1049
|
+
entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
1050
|
+
} catch {
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
for (const entry of entries) {
|
|
1054
|
+
if (entry.name.startsWith(".") && SKIP_DIRS2.has(entry.name)) continue;
|
|
1055
|
+
const absPath = path7.join(dir, entry.name);
|
|
1056
|
+
const relPath = path7.join(relBase, entry.name);
|
|
1057
|
+
if (entry.isDirectory()) {
|
|
1058
|
+
if (!SKIP_DIRS2.has(entry.name)) {
|
|
1059
|
+
await walk(absPath, relPath);
|
|
1060
|
+
}
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
1063
|
+
if (!entry.isFile()) continue;
|
|
1064
|
+
if (includeGlobs && includeGlobs.length > 0) {
|
|
1065
|
+
if (!matchesAnyGlob(relPath, includeGlobs)) continue;
|
|
1066
|
+
}
|
|
1067
|
+
if (excludeGlobs && excludeGlobs.length > 0) {
|
|
1068
|
+
if (matchesAnyGlob(relPath, excludeGlobs)) continue;
|
|
1069
|
+
}
|
|
1070
|
+
result.push({ relPath, absPath });
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
await walk(root, "");
|
|
1074
|
+
return result;
|
|
1075
|
+
}
|
|
1076
|
+
function matchesAnyGlob(filePath, globs) {
|
|
1077
|
+
for (const glob of globs) {
|
|
1078
|
+
if (matchGlob(filePath, glob)) return true;
|
|
1079
|
+
}
|
|
1080
|
+
return false;
|
|
1081
|
+
}
|
|
1082
|
+
function matchGlob(filePath, glob) {
|
|
1083
|
+
const pattern = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
1084
|
+
return new RegExp(pattern, "i").test(filePath);
|
|
1085
|
+
}
|
|
1086
|
+
function computeLineDiff(originalLines, generatedLines, contextLines, ignoreWhitespace) {
|
|
1087
|
+
const aLines = ignoreWhitespace ? originalLines.map((l) => l.trim()) : originalLines;
|
|
1088
|
+
const bLines = ignoreWhitespace ? generatedLines.map((l) => l.trim()) : generatedLines;
|
|
1089
|
+
const changes = computeEditScript(aLines, bLines);
|
|
1090
|
+
let insertions = 0;
|
|
1091
|
+
let deletions = 0;
|
|
1092
|
+
for (const change of changes) {
|
|
1093
|
+
if (change.type === "insert") insertions++;
|
|
1094
|
+
if (change.type === "delete") deletions++;
|
|
1095
|
+
}
|
|
1096
|
+
const hunks = buildHunks(originalLines, generatedLines, changes, contextLines);
|
|
1097
|
+
return { hunks, insertions, deletions };
|
|
1098
|
+
}
|
|
1099
|
+
function computeEditScript(a, b) {
|
|
1100
|
+
const n = a.length;
|
|
1101
|
+
const m = b.length;
|
|
1102
|
+
if (n === 0 && m === 0) return [];
|
|
1103
|
+
if (n === 0) return b.map((_, i2) => ({ type: "insert", aIndex: 0, bIndex: i2 }));
|
|
1104
|
+
if (m === 0) return a.map((_, i2) => ({ type: "delete", aIndex: i2, bIndex: 0 }));
|
|
1105
|
+
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
1106
|
+
for (let i2 = 1; i2 <= n; i2++) {
|
|
1107
|
+
for (let j2 = 1; j2 <= m; j2++) {
|
|
1108
|
+
if (a[i2 - 1] === b[j2 - 1]) {
|
|
1109
|
+
dp[i2][j2] = dp[i2 - 1][j2 - 1] + 1;
|
|
1110
|
+
} else {
|
|
1111
|
+
dp[i2][j2] = Math.max(dp[i2 - 1][j2], dp[i2][j2 - 1]);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
const changes = [];
|
|
1116
|
+
let i = n, j = m;
|
|
1117
|
+
while (i > 0 || j > 0) {
|
|
1118
|
+
if (i > 0 && j > 0 && a[i - 1] === b[j - 1]) {
|
|
1119
|
+
changes.unshift({ type: "equal", aIndex: i - 1, bIndex: j - 1 });
|
|
1120
|
+
i--;
|
|
1121
|
+
j--;
|
|
1122
|
+
} else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
|
|
1123
|
+
changes.unshift({ type: "insert", aIndex: i, bIndex: j - 1 });
|
|
1124
|
+
j--;
|
|
1125
|
+
} else {
|
|
1126
|
+
changes.unshift({ type: "delete", aIndex: i - 1, bIndex: j });
|
|
1127
|
+
i--;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
return changes;
|
|
1131
|
+
}
|
|
1132
|
+
function buildHunks(aLines, bLines, changes, contextLines) {
|
|
1133
|
+
const hunks = [];
|
|
1134
|
+
let currentHunk = null;
|
|
1135
|
+
let lastChangeEnd = -1;
|
|
1136
|
+
for (let ci = 0; ci < changes.length; ci++) {
|
|
1137
|
+
const change = changes[ci];
|
|
1138
|
+
if (change.type === "equal") {
|
|
1139
|
+
if (currentHunk && ci - lastChangeEnd > contextLines * 2) {
|
|
1140
|
+
for (let k = lastChangeEnd + 1; k <= Math.min(lastChangeEnd + contextLines, ci); k++) {
|
|
1141
|
+
const c = changes[k];
|
|
1142
|
+
if (c && c.type === "equal") {
|
|
1143
|
+
currentHunk.lines.push(` ${aLines[c.aIndex] ?? ""}`);
|
|
1144
|
+
currentHunk.originalCount++;
|
|
1145
|
+
currentHunk.generatedCount++;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
hunks.push(currentHunk);
|
|
1149
|
+
currentHunk = null;
|
|
1150
|
+
}
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1153
|
+
if (!currentHunk) {
|
|
1154
|
+
const contextStart = Math.max(0, ci - contextLines);
|
|
1155
|
+
const aStart = changes[contextStart]?.aIndex ?? 0;
|
|
1156
|
+
const bStart = changes[contextStart]?.bIndex ?? 0;
|
|
1157
|
+
currentHunk = {
|
|
1158
|
+
originalStart: aStart + 1,
|
|
1159
|
+
originalCount: 0,
|
|
1160
|
+
generatedStart: bStart + 1,
|
|
1161
|
+
generatedCount: 0,
|
|
1162
|
+
lines: []
|
|
1163
|
+
};
|
|
1164
|
+
for (let k = contextStart; k < ci; k++) {
|
|
1165
|
+
const c = changes[k];
|
|
1166
|
+
if (c && c.type === "equal") {
|
|
1167
|
+
currentHunk.lines.push(` ${aLines[c.aIndex] ?? ""}`);
|
|
1168
|
+
currentHunk.originalCount++;
|
|
1169
|
+
currentHunk.generatedCount++;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
if (change.type === "delete") {
|
|
1174
|
+
currentHunk.lines.push(`-${aLines[change.aIndex] ?? ""}`);
|
|
1175
|
+
currentHunk.originalCount++;
|
|
1176
|
+
} else if (change.type === "insert") {
|
|
1177
|
+
currentHunk.lines.push(`+${bLines[change.bIndex] ?? ""}`);
|
|
1178
|
+
currentHunk.generatedCount++;
|
|
1179
|
+
}
|
|
1180
|
+
lastChangeEnd = ci;
|
|
1181
|
+
}
|
|
1182
|
+
if (currentHunk) {
|
|
1183
|
+
for (let k = lastChangeEnd + 1; k < Math.min(changes.length, lastChangeEnd + 1 + contextLines); k++) {
|
|
1184
|
+
const c = changes[k];
|
|
1185
|
+
if (c && c.type === "equal") {
|
|
1186
|
+
currentHunk.lines.push(` ${aLines[c.aIndex] ?? ""}`);
|
|
1187
|
+
currentHunk.originalCount++;
|
|
1188
|
+
currentHunk.generatedCount++;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
hunks.push(currentHunk);
|
|
1192
|
+
}
|
|
1193
|
+
return hunks;
|
|
1194
|
+
}
|
|
1195
|
+
function computeSimilarity(a, b) {
|
|
1196
|
+
if (a === b) return 1;
|
|
1197
|
+
if (a.length === 0 && b.length === 0) return 1;
|
|
1198
|
+
if (a.length === 0 || b.length === 0) return 0;
|
|
1199
|
+
const aLines = a.split("\n");
|
|
1200
|
+
const bLines = b.split("\n");
|
|
1201
|
+
const n = aLines.length;
|
|
1202
|
+
const m = bLines.length;
|
|
1203
|
+
if (n * m > 1e6) {
|
|
1204
|
+
const sampleA = [...aLines.slice(0, 100), ...aLines.slice(-100)];
|
|
1205
|
+
const sampleB = [...bLines.slice(0, 100), ...bLines.slice(-100)];
|
|
1206
|
+
return lcsRatio(sampleA, sampleB);
|
|
1207
|
+
}
|
|
1208
|
+
return lcsRatio(aLines, bLines);
|
|
1209
|
+
}
|
|
1210
|
+
function lcsRatio(a, b) {
|
|
1211
|
+
const n = a.length;
|
|
1212
|
+
const m = b.length;
|
|
1213
|
+
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
1214
|
+
for (let i = 1; i <= n; i++) {
|
|
1215
|
+
for (let j = 1; j <= m; j++) {
|
|
1216
|
+
if (a[i - 1] === b[j - 1]) {
|
|
1217
|
+
dp[i][j] = dp[i - 1][j - 1] + 1;
|
|
1218
|
+
} else {
|
|
1219
|
+
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
const lcsLen = dp[n][m];
|
|
1224
|
+
return 2 * lcsLen / (n + m);
|
|
1225
|
+
}
|
|
1226
|
+
function generateDeltaId(kind, filePath) {
|
|
1227
|
+
const hash = crypto.createHash("sha256").update(`${kind}:${filePath}`).digest("hex").substring(0, 16);
|
|
1228
|
+
return `hcs:delta:${hash}`;
|
|
1229
|
+
}
|
|
1230
|
+
function formatNdjson(deltas) {
|
|
1231
|
+
return deltas.map((d) => JSON.stringify(d)).join("\n") + (deltas.length > 0 ? "\n" : "");
|
|
1232
|
+
}
|
|
1233
|
+
function formatJson(deltas) {
|
|
1234
|
+
return JSON.stringify({ deltas, count: deltas.length }, null, 2) + "\n";
|
|
1235
|
+
}
|
|
1236
|
+
function formatPatch(deltas, originalPath, generatedPath) {
|
|
1237
|
+
const lines = [];
|
|
1238
|
+
for (const delta of deltas) {
|
|
1239
|
+
const aFile = delta.originalPath ? path7.join("a", delta.originalPath) : "/dev/null";
|
|
1240
|
+
const bFile = delta.generatedPath ? path7.join("b", delta.generatedPath) : "/dev/null";
|
|
1241
|
+
if (delta.changeKind === "renamed" && delta.originalPath && delta.generatedPath) {
|
|
1242
|
+
lines.push(`diff --vibgrate ${aFile} ${bFile}`);
|
|
1243
|
+
lines.push(`similarity index ${Math.round((delta.similarity ?? 0) * 100)}%`);
|
|
1244
|
+
lines.push(`rename from ${delta.originalPath}`);
|
|
1245
|
+
lines.push(`rename to ${delta.generatedPath}`);
|
|
1246
|
+
} else {
|
|
1247
|
+
lines.push(`diff --vibgrate ${aFile} ${bFile}`);
|
|
1248
|
+
}
|
|
1249
|
+
lines.push(`--- ${delta.changeKind === "added" ? "/dev/null" : aFile}`);
|
|
1250
|
+
lines.push(`+++ ${delta.changeKind === "removed" ? "/dev/null" : bFile}`);
|
|
1251
|
+
if (delta.hunks) {
|
|
1252
|
+
for (const hunk of delta.hunks) {
|
|
1253
|
+
lines.push(`@@ -${hunk.originalStart},${hunk.originalCount} +${hunk.generatedStart},${hunk.generatedCount} @@`);
|
|
1254
|
+
lines.push(...hunk.lines);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
lines.push("");
|
|
1258
|
+
}
|
|
1259
|
+
return lines.join("\n");
|
|
1260
|
+
}
|
|
1261
|
+
var diffCommand = new Command6("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) => {
|
|
1262
|
+
const origDir = path7.resolve(originalPath);
|
|
1263
|
+
const genDir = path7.resolve(generatedPath);
|
|
1264
|
+
for (const [label, dir] of [["original_path", origDir], ["generated_path", genDir]]) {
|
|
1265
|
+
if (!await pathExists(dir)) {
|
|
1266
|
+
process.stderr.write(chalk6.red(`${label} does not exist: ${dir}
|
|
1267
|
+
`));
|
|
1268
|
+
process.exit(EXIT_INVALID_PATH);
|
|
1269
|
+
}
|
|
1270
|
+
let stat3;
|
|
1271
|
+
try {
|
|
1272
|
+
stat3 = await fs3.stat(dir);
|
|
1273
|
+
} catch {
|
|
1274
|
+
process.stderr.write(chalk6.red(`Cannot read ${label}: ${dir}
|
|
1275
|
+
`));
|
|
1276
|
+
process.exit(EXIT_INVALID_PATH);
|
|
1277
|
+
}
|
|
1278
|
+
if (!stat3.isDirectory()) {
|
|
1279
|
+
process.stderr.write(chalk6.red(`${label} must be a directory: ${dir}
|
|
1280
|
+
`));
|
|
1281
|
+
process.exit(EXIT_INVALID_PATH);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
const format = opts.format.toLowerCase();
|
|
1285
|
+
if (!VALID_FORMATS.has(format)) {
|
|
1286
|
+
process.stderr.write(chalk6.red(`Invalid format: "${opts.format}". Allowed: ndjson, json, patch
|
|
1287
|
+
`));
|
|
1288
|
+
process.exit(EXIT_USAGE_ERROR2);
|
|
1289
|
+
}
|
|
1290
|
+
const contextLines = parseInt(opts.context, 10);
|
|
1291
|
+
if (isNaN(contextLines) || contextLines < 0) {
|
|
1292
|
+
process.stderr.write(chalk6.red("--context must be >= 0\n"));
|
|
1293
|
+
process.exit(EXIT_USAGE_ERROR2);
|
|
1294
|
+
}
|
|
1295
|
+
const includeGlobs = opts.include ? opts.include.split(",").map((g) => g.trim()).filter(Boolean) : void 0;
|
|
1296
|
+
const excludeGlobs = opts.exclude ? opts.exclude.split(",").map((g) => g.trim()).filter(Boolean) : void 0;
|
|
1297
|
+
const [origFiles, genFiles] = await Promise.all([
|
|
1298
|
+
discoverFiles(origDir, includeGlobs, excludeGlobs),
|
|
1299
|
+
discoverFiles(genDir, includeGlobs, excludeGlobs)
|
|
1300
|
+
]);
|
|
1301
|
+
const origMap = new Map(origFiles.map((f) => [f.relPath, f]));
|
|
1302
|
+
const genMap = new Map(genFiles.map((f) => [f.relPath, f]));
|
|
1303
|
+
if (opts.verbose) {
|
|
1304
|
+
process.stderr.write(chalk6.dim(`Original: ${origFiles.length} files
|
|
1305
|
+
`));
|
|
1306
|
+
process.stderr.write(chalk6.dim(`Generated: ${genFiles.length} files
|
|
1307
|
+
`));
|
|
1308
|
+
}
|
|
1309
|
+
const deltas = [];
|
|
1310
|
+
const matchedOrig = /* @__PURE__ */ new Set();
|
|
1311
|
+
const matchedGen = /* @__PURE__ */ new Set();
|
|
1312
|
+
for (const [relPath, origFile] of origMap) {
|
|
1313
|
+
const genFile = genMap.get(relPath);
|
|
1314
|
+
if (genFile) {
|
|
1315
|
+
matchedOrig.add(relPath);
|
|
1316
|
+
matchedGen.add(relPath);
|
|
1317
|
+
const origContent = await fs3.readFile(origFile.absPath, "utf-8");
|
|
1318
|
+
const genContent = await fs3.readFile(genFile.absPath, "utf-8");
|
|
1319
|
+
const origCompare = opts.ignoreWhitespace ? origContent.replace(/\s+/g, " ").trim() : origContent;
|
|
1320
|
+
const genCompare = opts.ignoreWhitespace ? genContent.replace(/\s+/g, " ").trim() : genContent;
|
|
1321
|
+
if (origCompare !== genCompare) {
|
|
1322
|
+
const diffResult = computeLineDiff(
|
|
1323
|
+
origContent.split("\n"),
|
|
1324
|
+
genContent.split("\n"),
|
|
1325
|
+
contextLines,
|
|
1326
|
+
opts.ignoreWhitespace ?? false
|
|
1327
|
+
);
|
|
1328
|
+
const delta = {
|
|
1329
|
+
deltaId: generateDeltaId("modified", relPath),
|
|
1330
|
+
changeKind: "modified",
|
|
1331
|
+
originalPath: relPath,
|
|
1332
|
+
generatedPath: relPath,
|
|
1333
|
+
hunks: diffResult.hunks
|
|
1334
|
+
};
|
|
1335
|
+
if (opts.stats) {
|
|
1336
|
+
delta.stats = { insertions: diffResult.insertions, deletions: diffResult.deletions };
|
|
1337
|
+
}
|
|
1338
|
+
deltas.push(delta);
|
|
1339
|
+
if (opts.verbose) {
|
|
1340
|
+
process.stderr.write(chalk6.dim(` modified: ${relPath} (+${diffResult.insertions} -${diffResult.deletions})
|
|
1341
|
+
`));
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
const unmatchedOrig = origFiles.filter((f) => !matchedOrig.has(f.relPath));
|
|
1347
|
+
const unmatchedGen = genFiles.filter((f) => !matchedGen.has(f.relPath));
|
|
1348
|
+
const RENAME_THRESHOLD = 0.8;
|
|
1349
|
+
const renamedOrig = /* @__PURE__ */ new Set();
|
|
1350
|
+
const renamedGen = /* @__PURE__ */ new Set();
|
|
1351
|
+
if (unmatchedOrig.length > 0 && unmatchedGen.length > 0) {
|
|
1352
|
+
const origContents = /* @__PURE__ */ new Map();
|
|
1353
|
+
const genContents = /* @__PURE__ */ new Map();
|
|
1354
|
+
await Promise.all([
|
|
1355
|
+
...unmatchedOrig.map(async (f) => {
|
|
1356
|
+
try {
|
|
1357
|
+
origContents.set(f.relPath, await fs3.readFile(f.absPath, "utf-8"));
|
|
1358
|
+
} catch {
|
|
1359
|
+
}
|
|
1360
|
+
}),
|
|
1361
|
+
...unmatchedGen.map(async (f) => {
|
|
1362
|
+
try {
|
|
1363
|
+
genContents.set(f.relPath, await fs3.readFile(f.absPath, "utf-8"));
|
|
1364
|
+
} catch {
|
|
1365
|
+
}
|
|
1366
|
+
})
|
|
1367
|
+
]);
|
|
1368
|
+
const pairs = [];
|
|
1369
|
+
for (const [origPath, origContent] of origContents) {
|
|
1370
|
+
for (const [genPath, genContent] of genContents) {
|
|
1371
|
+
if (path7.extname(origPath) !== path7.extname(genPath)) continue;
|
|
1372
|
+
const similarity = computeSimilarity(origContent, genContent);
|
|
1373
|
+
if (similarity >= RENAME_THRESHOLD) {
|
|
1374
|
+
pairs.push({ origPath, genPath, similarity });
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
pairs.sort((a, b) => b.similarity - a.similarity);
|
|
1379
|
+
for (const pair of pairs) {
|
|
1380
|
+
if (renamedOrig.has(pair.origPath) || renamedGen.has(pair.genPath)) continue;
|
|
1381
|
+
renamedOrig.add(pair.origPath);
|
|
1382
|
+
renamedGen.add(pair.genPath);
|
|
1383
|
+
const origContent = origContents.get(pair.origPath) ?? "";
|
|
1384
|
+
const genContent = genContents.get(pair.genPath) ?? "";
|
|
1385
|
+
const diffResult = computeLineDiff(
|
|
1386
|
+
origContent.split("\n"),
|
|
1387
|
+
genContent.split("\n"),
|
|
1388
|
+
contextLines,
|
|
1389
|
+
opts.ignoreWhitespace ?? false
|
|
1390
|
+
);
|
|
1391
|
+
const delta = {
|
|
1392
|
+
deltaId: generateDeltaId("renamed", `${pair.origPath} -> ${pair.genPath}`),
|
|
1393
|
+
changeKind: "renamed",
|
|
1394
|
+
originalPath: pair.origPath,
|
|
1395
|
+
generatedPath: pair.genPath,
|
|
1396
|
+
similarity: pair.similarity,
|
|
1397
|
+
hunks: diffResult.hunks
|
|
1398
|
+
};
|
|
1399
|
+
if (opts.stats) {
|
|
1400
|
+
delta.stats = { insertions: diffResult.insertions, deletions: diffResult.deletions };
|
|
1401
|
+
}
|
|
1402
|
+
deltas.push(delta);
|
|
1403
|
+
if (opts.verbose) {
|
|
1404
|
+
process.stderr.write(chalk6.dim(
|
|
1405
|
+
` renamed: ${pair.origPath} \u2192 ${pair.genPath} (${(pair.similarity * 100).toFixed(0)}% similar)
|
|
1406
|
+
`
|
|
1407
|
+
));
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
for (const file of unmatchedOrig) {
|
|
1412
|
+
if (renamedOrig.has(file.relPath)) continue;
|
|
1413
|
+
const delta = {
|
|
1414
|
+
deltaId: generateDeltaId("removed", file.relPath),
|
|
1415
|
+
changeKind: "removed",
|
|
1416
|
+
originalPath: file.relPath,
|
|
1417
|
+
generatedPath: null
|
|
1418
|
+
};
|
|
1419
|
+
if (opts.stats) {
|
|
1420
|
+
try {
|
|
1421
|
+
const content = await fs3.readFile(file.absPath, "utf-8");
|
|
1422
|
+
delta.stats = { insertions: 0, deletions: content.split("\n").length };
|
|
1423
|
+
} catch {
|
|
1424
|
+
delta.stats = { insertions: 0, deletions: 0 };
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
deltas.push(delta);
|
|
1428
|
+
if (opts.verbose) {
|
|
1429
|
+
process.stderr.write(chalk6.dim(` removed: ${file.relPath}
|
|
1430
|
+
`));
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
for (const file of unmatchedGen) {
|
|
1434
|
+
if (renamedGen.has(file.relPath)) continue;
|
|
1435
|
+
const delta = {
|
|
1436
|
+
deltaId: generateDeltaId("added", file.relPath),
|
|
1437
|
+
changeKind: "added",
|
|
1438
|
+
originalPath: null,
|
|
1439
|
+
generatedPath: file.relPath
|
|
1440
|
+
};
|
|
1441
|
+
if (opts.stats) {
|
|
1442
|
+
try {
|
|
1443
|
+
const content = await fs3.readFile(file.absPath, "utf-8");
|
|
1444
|
+
delta.stats = { insertions: content.split("\n").length, deletions: 0 };
|
|
1445
|
+
} catch {
|
|
1446
|
+
delta.stats = { insertions: 0, deletions: 0 };
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
deltas.push(delta);
|
|
1450
|
+
if (opts.verbose) {
|
|
1451
|
+
process.stderr.write(chalk6.dim(` added: ${file.relPath}
|
|
1452
|
+
`));
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
deltas.sort((a, b) => {
|
|
1456
|
+
const aPath = a.originalPath ?? a.generatedPath ?? "";
|
|
1457
|
+
const bPath = b.originalPath ?? b.generatedPath ?? "";
|
|
1458
|
+
return aPath.localeCompare(bPath);
|
|
1459
|
+
});
|
|
1460
|
+
let output;
|
|
1461
|
+
switch (format) {
|
|
1462
|
+
case "ndjson":
|
|
1463
|
+
output = formatNdjson(deltas);
|
|
1464
|
+
break;
|
|
1465
|
+
case "json":
|
|
1466
|
+
output = formatJson(deltas);
|
|
1467
|
+
break;
|
|
1468
|
+
case "patch":
|
|
1469
|
+
output = formatPatch(deltas, originalPath, generatedPath);
|
|
1470
|
+
break;
|
|
1471
|
+
}
|
|
1472
|
+
const outPath = path7.resolve(opts.out);
|
|
1473
|
+
await fs3.writeFile(outPath, output, "utf-8");
|
|
1474
|
+
const added = deltas.filter((d) => d.changeKind === "added").length;
|
|
1475
|
+
const removed = deltas.filter((d) => d.changeKind === "removed").length;
|
|
1476
|
+
const modified = deltas.filter((d) => d.changeKind === "modified").length;
|
|
1477
|
+
const renamed = deltas.filter((d) => d.changeKind === "renamed").length;
|
|
1478
|
+
process.stderr.write(
|
|
1479
|
+
chalk6.green(`\u2714 `) + `${deltas.length} changes: ` + chalk6.green(`+${added} added`) + ", " + chalk6.red(`-${removed} removed`) + ", " + chalk6.yellow(`~${modified} modified`) + ", " + chalk6.blue(`\u2192${renamed} renamed`) + "\n"
|
|
1480
|
+
);
|
|
1481
|
+
process.stderr.write(chalk6.dim(` Written to ${outPath}
|
|
1482
|
+
`));
|
|
1483
|
+
if (opts.verbose && opts.stats) {
|
|
1484
|
+
let totalIns = 0;
|
|
1485
|
+
let totalDel = 0;
|
|
1486
|
+
for (const d of deltas) {
|
|
1487
|
+
if (d.stats) {
|
|
1488
|
+
totalIns += d.stats.insertions;
|
|
1489
|
+
totalDel += d.stats.deletions;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
process.stderr.write(chalk6.dim(` Total: +${totalIns} insertions, -${totalDel} deletions
|
|
1493
|
+
`));
|
|
1494
|
+
}
|
|
1495
|
+
});
|
|
1496
|
+
|
|
1497
|
+
// src/commands/help.ts
|
|
1498
|
+
import { Command as Command7 } from "commander";
|
|
1499
|
+
import chalk7 from "chalk";
|
|
415
1500
|
var HELP_URL = "https://vibgrate.com/help";
|
|
416
1501
|
function printFooter() {
|
|
417
1502
|
console.log("");
|
|
418
|
-
console.log(
|
|
1503
|
+
console.log(chalk7.dim(`See ${HELP_URL} for more guidance`));
|
|
419
1504
|
}
|
|
420
1505
|
var detailedHelp = {
|
|
421
1506
|
scan: () => {
|
|
422
1507
|
console.log("");
|
|
423
|
-
console.log(
|
|
1508
|
+
console.log(chalk7.bold.underline("vibgrate scan") + chalk7.dim(" \u2014 Scan a project for upgrade drift"));
|
|
424
1509
|
console.log("");
|
|
425
|
-
console.log(
|
|
1510
|
+
console.log(chalk7.bold("Usage:"));
|
|
426
1511
|
console.log(" vibgrate scan [path] [options]");
|
|
427
1512
|
console.log("");
|
|
428
|
-
console.log(
|
|
429
|
-
console.log(` ${
|
|
430
|
-
console.log("");
|
|
431
|
-
console.log(
|
|
432
|
-
console.log(` ${
|
|
433
|
-
console.log(` ${
|
|
434
|
-
console.log("");
|
|
435
|
-
console.log(
|
|
436
|
-
console.log(` ${
|
|
437
|
-
console.log(` ${
|
|
438
|
-
console.log(` ${
|
|
439
|
-
console.log(` ${
|
|
440
|
-
console.log("");
|
|
441
|
-
console.log(
|
|
442
|
-
console.log(` ${
|
|
443
|
-
console.log(` ${
|
|
444
|
-
console.log("");
|
|
445
|
-
console.log(
|
|
446
|
-
console.log(` ${
|
|
447
|
-
console.log(` ${
|
|
448
|
-
console.log(` ${
|
|
449
|
-
console.log(` ${
|
|
450
|
-
console.log("");
|
|
451
|
-
console.log(
|
|
452
|
-
console.log(` ${
|
|
453
|
-
console.log(` ${
|
|
454
|
-
console.log("");
|
|
455
|
-
console.log(
|
|
456
|
-
console.log(` ${
|
|
457
|
-
console.log(` ${
|
|
458
|
-
console.log(` ${
|
|
459
|
-
console.log(` ${
|
|
460
|
-
console.log("");
|
|
461
|
-
console.log(
|
|
462
|
-
console.log(` ${
|
|
1513
|
+
console.log(chalk7.bold("Arguments:"));
|
|
1514
|
+
console.log(` ${chalk7.cyan("[path]")} Path to scan (default: current directory)`);
|
|
1515
|
+
console.log("");
|
|
1516
|
+
console.log(chalk7.bold("Output options:"));
|
|
1517
|
+
console.log(` ${chalk7.cyan("--format <format>")} Output format: ${chalk7.white("text")} | json | sarif | md (default: text)`);
|
|
1518
|
+
console.log(` ${chalk7.cyan("--out <file>")} Write output to a file instead of stdout`);
|
|
1519
|
+
console.log("");
|
|
1520
|
+
console.log(chalk7.bold("Baseline & gating:"));
|
|
1521
|
+
console.log(` ${chalk7.cyan("--baseline <file>")} Compare results against a saved baseline`);
|
|
1522
|
+
console.log(` ${chalk7.cyan("--drift-budget <score>")} Fail if drift score exceeds this value (0\u2013100)`);
|
|
1523
|
+
console.log(` ${chalk7.cyan("--drift-worsening <percent>")} Fail if drift worsens by more than % since baseline`);
|
|
1524
|
+
console.log(` ${chalk7.cyan("--fail-on <level>")} Fail exit code on warn or error findings`);
|
|
1525
|
+
console.log("");
|
|
1526
|
+
console.log(chalk7.bold("Performance:"));
|
|
1527
|
+
console.log(` ${chalk7.cyan("--concurrency <n>")} Max concurrent registry calls (default: 8)`);
|
|
1528
|
+
console.log(` ${chalk7.cyan("--changed-only")} Only scan files changed since last git commit`);
|
|
1529
|
+
console.log("");
|
|
1530
|
+
console.log(chalk7.bold("Privacy & offline:"));
|
|
1531
|
+
console.log(` ${chalk7.cyan("--offline")} Run without any network calls; skip result upload`);
|
|
1532
|
+
console.log(` ${chalk7.cyan("--package-manifest <file>")} Use a local package-version manifest (JSON or ZIP) for offline mode`);
|
|
1533
|
+
console.log(` ${chalk7.cyan("--no-local-artifacts")} Do not write .vibgrate JSON artifacts to disk`);
|
|
1534
|
+
console.log(` ${chalk7.cyan("--max-privacy")} Strongest privacy mode: minimal scanners + no local artifacts`);
|
|
1535
|
+
console.log("");
|
|
1536
|
+
console.log(chalk7.bold("Tooling:"));
|
|
1537
|
+
console.log(` ${chalk7.cyan("--install-tools")} Auto-install missing security scanners via Homebrew`);
|
|
1538
|
+
console.log(` ${chalk7.cyan("--ui-purpose")} Enable UI purpose evidence extraction (slower)`);
|
|
1539
|
+
console.log("");
|
|
1540
|
+
console.log(chalk7.bold("Uploading results:"));
|
|
1541
|
+
console.log(` ${chalk7.cyan("--push")} Auto-push results to Vibgrate API after scan`);
|
|
1542
|
+
console.log(` ${chalk7.cyan("--dsn <dsn>")} DSN token for push (or set VIBGRATE_DSN env var)`);
|
|
1543
|
+
console.log(` ${chalk7.cyan("--region <region>")} Data residency region: us | eu (default: us)`);
|
|
1544
|
+
console.log(` ${chalk7.cyan("--strict")} Fail if the upload to Vibgrate API fails`);
|
|
1545
|
+
console.log("");
|
|
1546
|
+
console.log(chalk7.bold("Examples:"));
|
|
1547
|
+
console.log(` ${chalk7.dim("# Scan the current directory and display a text report")}`);
|
|
463
1548
|
console.log(" vibgrate scan .");
|
|
464
1549
|
console.log("");
|
|
465
|
-
console.log(` ${
|
|
1550
|
+
console.log(` ${chalk7.dim("# Scan, fail if drift score > 40, and write SARIF for GitHub Actions")}`);
|
|
466
1551
|
console.log(" vibgrate scan . --drift-budget 40 --format sarif --out drift.sarif");
|
|
467
1552
|
console.log("");
|
|
468
|
-
console.log(` ${
|
|
1553
|
+
console.log(` ${chalk7.dim("# Scan and automatically upload results via a DSN")}`);
|
|
469
1554
|
console.log(" vibgrate scan . --push --dsn $VIBGRATE_DSN");
|
|
470
1555
|
console.log("");
|
|
471
|
-
console.log(` ${
|
|
1556
|
+
console.log(` ${chalk7.dim("# Offline scan using a pre-downloaded package manifest")}`);
|
|
472
1557
|
console.log(" vibgrate scan . --offline --package-manifest ./manifest.zip");
|
|
473
1558
|
},
|
|
474
1559
|
init: () => {
|
|
475
1560
|
console.log("");
|
|
476
|
-
console.log(
|
|
1561
|
+
console.log(chalk7.bold.underline("vibgrate init") + chalk7.dim(" \u2014 Initialise vibgrate in a project directory"));
|
|
477
1562
|
console.log("");
|
|
478
|
-
console.log(
|
|
1563
|
+
console.log(chalk7.bold("Usage:"));
|
|
479
1564
|
console.log(" vibgrate init [path] [options]");
|
|
480
1565
|
console.log("");
|
|
481
|
-
console.log(
|
|
482
|
-
console.log(` ${
|
|
1566
|
+
console.log(chalk7.bold("Arguments:"));
|
|
1567
|
+
console.log(` ${chalk7.cyan("[path]")} Directory to initialise (default: current directory)`);
|
|
483
1568
|
console.log("");
|
|
484
|
-
console.log(
|
|
485
|
-
console.log(` ${
|
|
486
|
-
console.log(` ${
|
|
1569
|
+
console.log(chalk7.bold("Options:"));
|
|
1570
|
+
console.log(` ${chalk7.cyan("--baseline")} Create an initial drift baseline after init`);
|
|
1571
|
+
console.log(` ${chalk7.cyan("--yes")} Skip all confirmation prompts`);
|
|
487
1572
|
console.log("");
|
|
488
|
-
console.log(
|
|
1573
|
+
console.log(chalk7.bold("What it does:"));
|
|
489
1574
|
console.log(" \u2022 Creates a .vibgrate/ directory");
|
|
490
1575
|
console.log(" \u2022 Writes a vibgrate.config.ts starter config");
|
|
491
1576
|
console.log(" \u2022 Optionally runs an initial baseline scan (--baseline)");
|
|
492
1577
|
console.log("");
|
|
493
|
-
console.log(
|
|
1578
|
+
console.log(chalk7.bold("Examples:"));
|
|
494
1579
|
console.log(" vibgrate init");
|
|
495
1580
|
console.log(" vibgrate init ./my-project --baseline");
|
|
496
1581
|
},
|
|
497
1582
|
baseline: () => {
|
|
498
1583
|
console.log("");
|
|
499
|
-
console.log(
|
|
1584
|
+
console.log(chalk7.bold.underline("vibgrate baseline") + chalk7.dim(" \u2014 Save a drift baseline snapshot"));
|
|
500
1585
|
console.log("");
|
|
501
|
-
console.log(
|
|
1586
|
+
console.log(chalk7.bold("Usage:"));
|
|
502
1587
|
console.log(" vibgrate baseline [path]");
|
|
503
1588
|
console.log("");
|
|
504
|
-
console.log(
|
|
505
|
-
console.log(` ${
|
|
1589
|
+
console.log(chalk7.bold("Arguments:"));
|
|
1590
|
+
console.log(` ${chalk7.cyan("[path]")} Path to baseline (default: current directory)`);
|
|
506
1591
|
console.log("");
|
|
507
|
-
console.log(
|
|
1592
|
+
console.log(chalk7.bold("What it does:"));
|
|
508
1593
|
console.log(" Runs a full scan and saves the result as .vibgrate/baseline.json.");
|
|
509
1594
|
console.log(" Future scans can compare against this file using --baseline.");
|
|
510
1595
|
console.log("");
|
|
511
|
-
console.log(
|
|
1596
|
+
console.log(chalk7.bold("Examples:"));
|
|
512
1597
|
console.log(" vibgrate baseline .");
|
|
513
1598
|
console.log(" vibgrate scan . --baseline .vibgrate/baseline.json --drift-worsening 10");
|
|
514
1599
|
},
|
|
515
1600
|
report: () => {
|
|
516
1601
|
console.log("");
|
|
517
|
-
console.log(
|
|
1602
|
+
console.log(chalk7.bold.underline("vibgrate report") + chalk7.dim(" \u2014 Generate a report from a saved scan artifact"));
|
|
518
1603
|
console.log("");
|
|
519
|
-
console.log(
|
|
1604
|
+
console.log(chalk7.bold("Usage:"));
|
|
520
1605
|
console.log(" vibgrate report [options]");
|
|
521
1606
|
console.log("");
|
|
522
|
-
console.log(
|
|
523
|
-
console.log(` ${
|
|
524
|
-
console.log(` ${
|
|
1607
|
+
console.log(chalk7.bold("Options:"));
|
|
1608
|
+
console.log(` ${chalk7.cyan("--in <file>")} Input artifact file (default: .vibgrate/scan_result.json)`);
|
|
1609
|
+
console.log(` ${chalk7.cyan("--format <format>")} Output format: ${chalk7.white("text")} | md | json (default: text)`);
|
|
525
1610
|
console.log("");
|
|
526
|
-
console.log(
|
|
1611
|
+
console.log(chalk7.bold("Examples:"));
|
|
527
1612
|
console.log(" vibgrate report");
|
|
528
1613
|
console.log(" vibgrate report --format md > DRIFT-REPORT.md");
|
|
529
1614
|
console.log(" vibgrate report --in ./ci/scan_result.json --format json");
|
|
530
1615
|
},
|
|
531
1616
|
sbom: () => {
|
|
532
1617
|
console.log("");
|
|
533
|
-
console.log(
|
|
1618
|
+
console.log(chalk7.bold.underline("vibgrate sbom") + chalk7.dim(" \u2014 Export a Software Bill of Materials from a scan artifact"));
|
|
534
1619
|
console.log("");
|
|
535
|
-
console.log(
|
|
1620
|
+
console.log(chalk7.bold("Usage:"));
|
|
536
1621
|
console.log(" vibgrate sbom [options]");
|
|
537
1622
|
console.log("");
|
|
538
|
-
console.log(
|
|
539
|
-
console.log(` ${
|
|
540
|
-
console.log(` ${
|
|
541
|
-
console.log(` ${
|
|
1623
|
+
console.log(chalk7.bold("Options:"));
|
|
1624
|
+
console.log(` ${chalk7.cyan("--in <file>")} Input artifact (default: .vibgrate/scan_result.json)`);
|
|
1625
|
+
console.log(` ${chalk7.cyan("--format <format>")} SBOM format: ${chalk7.white("cyclonedx")} | spdx (default: cyclonedx)`);
|
|
1626
|
+
console.log(` ${chalk7.cyan("--out <file>")} Write SBOM to file instead of stdout`);
|
|
542
1627
|
console.log("");
|
|
543
|
-
console.log(
|
|
1628
|
+
console.log(chalk7.bold("Examples:"));
|
|
544
1629
|
console.log(" vibgrate sbom --format cyclonedx --out sbom.json");
|
|
545
1630
|
console.log(" vibgrate sbom --format spdx --out sbom.spdx.json");
|
|
546
1631
|
},
|
|
547
1632
|
push: () => {
|
|
548
1633
|
console.log("");
|
|
549
|
-
console.log(
|
|
1634
|
+
console.log(chalk7.bold.underline("vibgrate push") + chalk7.dim(" \u2014 Upload a scan artifact to the Vibgrate API"));
|
|
550
1635
|
console.log("");
|
|
551
|
-
console.log(
|
|
1636
|
+
console.log(chalk7.bold("Usage:"));
|
|
552
1637
|
console.log(" vibgrate push [options]");
|
|
553
1638
|
console.log("");
|
|
554
|
-
console.log(
|
|
555
|
-
console.log(` ${
|
|
556
|
-
console.log(` ${
|
|
557
|
-
console.log(` ${
|
|
558
|
-
console.log(` ${
|
|
1639
|
+
console.log(chalk7.bold("Options:"));
|
|
1640
|
+
console.log(` ${chalk7.cyan("--dsn <dsn>")} DSN token (or set VIBGRATE_DSN env var)`);
|
|
1641
|
+
console.log(` ${chalk7.cyan("--file <file>")} Artifact to upload (default: .vibgrate/scan_result.json)`);
|
|
1642
|
+
console.log(` ${chalk7.cyan("--region <region>")} Override data residency region: us | eu`);
|
|
1643
|
+
console.log(` ${chalk7.cyan("--strict")} Fail with non-zero exit code on upload error`);
|
|
559
1644
|
console.log("");
|
|
560
|
-
console.log(
|
|
1645
|
+
console.log(chalk7.bold("Examples:"));
|
|
561
1646
|
console.log(" vibgrate push --dsn $VIBGRATE_DSN");
|
|
562
1647
|
console.log(" vibgrate push --file ./ci/scan_result.json --strict");
|
|
563
1648
|
},
|
|
564
1649
|
dsn: () => {
|
|
565
1650
|
console.log("");
|
|
566
|
-
console.log(
|
|
1651
|
+
console.log(chalk7.bold.underline("vibgrate dsn") + chalk7.dim(" \u2014 Manage DSN tokens for API authentication"));
|
|
567
1652
|
console.log("");
|
|
568
|
-
console.log(
|
|
569
|
-
console.log(` ${
|
|
1653
|
+
console.log(chalk7.bold("Subcommands:"));
|
|
1654
|
+
console.log(` ${chalk7.cyan("vibgrate dsn create")} Generate a new DSN token`);
|
|
570
1655
|
console.log("");
|
|
571
|
-
console.log(
|
|
572
|
-
console.log(` ${
|
|
573
|
-
console.log(` ${
|
|
574
|
-
console.log(` ${
|
|
575
|
-
console.log(` ${
|
|
1656
|
+
console.log(chalk7.bold("dsn create options:"));
|
|
1657
|
+
console.log(` ${chalk7.cyan("--workspace <id>")} Workspace ID or "new" to auto-generate ${chalk7.red("(required)")}`);
|
|
1658
|
+
console.log(` ${chalk7.cyan("--region <region>")} Data residency region: us | eu (default: us)`);
|
|
1659
|
+
console.log(` ${chalk7.cyan("--ingest <url>")} Override ingest API URL`);
|
|
1660
|
+
console.log(` ${chalk7.cyan("--write <path>")} Write the DSN to a file (add to .gitignore!)`);
|
|
576
1661
|
console.log("");
|
|
577
|
-
console.log(
|
|
1662
|
+
console.log(chalk7.bold("Examples:"));
|
|
578
1663
|
console.log(" vibgrate dsn create --workspace new");
|
|
579
1664
|
console.log(" vibgrate dsn create --workspace abc123");
|
|
580
1665
|
console.log(" vibgrate dsn create --workspace new --region eu --write .vibgrate/.dsn");
|
|
581
1666
|
},
|
|
582
1667
|
update: () => {
|
|
583
1668
|
console.log("");
|
|
584
|
-
console.log(
|
|
1669
|
+
console.log(chalk7.bold.underline("vibgrate update") + chalk7.dim(" \u2014 Update the vibgrate CLI to the latest version"));
|
|
585
1670
|
console.log("");
|
|
586
|
-
console.log(
|
|
1671
|
+
console.log(chalk7.bold("Usage:"));
|
|
587
1672
|
console.log(" vibgrate update [options]");
|
|
588
1673
|
console.log("");
|
|
589
|
-
console.log(
|
|
590
|
-
console.log(` ${
|
|
591
|
-
console.log(` ${
|
|
1674
|
+
console.log(chalk7.bold("Options:"));
|
|
1675
|
+
console.log(` ${chalk7.cyan("--check")} Check for a newer version without installing`);
|
|
1676
|
+
console.log(` ${chalk7.cyan("--pm <manager>")} Force a package manager: npm | pnpm | yarn | bun`);
|
|
592
1677
|
console.log("");
|
|
593
|
-
console.log(
|
|
1678
|
+
console.log(chalk7.bold("Examples:"));
|
|
594
1679
|
console.log(" vibgrate update");
|
|
595
1680
|
console.log(" vibgrate update --check");
|
|
596
1681
|
console.log(" vibgrate update --pm pnpm");
|
|
@@ -598,40 +1683,40 @@ var detailedHelp = {
|
|
|
598
1683
|
};
|
|
599
1684
|
function printSummaryHelp() {
|
|
600
1685
|
console.log("");
|
|
601
|
-
console.log(
|
|
1686
|
+
console.log(chalk7.bold("vibgrate") + chalk7.dim(" \u2014 Continuous Drift Intelligence"));
|
|
602
1687
|
console.log("");
|
|
603
|
-
console.log(
|
|
1688
|
+
console.log(chalk7.bold("Usage:"));
|
|
604
1689
|
console.log(" vibgrate <command> [options]");
|
|
605
1690
|
console.log(" vibgrate help [command] Show detailed help for a command");
|
|
606
1691
|
console.log("");
|
|
607
|
-
console.log(
|
|
608
|
-
console.log(` ${
|
|
1692
|
+
console.log(chalk7.bold("Getting started:"));
|
|
1693
|
+
console.log(` ${chalk7.cyan("init")} Initialise vibgrate in a project (creates config & .vibgrate/ dir)`);
|
|
609
1694
|
console.log("");
|
|
610
|
-
console.log(
|
|
611
|
-
console.log(` ${
|
|
612
|
-
console.log(` ${
|
|
1695
|
+
console.log(chalk7.bold("Core scanning:"));
|
|
1696
|
+
console.log(` ${chalk7.cyan("scan")} Scan a project for upgrade drift and generate a report`);
|
|
1697
|
+
console.log(` ${chalk7.cyan("baseline")} Save a baseline snapshot to compare future scans against`);
|
|
613
1698
|
console.log("");
|
|
614
|
-
console.log(
|
|
615
|
-
console.log(` ${
|
|
616
|
-
console.log(` ${
|
|
1699
|
+
console.log(chalk7.bold("Reporting & export:"));
|
|
1700
|
+
console.log(` ${chalk7.cyan("report")} Re-generate a report from a previously saved scan artifact`);
|
|
1701
|
+
console.log(` ${chalk7.cyan("sbom")} Export a Software Bill of Materials (CycloneDX or SPDX)`);
|
|
617
1702
|
console.log("");
|
|
618
|
-
console.log(
|
|
619
|
-
console.log(` ${
|
|
620
|
-
console.log(` ${
|
|
1703
|
+
console.log(chalk7.bold("CI/CD integration:"));
|
|
1704
|
+
console.log(` ${chalk7.cyan("push")} Upload a scan artifact to the Vibgrate API`);
|
|
1705
|
+
console.log(` ${chalk7.cyan("dsn")} Create and manage DSN tokens for API authentication`);
|
|
621
1706
|
console.log("");
|
|
622
|
-
console.log(
|
|
623
|
-
console.log(` ${
|
|
1707
|
+
console.log(chalk7.bold("Maintenance:"));
|
|
1708
|
+
console.log(` ${chalk7.cyan("update")} Update the vibgrate CLI to the latest version`);
|
|
624
1709
|
console.log("");
|
|
625
|
-
console.log(
|
|
1710
|
+
console.log(chalk7.dim("Run") + ` ${chalk7.cyan("vibgrate help <command>")} ` + chalk7.dim("for detailed options, e.g.") + ` ${chalk7.cyan("vibgrate help scan")}`);
|
|
626
1711
|
}
|
|
627
|
-
var helpCommand = new
|
|
1712
|
+
var helpCommand = new Command7("help").description("Show help for vibgrate commands").argument("[command]", "Command to show detailed help for").helpOption(false).action((cmd) => {
|
|
628
1713
|
const name = cmd?.toLowerCase().trim();
|
|
629
1714
|
if (name && detailedHelp[name]) {
|
|
630
1715
|
detailedHelp[name]();
|
|
631
1716
|
} else if (name) {
|
|
632
1717
|
console.log("");
|
|
633
|
-
console.log(
|
|
634
|
-
console.log(
|
|
1718
|
+
console.log(chalk7.red(`Unknown command: ${name}`));
|
|
1719
|
+
console.log(chalk7.dim(`Available commands: ${Object.keys(detailedHelp).join(", ")}`));
|
|
635
1720
|
printSummaryHelp();
|
|
636
1721
|
} else {
|
|
637
1722
|
printSummaryHelp();
|
|
@@ -640,7 +1725,7 @@ var helpCommand = new Command5("help").description("Show help for vibgrate comma
|
|
|
640
1725
|
});
|
|
641
1726
|
|
|
642
1727
|
// src/cli.ts
|
|
643
|
-
var program = new
|
|
1728
|
+
var program = new Command8();
|
|
644
1729
|
program.name("vibgrate").description("Continuous Drift Intelligence").version(VERSION).addHelpText("after", "\nSee https://vibgrate.com/help for more guidance");
|
|
645
1730
|
program.addCommand(helpCommand);
|
|
646
1731
|
program.addCommand(initCommand);
|
|
@@ -651,12 +1736,14 @@ program.addCommand(dsnCommand);
|
|
|
651
1736
|
program.addCommand(pushCommand);
|
|
652
1737
|
program.addCommand(updateCommand);
|
|
653
1738
|
program.addCommand(sbomCommand);
|
|
1739
|
+
program.addCommand(extractCommand);
|
|
1740
|
+
program.addCommand(diffCommand);
|
|
654
1741
|
function notifyIfUpdateAvailable() {
|
|
655
1742
|
void checkForUpdate().then((update) => {
|
|
656
1743
|
if (!update?.updateAvailable) return;
|
|
657
1744
|
console.error("");
|
|
658
|
-
console.error(
|
|
659
|
-
console.error(
|
|
1745
|
+
console.error(chalk8.yellow(` Update available: ${update.current} \u2192 ${update.latest}`));
|
|
1746
|
+
console.error(chalk8.dim(' Run "vibgrate update" to install the latest version.'));
|
|
660
1747
|
console.error("");
|
|
661
1748
|
}).catch(() => {
|
|
662
1749
|
});
|