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