@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-CCU77BBA.js";
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-S5HZOJPF.js";
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 Command6 } from "commander";
23
- import chalk6 from "chalk";
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-ZFMJMB7S.js");
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/help.ts
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(chalk5.dim(`See ${HELP_URL} for more guidance`));
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(chalk5.bold.underline("vibgrate scan") + chalk5.dim(" \u2014 Scan a project for upgrade drift"));
1508
+ console.log(chalk7.bold.underline("vibgrate scan") + chalk7.dim(" \u2014 Scan a project for upgrade drift"));
424
1509
  console.log("");
425
- console.log(chalk5.bold("Usage:"));
1510
+ console.log(chalk7.bold("Usage:"));
426
1511
  console.log(" vibgrate scan [path] [options]");
427
1512
  console.log("");
428
- console.log(chalk5.bold("Arguments:"));
429
- console.log(` ${chalk5.cyan("[path]")} Path to scan (default: current directory)`);
430
- console.log("");
431
- console.log(chalk5.bold("Output options:"));
432
- console.log(` ${chalk5.cyan("--format <format>")} Output format: ${chalk5.white("text")} | json | sarif | md (default: text)`);
433
- console.log(` ${chalk5.cyan("--out <file>")} Write output to a file instead of stdout`);
434
- console.log("");
435
- console.log(chalk5.bold("Baseline & gating:"));
436
- console.log(` ${chalk5.cyan("--baseline <file>")} Compare results against a saved baseline`);
437
- console.log(` ${chalk5.cyan("--drift-budget <score>")} Fail if drift score exceeds this value (0\u2013100)`);
438
- console.log(` ${chalk5.cyan("--drift-worsening <percent>")} Fail if drift worsens by more than % since baseline`);
439
- console.log(` ${chalk5.cyan("--fail-on <level>")} Fail exit code on warn or error findings`);
440
- console.log("");
441
- console.log(chalk5.bold("Performance:"));
442
- console.log(` ${chalk5.cyan("--concurrency <n>")} Max concurrent registry calls (default: 8)`);
443
- console.log(` ${chalk5.cyan("--changed-only")} Only scan files changed since last git commit`);
444
- console.log("");
445
- console.log(chalk5.bold("Privacy & offline:"));
446
- console.log(` ${chalk5.cyan("--offline")} Run without any network calls; skip result upload`);
447
- console.log(` ${chalk5.cyan("--package-manifest <file>")} Use a local package-version manifest (JSON or ZIP) for offline mode`);
448
- console.log(` ${chalk5.cyan("--no-local-artifacts")} Do not write .vibgrate JSON artifacts to disk`);
449
- console.log(` ${chalk5.cyan("--max-privacy")} Strongest privacy mode: minimal scanners + no local artifacts`);
450
- console.log("");
451
- console.log(chalk5.bold("Tooling:"));
452
- console.log(` ${chalk5.cyan("--install-tools")} Auto-install missing security scanners via Homebrew`);
453
- console.log(` ${chalk5.cyan("--ui-purpose")} Enable UI purpose evidence extraction (slower)`);
454
- console.log("");
455
- console.log(chalk5.bold("Uploading results:"));
456
- console.log(` ${chalk5.cyan("--push")} Auto-push results to Vibgrate API after scan`);
457
- console.log(` ${chalk5.cyan("--dsn <dsn>")} DSN token for push (or set VIBGRATE_DSN env var)`);
458
- console.log(` ${chalk5.cyan("--region <region>")} Data residency region: us | eu (default: us)`);
459
- console.log(` ${chalk5.cyan("--strict")} Fail if the upload to Vibgrate API fails`);
460
- console.log("");
461
- console.log(chalk5.bold("Examples:"));
462
- console.log(` ${chalk5.dim("# Scan the current directory and display a text report")}`);
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(` ${chalk5.dim("# Scan, fail if drift score > 40, and write SARIF for GitHub Actions")}`);
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(` ${chalk5.dim("# Scan and automatically upload results via a DSN")}`);
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(` ${chalk5.dim("# Offline scan using a pre-downloaded package manifest")}`);
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(chalk5.bold.underline("vibgrate init") + chalk5.dim(" \u2014 Initialise vibgrate in a project directory"));
1561
+ console.log(chalk7.bold.underline("vibgrate init") + chalk7.dim(" \u2014 Initialise vibgrate in a project directory"));
477
1562
  console.log("");
478
- console.log(chalk5.bold("Usage:"));
1563
+ console.log(chalk7.bold("Usage:"));
479
1564
  console.log(" vibgrate init [path] [options]");
480
1565
  console.log("");
481
- console.log(chalk5.bold("Arguments:"));
482
- console.log(` ${chalk5.cyan("[path]")} Directory to initialise (default: current directory)`);
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(chalk5.bold("Options:"));
485
- console.log(` ${chalk5.cyan("--baseline")} Create an initial drift baseline after init`);
486
- console.log(` ${chalk5.cyan("--yes")} Skip all confirmation prompts`);
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(chalk5.bold("What it does:"));
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(chalk5.bold("Examples:"));
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(chalk5.bold.underline("vibgrate baseline") + chalk5.dim(" \u2014 Save a drift baseline snapshot"));
1584
+ console.log(chalk7.bold.underline("vibgrate baseline") + chalk7.dim(" \u2014 Save a drift baseline snapshot"));
500
1585
  console.log("");
501
- console.log(chalk5.bold("Usage:"));
1586
+ console.log(chalk7.bold("Usage:"));
502
1587
  console.log(" vibgrate baseline [path]");
503
1588
  console.log("");
504
- console.log(chalk5.bold("Arguments:"));
505
- console.log(` ${chalk5.cyan("[path]")} Path to baseline (default: current directory)`);
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(chalk5.bold("What it does:"));
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(chalk5.bold("Examples:"));
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(chalk5.bold.underline("vibgrate report") + chalk5.dim(" \u2014 Generate a report from a saved scan artifact"));
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(chalk5.bold("Usage:"));
1604
+ console.log(chalk7.bold("Usage:"));
520
1605
  console.log(" vibgrate report [options]");
521
1606
  console.log("");
522
- console.log(chalk5.bold("Options:"));
523
- console.log(` ${chalk5.cyan("--in <file>")} Input artifact file (default: .vibgrate/scan_result.json)`);
524
- console.log(` ${chalk5.cyan("--format <format>")} Output format: ${chalk5.white("text")} | md | json (default: text)`);
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(chalk5.bold("Examples:"));
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(chalk5.bold.underline("vibgrate sbom") + chalk5.dim(" \u2014 Export a Software Bill of Materials from a scan artifact"));
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(chalk5.bold("Usage:"));
1620
+ console.log(chalk7.bold("Usage:"));
536
1621
  console.log(" vibgrate sbom [options]");
537
1622
  console.log("");
538
- console.log(chalk5.bold("Options:"));
539
- console.log(` ${chalk5.cyan("--in <file>")} Input artifact (default: .vibgrate/scan_result.json)`);
540
- console.log(` ${chalk5.cyan("--format <format>")} SBOM format: ${chalk5.white("cyclonedx")} | spdx (default: cyclonedx)`);
541
- console.log(` ${chalk5.cyan("--out <file>")} Write SBOM to file instead of stdout`);
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(chalk5.bold("Examples:"));
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(chalk5.bold.underline("vibgrate push") + chalk5.dim(" \u2014 Upload a scan artifact to the Vibgrate API"));
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(chalk5.bold("Usage:"));
1636
+ console.log(chalk7.bold("Usage:"));
552
1637
  console.log(" vibgrate push [options]");
553
1638
  console.log("");
554
- console.log(chalk5.bold("Options:"));
555
- console.log(` ${chalk5.cyan("--dsn <dsn>")} DSN token (or set VIBGRATE_DSN env var)`);
556
- console.log(` ${chalk5.cyan("--file <file>")} Artifact to upload (default: .vibgrate/scan_result.json)`);
557
- console.log(` ${chalk5.cyan("--region <region>")} Override data residency region: us | eu`);
558
- console.log(` ${chalk5.cyan("--strict")} Fail with non-zero exit code on upload error`);
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(chalk5.bold("Examples:"));
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(chalk5.bold.underline("vibgrate dsn") + chalk5.dim(" \u2014 Manage DSN tokens for API authentication"));
1651
+ console.log(chalk7.bold.underline("vibgrate dsn") + chalk7.dim(" \u2014 Manage DSN tokens for API authentication"));
567
1652
  console.log("");
568
- console.log(chalk5.bold("Subcommands:"));
569
- console.log(` ${chalk5.cyan("vibgrate dsn create")} Generate a new DSN token`);
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(chalk5.bold("dsn create options:"));
572
- console.log(` ${chalk5.cyan("--workspace <id>")} Workspace ID or "new" to auto-generate ${chalk5.red("(required)")}`);
573
- console.log(` ${chalk5.cyan("--region <region>")} Data residency region: us | eu (default: us)`);
574
- console.log(` ${chalk5.cyan("--ingest <url>")} Override ingest API URL`);
575
- console.log(` ${chalk5.cyan("--write <path>")} Write the DSN to a file (add to .gitignore!)`);
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(chalk5.bold("Examples:"));
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(chalk5.bold.underline("vibgrate update") + chalk5.dim(" \u2014 Update the vibgrate CLI to the latest version"));
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(chalk5.bold("Usage:"));
1671
+ console.log(chalk7.bold("Usage:"));
587
1672
  console.log(" vibgrate update [options]");
588
1673
  console.log("");
589
- console.log(chalk5.bold("Options:"));
590
- console.log(` ${chalk5.cyan("--check")} Check for a newer version without installing`);
591
- console.log(` ${chalk5.cyan("--pm <manager>")} Force a package manager: npm | pnpm | yarn | bun`);
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(chalk5.bold("Examples:"));
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(chalk5.bold("vibgrate") + chalk5.dim(" \u2014 Continuous Drift Intelligence"));
1686
+ console.log(chalk7.bold("vibgrate") + chalk7.dim(" \u2014 Continuous Drift Intelligence"));
602
1687
  console.log("");
603
- console.log(chalk5.bold("Usage:"));
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(chalk5.bold("Getting started:"));
608
- console.log(` ${chalk5.cyan("init")} Initialise vibgrate in a project (creates config & .vibgrate/ dir)`);
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(chalk5.bold("Core scanning:"));
611
- console.log(` ${chalk5.cyan("scan")} Scan a project for upgrade drift and generate a report`);
612
- console.log(` ${chalk5.cyan("baseline")} Save a baseline snapshot to compare future scans against`);
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(chalk5.bold("Reporting & export:"));
615
- console.log(` ${chalk5.cyan("report")} Re-generate a report from a previously saved scan artifact`);
616
- console.log(` ${chalk5.cyan("sbom")} Export a Software Bill of Materials (CycloneDX or SPDX)`);
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(chalk5.bold("CI/CD integration:"));
619
- console.log(` ${chalk5.cyan("push")} Upload a scan artifact to the Vibgrate API`);
620
- console.log(` ${chalk5.cyan("dsn")} Create and manage DSN tokens for API authentication`);
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(chalk5.bold("Maintenance:"));
623
- console.log(` ${chalk5.cyan("update")} Update the vibgrate CLI to the latest version`);
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(chalk5.dim("Run") + ` ${chalk5.cyan("vibgrate help <command>")} ` + chalk5.dim("for detailed options, e.g.") + ` ${chalk5.cyan("vibgrate help scan")}`);
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 Command5("help").description("Show help for vibgrate commands").argument("[command]", "Command to show detailed help for").helpOption(false).action((cmd) => {
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(chalk5.red(`Unknown command: ${name}`));
634
- console.log(chalk5.dim(`Available commands: ${Object.keys(detailedHelp).join(", ")}`));
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 Command6();
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(chalk6.yellow(` Update available: ${update.current} \u2192 ${update.latest}`));
659
- console.error(chalk6.dim(' Run "vibgrate update" to install the latest version.'));
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
  });