@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/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,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/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
+ 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(chalk5.dim(`See ${HELP_URL} for more guidance`));
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(chalk5.bold.underline("vibgrate scan") + chalk5.dim(" \u2014 Scan a project for upgrade drift"));
1512
+ console.log(chalk7.bold.underline("vibgrate scan") + chalk7.dim(" \u2014 Scan a project for upgrade drift"));
424
1513
  console.log("");
425
- console.log(chalk5.bold("Usage:"));
1514
+ console.log(chalk7.bold("Usage:"));
426
1515
  console.log(" vibgrate scan [path] [options]");
427
1516
  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")}`);
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(` ${chalk5.dim("# Scan, fail if drift score > 40, and write SARIF for GitHub Actions")}`);
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(` ${chalk5.dim("# Scan and automatically upload results via a DSN")}`);
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(` ${chalk5.dim("# Offline scan using a pre-downloaded package manifest")}`);
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(chalk5.bold.underline("vibgrate init") + chalk5.dim(" \u2014 Initialise vibgrate in a project directory"));
1565
+ console.log(chalk7.bold.underline("vibgrate init") + chalk7.dim(" \u2014 Initialise vibgrate in a project directory"));
477
1566
  console.log("");
478
- console.log(chalk5.bold("Usage:"));
1567
+ console.log(chalk7.bold("Usage:"));
479
1568
  console.log(" vibgrate init [path] [options]");
480
1569
  console.log("");
481
- console.log(chalk5.bold("Arguments:"));
482
- console.log(` ${chalk5.cyan("[path]")} Directory to initialise (default: current directory)`);
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(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`);
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(chalk5.bold("What it does:"));
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(chalk5.bold("Examples:"));
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(chalk5.bold.underline("vibgrate baseline") + chalk5.dim(" \u2014 Save a drift baseline snapshot"));
1588
+ console.log(chalk7.bold.underline("vibgrate baseline") + chalk7.dim(" \u2014 Save a drift baseline snapshot"));
500
1589
  console.log("");
501
- console.log(chalk5.bold("Usage:"));
1590
+ console.log(chalk7.bold("Usage:"));
502
1591
  console.log(" vibgrate baseline [path]");
503
1592
  console.log("");
504
- console.log(chalk5.bold("Arguments:"));
505
- console.log(` ${chalk5.cyan("[path]")} Path to baseline (default: current directory)`);
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(chalk5.bold("What it does:"));
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(chalk5.bold("Examples:"));
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(chalk5.bold.underline("vibgrate report") + chalk5.dim(" \u2014 Generate a report from a saved scan artifact"));
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(chalk5.bold("Usage:"));
1608
+ console.log(chalk7.bold("Usage:"));
520
1609
  console.log(" vibgrate report [options]");
521
1610
  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)`);
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(chalk5.bold("Examples:"));
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(chalk5.bold.underline("vibgrate sbom") + chalk5.dim(" \u2014 Export a Software Bill of Materials from a scan artifact"));
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(chalk5.bold("Usage:"));
1624
+ console.log(chalk7.bold("Usage:"));
536
1625
  console.log(" vibgrate sbom [options]");
537
1626
  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`);
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(chalk5.bold("Examples:"));
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(chalk5.bold.underline("vibgrate push") + chalk5.dim(" \u2014 Upload a scan artifact to the Vibgrate API"));
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(chalk5.bold("Usage:"));
1640
+ console.log(chalk7.bold("Usage:"));
552
1641
  console.log(" vibgrate push [options]");
553
1642
  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`);
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(chalk5.bold("Examples:"));
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(chalk5.bold.underline("vibgrate dsn") + chalk5.dim(" \u2014 Manage DSN tokens for API authentication"));
1655
+ console.log(chalk7.bold.underline("vibgrate dsn") + chalk7.dim(" \u2014 Manage DSN tokens for API authentication"));
567
1656
  console.log("");
568
- console.log(chalk5.bold("Subcommands:"));
569
- console.log(` ${chalk5.cyan("vibgrate dsn create")} Generate a new DSN token`);
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(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!)`);
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(chalk5.bold("Examples:"));
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(chalk5.bold.underline("vibgrate update") + chalk5.dim(" \u2014 Update the vibgrate CLI to the latest version"));
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(chalk5.bold("Usage:"));
1675
+ console.log(chalk7.bold("Usage:"));
587
1676
  console.log(" vibgrate update [options]");
588
1677
  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`);
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(chalk5.bold("Examples:"));
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(chalk5.bold("vibgrate") + chalk5.dim(" \u2014 Continuous Drift Intelligence"));
1690
+ console.log(chalk7.bold("vibgrate") + chalk7.dim(" \u2014 Continuous Drift Intelligence"));
602
1691
  console.log("");
603
- console.log(chalk5.bold("Usage:"));
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(chalk5.bold("Getting started:"));
608
- console.log(` ${chalk5.cyan("init")} Initialise vibgrate in a project (creates config & .vibgrate/ dir)`);
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(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`);
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(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)`);
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(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`);
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(chalk5.bold("Maintenance:"));
623
- console.log(` ${chalk5.cyan("update")} Update the vibgrate CLI to the latest version`);
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(chalk5.dim("Run") + ` ${chalk5.cyan("vibgrate help <command>")} ` + chalk5.dim("for detailed options, e.g.") + ` ${chalk5.cyan("vibgrate help scan")}`);
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 Command5("help").description("Show help for vibgrate commands").argument("[command]", "Command to show detailed help for").helpOption(false).action((cmd) => {
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(chalk5.red(`Unknown command: ${name}`));
634
- console.log(chalk5.dim(`Available commands: ${Object.keys(detailedHelp).join(", ")}`));
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 Command6();
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(chalk6.yellow(` Update available: ${update.current} \u2192 ${update.latest}`));
659
- console.error(chalk6.dim(' Run "vibgrate update" to install the latest version.'));
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
  });