@vibgrate/cli 1.0.1 → 1.0.2

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.
@@ -4,6 +4,7 @@ import * as path from "path";
4
4
  var SKIP_DIRS = /* @__PURE__ */ new Set([
5
5
  "node_modules",
6
6
  ".git",
7
+ ".vibgrate",
7
8
  ".next",
8
9
  "dist",
9
10
  "build",
@@ -80,6 +81,7 @@ async function writeTextFile(filePath, content) {
80
81
  }
81
82
 
82
83
  // src/scoring/drift-score.ts
84
+ import * as crypto from "crypto";
83
85
  var DEFAULT_THRESHOLDS = {
84
86
  failOnError: {
85
87
  eolDays: 180,
@@ -274,6 +276,10 @@ function generateFindings(projects, config) {
274
276
  }
275
277
  return findings;
276
278
  }
279
+ function computeProjectId(relativePath, projectName, workspaceId) {
280
+ const input = workspaceId ? `${relativePath}:${projectName}:${workspaceId}` : `${relativePath}:${projectName}`;
281
+ return crypto.createHash("sha256").update(input).digest("hex").slice(0, 16);
282
+ }
277
283
 
278
284
  // src/version.ts
279
285
  import { createRequire } from "module";
@@ -553,10 +559,17 @@ function generatePriorityActions(artifact) {
553
559
  );
554
560
  if (eolProjects.length > 0) {
555
561
  const names = eolProjects.map((p) => p.name).join(", ");
556
- const runtimes = eolProjects.map((p) => `${p.runtime} \u2192 ${p.runtimeLatest}`).join(", ");
562
+ let detail = `End-of-life runtimes no longer receive security patches and block ecosystem upgrades.`;
563
+ const fileLines = [];
564
+ for (const p of eolProjects) {
565
+ fileLines.push(`
566
+ ./${p.path}`);
567
+ fileLines.push(` ${p.runtime} \u2192 ${p.runtimeLatest} (${p.runtimeMajorsBehind} major${p.runtimeMajorsBehind > 1 ? "s" : ""} behind)`);
568
+ }
569
+ detail += fileLines.join("");
557
570
  actions.push({
558
571
  title: `Upgrade EOL runtime${eolProjects.length > 1 ? "s" : ""} in ${names}`,
559
- explanation: `${runtimes}. End-of-life runtimes no longer receive security patches and block ecosystem upgrades.`,
572
+ explanation: detail,
560
573
  impact: `+${Math.min(eolProjects.length * 10, 30)} points (runtime & EOL scores)`,
561
574
  severity: 100
562
575
  });
@@ -565,16 +578,30 @@ function generatePriorityActions(artifact) {
565
578
  for (const p of artifact.projects) {
566
579
  for (const fw of p.frameworks) {
567
580
  if (fw.majorsBehind !== null && fw.majorsBehind >= 3) {
568
- severeFrameworks.push({ name: fw.name, fw: `${fw.currentVersion} \u2192 ${fw.latestVersion}`, behind: fw.majorsBehind, project: p.name });
581
+ severeFrameworks.push({ name: fw.name, fw: `${fw.currentVersion} \u2192 ${fw.latestVersion}`, behind: fw.majorsBehind, project: p.name, projectPath: p.path });
569
582
  }
570
583
  }
571
584
  }
572
585
  if (severeFrameworks.length > 0) {
573
586
  const worst = severeFrameworks.sort((a, b) => b.behind - a.behind)[0];
574
587
  const others = severeFrameworks.length > 1 ? ` (+${severeFrameworks.length - 1} more)` : "";
588
+ let detail = `${worst.behind} major versions behind. Major framework drift increases breaking change risk and blocks access to security fixes and performance improvements.`;
589
+ const fileLines = [];
590
+ let shown = 0;
591
+ for (const sf of severeFrameworks) {
592
+ if (shown >= 8) break;
593
+ fileLines.push(`
594
+ ./${sf.projectPath}`);
595
+ fileLines.push(` ${sf.name}: ${sf.fw} (${sf.behind} major${sf.behind > 1 ? "s" : ""} behind)`);
596
+ shown++;
597
+ }
598
+ const remaining = severeFrameworks.length - shown;
599
+ detail += fileLines.join("");
600
+ if (remaining > 0) detail += `
601
+ ... and ${remaining} more`;
575
602
  actions.push({
576
603
  title: `Upgrade ${worst.name} ${worst.fw} in ${worst.project}${others}`,
577
- explanation: `${worst.behind} major versions behind. Major framework drift increases breaking change risk and blocks access to security fixes and performance improvements.`,
604
+ explanation: detail,
578
605
  impact: `+5\u201315 points (framework score)`,
579
606
  severity: 90
580
607
  });
@@ -585,9 +612,28 @@ function generatePriorityActions(artifact) {
585
612
  if (total === 0) continue;
586
613
  const twoPlusPct = Math.round(b.twoPlusBehind / total * 100);
587
614
  if (twoPlusPct >= 40) {
615
+ let detail = `${b.twoPlusBehind} of ${total} dependencies are 2+ majors behind. Run \`npm outdated\` and prioritise packages with known CVEs or breaking API changes.`;
616
+ const worstDeps = p.dependencies.filter((d) => d.majorsBehind !== null && d.majorsBehind >= 2).sort((a, b2) => (b2.majorsBehind ?? 0) - (a.majorsBehind ?? 0));
617
+ if (worstDeps.length > 0) {
618
+ const depLines = [];
619
+ let shown = 0;
620
+ depLines.push(`
621
+ ./${p.path}`);
622
+ for (const dep of worstDeps) {
623
+ if (shown >= 8) break;
624
+ const current = dep.resolvedVersion ?? dep.currentSpec;
625
+ const latest = dep.latestStable ?? "?";
626
+ depLines.push(` ${dep.package}: ${current} \u2192 ${latest} (${dep.majorsBehind} major${dep.majorsBehind > 1 ? "s" : ""} behind)`);
627
+ shown++;
628
+ }
629
+ const remaining = worstDeps.length - shown;
630
+ detail += depLines.join("");
631
+ if (remaining > 0) detail += `
632
+ ... and ${remaining} more`;
633
+ }
588
634
  actions.push({
589
635
  title: `Reduce dependency rot in ${p.name} (${twoPlusPct}% severely outdated)`,
590
- explanation: `${b.twoPlusBehind} of ${total} dependencies are 2+ majors behind. Run \`npm outdated\` and prioritise packages with known CVEs or breaking API changes.`,
636
+ explanation: detail,
591
637
  impact: `+5\u201310 points (dependency score)`,
592
638
  severity: 80 + twoPlusPct / 10
593
639
  });
@@ -597,7 +643,7 @@ function generatePriorityActions(artifact) {
597
643
  for (const p of artifact.projects) {
598
644
  for (const fw of p.frameworks) {
599
645
  if (fw.majorsBehind === 2) {
600
- twoMajorFrameworks.push({ name: fw.name, project: p.name, fw: `${fw.currentVersion} \u2192 ${fw.latestVersion}` });
646
+ twoMajorFrameworks.push({ name: fw.name, project: p.name, projectPath: p.path, fw: `${fw.currentVersion} \u2192 ${fw.latestVersion}` });
601
647
  }
602
648
  }
603
649
  }
@@ -605,9 +651,23 @@ function generatePriorityActions(artifact) {
605
651
  if (uniqueTwo.length > 0) {
606
652
  const list = uniqueTwo.slice(0, 3).map((f) => `${f.name} (${f.fw})`).join(", ");
607
653
  const moreCount = uniqueTwo.length > 3 ? ` +${uniqueTwo.length - 3} more` : "";
654
+ let detail = `These frameworks are 2 major versions behind. Create upgrade tickets and check migration guides \u2014 the gap will widen with each new release.`;
655
+ const fileLines = [];
656
+ let shown = 0;
657
+ for (const tf of twoMajorFrameworks) {
658
+ if (shown >= 8) break;
659
+ fileLines.push(`
660
+ ./${tf.projectPath}`);
661
+ fileLines.push(` ${tf.name}: ${tf.fw}`);
662
+ shown++;
663
+ }
664
+ const remaining = twoMajorFrameworks.length - shown;
665
+ detail += fileLines.join("");
666
+ if (remaining > 0) detail += `
667
+ ... and ${remaining} more`;
608
668
  actions.push({
609
669
  title: `Plan major framework upgrades: ${list}${moreCount}`,
610
- explanation: `These frameworks are 2 major versions behind. Create upgrade tickets and check migration guides \u2014 the gap will widen with each new release.`,
670
+ explanation: detail,
611
671
  impact: `+5\u201310 points (framework score)`,
612
672
  severity: 60
613
673
  });
@@ -618,9 +678,31 @@ function generatePriorityActions(artifact) {
618
678
  if (total > 0) {
619
679
  const items = [...bc.deprecatedPackages, ...bc.legacyPolyfills].slice(0, 5).join(", ");
620
680
  const moreCount = total > 5 ? ` +${total - 5} more` : "";
681
+ let detail = `${total} package${total !== 1 ? "s" : ""} are deprecated or legacy polyfills. These receive no updates and may have known vulnerabilities.`;
682
+ const allPkgNames = /* @__PURE__ */ new Set([...bc.deprecatedPackages, ...bc.legacyPolyfills]);
683
+ const fileLines = [];
684
+ let shown = 0;
685
+ for (const p of artifact.projects) {
686
+ const matches = p.dependencies.filter((d) => allPkgNames.has(d.package));
687
+ if (matches.length === 0) continue;
688
+ if (shown >= 10) break;
689
+ fileLines.push(`
690
+ ./${p.path}`);
691
+ for (const dep of matches) {
692
+ if (shown >= 10) break;
693
+ const ver = dep.resolvedVersion ?? dep.currentSpec;
694
+ const label = bc.deprecatedPackages.includes(dep.package) ? "deprecated" : "polyfill";
695
+ fileLines.push(` ${dep.package}: ${ver} (${label})`);
696
+ shown++;
697
+ }
698
+ }
699
+ const remaining = total - shown;
700
+ detail += fileLines.join("");
701
+ if (remaining > 0) detail += `
702
+ ... and ${remaining} more`;
621
703
  actions.push({
622
704
  title: `Replace deprecated/legacy packages: ${items}${moreCount}`,
623
- explanation: `${total} package${total !== 1 ? "s" : ""} are deprecated or legacy polyfills. These receive no updates and may have known vulnerabilities.`,
705
+ explanation: detail,
624
706
  severity: 55
625
707
  });
626
708
  }
@@ -667,9 +749,20 @@ function generatePriorityActions(artifact) {
667
749
  const issues = [];
668
750
  if (sec.envFilesTracked) issues.push(".env files are tracked in git");
669
751
  if (!sec.lockfilePresent) issues.push("no lockfile found");
752
+ let detail;
753
+ if (sec.envFilesTracked) {
754
+ detail = "Environment files may contain secrets. Add them to .gitignore and rotate any exposed credentials immediately.";
755
+ detail += "\n ./.gitignore";
756
+ detail += "\n Add: .env, .env.*, .env.local";
757
+ } else {
758
+ detail = "Without a lockfile, installs are non-deterministic. Run the install command to generate one and commit it.";
759
+ detail += "\n ./";
760
+ detail += `
761
+ Missing: ${sec.lockfileTypes.length > 0 ? sec.lockfileTypes.join(", ") + " (multiple types detected)" : "package-lock.json, pnpm-lock.yaml, or yarn.lock"}`;
762
+ }
670
763
  actions.push({
671
764
  title: `Fix security posture: ${issues.join(", ")}`,
672
- explanation: sec.envFilesTracked ? "Environment files may contain secrets. Add them to .gitignore and rotate any exposed credentials immediately." : "Without a lockfile, installs are non-deterministic. Run the install command to generate one and commit it.",
765
+ explanation: detail,
673
766
  severity: 95
674
767
  });
675
768
  }
@@ -679,9 +772,22 @@ function generatePriorityActions(artifact) {
679
772
  const highImpactDupes = dupes.filter((d) => d.versions.length >= 3);
680
773
  if (highImpactDupes.length >= 3) {
681
774
  const names = highImpactDupes.slice(0, 4).map((d) => `${d.name} (${d.versions.length}v)`).join(", ");
775
+ let detail = `${highImpactDupes.length} packages have 3+ versions installed. Run \`npm dedupe\` to reduce bundle size and install time.`;
776
+ const dupeLines = [];
777
+ let shown = 0;
778
+ for (const d of highImpactDupes) {
779
+ if (shown >= 8) break;
780
+ dupeLines.push(`
781
+ ${d.name}: ${d.versions.join(", ")} (${d.consumers} consumer${d.consumers !== 1 ? "s" : ""})`);
782
+ shown++;
783
+ }
784
+ const remaining = highImpactDupes.length - shown;
785
+ detail += dupeLines.join("");
786
+ if (remaining > 0) detail += `
787
+ ... and ${remaining} more`;
682
788
  actions.push({
683
789
  title: `Deduplicate heavily-versioned packages`,
684
- explanation: `${highImpactDupes.length} packages have 3+ versions installed: ${names}. Run \`npm dedupe\` to reduce bundle size and install time.`,
790
+ explanation: detail,
685
791
  severity: 35
686
792
  });
687
793
  }
@@ -772,13 +878,146 @@ function toSarifResult(finding) {
772
878
  };
773
879
  }
774
880
 
775
- // src/commands/scan.ts
776
- import * as path12 from "path";
881
+ // src/commands/dsn.ts
882
+ import * as crypto2 from "crypto";
883
+ import * as path2 from "path";
777
884
  import { Command } from "commander";
885
+ import chalk2 from "chalk";
886
+ var REGION_HOSTS = {
887
+ us: "us.ingest.vibgrate.com",
888
+ eu: "eu.ingest.vibgrate.com"
889
+ };
890
+ function resolveIngestHost(region, ingest) {
891
+ if (ingest) {
892
+ try {
893
+ return new URL(ingest).host;
894
+ } catch {
895
+ throw new Error(`Invalid ingest URL: ${ingest}`);
896
+ }
897
+ }
898
+ const r = (region ?? "us").toLowerCase();
899
+ const host = REGION_HOSTS[r];
900
+ if (!host) {
901
+ throw new Error(`Unknown region "${r}". Supported: ${Object.keys(REGION_HOSTS).join(", ")}`);
902
+ }
903
+ return host;
904
+ }
905
+ var dsnCommand = new Command("dsn").description("Manage DSN tokens");
906
+ dsnCommand.command("create").description("Create a new DSN token").option("--ingest <url>", "Ingest API URL (overrides --region)").option("--region <region>", "Data residency region (us, eu)", "us").requiredOption("--workspace <id>", "Workspace ID").option("--write <path>", "Write DSN to file").action(async (opts) => {
907
+ const keyId = crypto2.randomBytes(8).toString("hex");
908
+ const secret = crypto2.randomBytes(32).toString("hex");
909
+ let ingestHost;
910
+ try {
911
+ ingestHost = resolveIngestHost(opts.region, opts.ingest);
912
+ } catch (e) {
913
+ console.error(chalk2.red(e instanceof Error ? e.message : String(e)));
914
+ process.exit(1);
915
+ }
916
+ const dsn = `vibgrate+https://${keyId}:${secret}@${ingestHost}/${opts.workspace}`;
917
+ console.log(chalk2.green("\u2714") + " DSN created");
918
+ console.log("");
919
+ console.log(chalk2.bold("DSN:"));
920
+ console.log(` ${dsn}`);
921
+ console.log("");
922
+ console.log(chalk2.bold("Key ID:"));
923
+ console.log(` ${keyId}`);
924
+ console.log("");
925
+ console.log(chalk2.dim("Set this as VIBGRATE_DSN in your CI environment."));
926
+ console.log(chalk2.dim("The secret must be registered on your Vibgrate ingest API."));
927
+ if (opts.write) {
928
+ const writePath = path2.resolve(opts.write);
929
+ await writeTextFile(writePath, dsn + "\n");
930
+ console.log("");
931
+ console.log(chalk2.green("\u2714") + ` DSN written to ${opts.write}`);
932
+ console.log(chalk2.yellow("\u26A0") + " Add this file to .gitignore!");
933
+ }
934
+ });
935
+
936
+ // src/commands/push.ts
937
+ import * as crypto3 from "crypto";
938
+ import * as path3 from "path";
939
+ import { Command as Command2 } from "commander";
778
940
  import chalk3 from "chalk";
941
+ function parseDsn(dsn) {
942
+ const match = dsn.match(/^vibgrate\+https:\/\/([^:]+):([^@]+)@([^/]+)\/(.+)$/);
943
+ if (!match) return null;
944
+ return {
945
+ keyId: match[1],
946
+ secret: match[2],
947
+ host: match[3],
948
+ workspaceId: match[4]
949
+ };
950
+ }
951
+ function computeHmac(body, secret) {
952
+ return crypto3.createHmac("sha256", secret).update(body).digest("base64");
953
+ }
954
+ var pushCommand = new Command2("push").description("Push scan results to Vibgrate API").option("--dsn <dsn>", "DSN token (or use VIBGRATE_DSN env)").option("--region <region>", "Override data residency region (us, eu)").option("--file <file>", "Scan artifact file", ".vibgrate/scan_result.json").option("--strict", "Fail on upload errors").action(async (opts) => {
955
+ const dsn = opts.dsn || process.env.VIBGRATE_DSN;
956
+ if (!dsn) {
957
+ console.error(chalk3.red("No DSN provided."));
958
+ console.error(chalk3.dim("Set VIBGRATE_DSN environment variable or use --dsn flag."));
959
+ if (opts.strict) process.exit(1);
960
+ return;
961
+ }
962
+ const parsed = parseDsn(dsn);
963
+ if (!parsed) {
964
+ console.error(chalk3.red("Invalid DSN format."));
965
+ console.error(chalk3.dim("Expected: vibgrate+https://<key_id>:<secret>@<host>/<workspace_id>"));
966
+ if (opts.strict) process.exit(1);
967
+ return;
968
+ }
969
+ const filePath = path3.resolve(opts.file);
970
+ if (!await pathExists(filePath)) {
971
+ console.error(chalk3.red(`Scan artifact not found: ${filePath}`));
972
+ console.error(chalk3.dim('Run "vibgrate scan" first.'));
973
+ if (opts.strict) process.exit(1);
974
+ return;
975
+ }
976
+ const body = await readTextFile(filePath);
977
+ const timestamp = String(Date.now());
978
+ const hmac = computeHmac(body, parsed.secret);
979
+ let host = parsed.host;
980
+ if (opts.region) {
981
+ try {
982
+ host = resolveIngestHost(opts.region);
983
+ } catch (e) {
984
+ console.error(chalk3.red(e instanceof Error ? e.message : String(e)));
985
+ if (opts.strict) process.exit(1);
986
+ return;
987
+ }
988
+ }
989
+ const url = `https://${host}/v1/ingest/scan`;
990
+ console.log(chalk3.dim(`Uploading to ${host}...`));
991
+ try {
992
+ const response = await fetch(url, {
993
+ method: "POST",
994
+ headers: {
995
+ "Content-Type": "application/json",
996
+ "X-Vibgrate-Timestamp": timestamp,
997
+ "Authorization": `VibgrateDSN ${parsed.keyId}:${hmac}`
998
+ },
999
+ body
1000
+ });
1001
+ if (!response.ok) {
1002
+ const text = await response.text();
1003
+ throw new Error(`HTTP ${response.status}: ${text}`);
1004
+ }
1005
+ const result = await response.json();
1006
+ console.log(chalk3.green("\u2714") + ` Uploaded successfully (${result.ingestId ?? "ok"})`);
1007
+ } catch (e) {
1008
+ const msg = e instanceof Error ? e.message : String(e);
1009
+ console.error(chalk3.red(`Upload failed: ${msg}`));
1010
+ if (opts.strict) process.exit(1);
1011
+ }
1012
+ });
1013
+
1014
+ // src/commands/scan.ts
1015
+ import * as path14 from "path";
1016
+ import { Command as Command3 } from "commander";
1017
+ import chalk5 from "chalk";
779
1018
 
780
1019
  // src/scanners/node-scanner.ts
781
- import * as path2 from "path";
1020
+ import * as path4 from "path";
782
1021
  import * as semver2 from "semver";
783
1022
 
784
1023
  // src/scanners/npm-cache.ts
@@ -793,7 +1032,7 @@ function maxStable(versions) {
793
1032
  return stable.sort(semver.rcompare)[0] ?? null;
794
1033
  }
795
1034
  async function npmViewJson(args, cwd) {
796
- return new Promise((resolve4, reject) => {
1035
+ return new Promise((resolve6, reject) => {
797
1036
  const child = spawn("npm", ["view", ...args, "--json"], {
798
1037
  cwd,
799
1038
  stdio: ["ignore", "pipe", "pipe"]
@@ -810,13 +1049,13 @@ async function npmViewJson(args, cwd) {
810
1049
  }
811
1050
  const trimmed = out.trim();
812
1051
  if (!trimmed) {
813
- resolve4(null);
1052
+ resolve6(null);
814
1053
  return;
815
1054
  }
816
1055
  try {
817
- resolve4(JSON.parse(trimmed));
1056
+ resolve6(JSON.parse(trimmed));
818
1057
  } catch {
819
- resolve4(trimmed.replace(/^"|"$/g, ""));
1058
+ resolve6(trimmed.replace(/^"|"$/g, ""));
820
1059
  }
821
1060
  });
822
1061
  });
@@ -968,8 +1207,8 @@ async function scanNodeProjects(rootDir, npmCache) {
968
1207
  }
969
1208
  async function scanOnePackageJson(packageJsonPath, rootDir, npmCache) {
970
1209
  const pj = await readJsonFile(packageJsonPath);
971
- const absProjectPath = path2.dirname(packageJsonPath);
972
- const projectPath = path2.relative(rootDir, absProjectPath) || ".";
1210
+ const absProjectPath = path4.dirname(packageJsonPath);
1211
+ const projectPath = path4.relative(rootDir, absProjectPath) || ".";
973
1212
  const nodeEngine = pj.engines?.node ?? void 0;
974
1213
  let runtimeLatest;
975
1214
  let runtimeMajorsBehind;
@@ -1051,7 +1290,7 @@ async function scanOnePackageJson(packageJsonPath, rootDir, npmCache) {
1051
1290
  return {
1052
1291
  type: "node",
1053
1292
  path: projectPath,
1054
- name: pj.name ?? path2.basename(absProjectPath),
1293
+ name: pj.name ?? path4.basename(absProjectPath),
1055
1294
  runtime: nodeEngine,
1056
1295
  runtimeLatest,
1057
1296
  runtimeMajorsBehind,
@@ -1062,7 +1301,7 @@ async function scanOnePackageJson(packageJsonPath, rootDir, npmCache) {
1062
1301
  }
1063
1302
 
1064
1303
  // src/scanners/dotnet-scanner.ts
1065
- import * as path3 from "path";
1304
+ import * as path5 from "path";
1066
1305
  import { XMLParser } from "fast-xml-parser";
1067
1306
  var parser = new XMLParser({
1068
1307
  ignoreAttributes: false,
@@ -1263,7 +1502,7 @@ function parseCsproj(xml, filePath) {
1263
1502
  const parsed = parser.parse(xml);
1264
1503
  const project = parsed?.Project;
1265
1504
  if (!project) {
1266
- return { targetFrameworks: [], packageReferences: [], projectName: path3.basename(filePath, ".csproj") };
1505
+ return { targetFrameworks: [], packageReferences: [], projectName: path5.basename(filePath, ".csproj") };
1267
1506
  }
1268
1507
  const propertyGroups = Array.isArray(project.PropertyGroup) ? project.PropertyGroup : project.PropertyGroup ? [project.PropertyGroup] : [];
1269
1508
  const targetFrameworks = [];
@@ -1291,7 +1530,7 @@ function parseCsproj(xml, filePath) {
1291
1530
  return {
1292
1531
  targetFrameworks: [...new Set(targetFrameworks)],
1293
1532
  packageReferences,
1294
- projectName: path3.basename(filePath, ".csproj")
1533
+ projectName: path5.basename(filePath, ".csproj")
1295
1534
  };
1296
1535
  }
1297
1536
  async function scanDotnetProjects(rootDir) {
@@ -1301,12 +1540,12 @@ async function scanDotnetProjects(rootDir) {
1301
1540
  for (const slnPath of slnFiles) {
1302
1541
  try {
1303
1542
  const slnContent = await readTextFile(slnPath);
1304
- const slnDir = path3.dirname(slnPath);
1543
+ const slnDir = path5.dirname(slnPath);
1305
1544
  const projectRegex = /Project\("[^"]*"\)\s*=\s*"[^"]*",\s*"([^"]+\.csproj)"/g;
1306
1545
  let match;
1307
1546
  while ((match = projectRegex.exec(slnContent)) !== null) {
1308
1547
  if (match[1]) {
1309
- const csprojPath = path3.resolve(slnDir, match[1].replace(/\\/g, "/"));
1548
+ const csprojPath = path5.resolve(slnDir, match[1].replace(/\\/g, "/"));
1310
1549
  slnCsprojPaths.add(csprojPath);
1311
1550
  }
1312
1551
  }
@@ -1362,7 +1601,7 @@ async function scanOneCsproj(csprojPath, rootDir) {
1362
1601
  const buckets = { current: 0, oneBehind: 0, twoPlusBehind: 0, unknown: dependencies.length };
1363
1602
  return {
1364
1603
  type: "dotnet",
1365
- path: path3.relative(rootDir, path3.dirname(csprojPath)) || ".",
1604
+ path: path5.relative(rootDir, path5.dirname(csprojPath)) || ".",
1366
1605
  name: data.projectName,
1367
1606
  targetFramework,
1368
1607
  runtime: primaryTfm,
@@ -1394,7 +1633,7 @@ var Semaphore = class {
1394
1633
  this.available--;
1395
1634
  return Promise.resolve();
1396
1635
  }
1397
- return new Promise((resolve4) => this.queue.push(resolve4));
1636
+ return new Promise((resolve6) => this.queue.push(resolve6));
1398
1637
  }
1399
1638
  release() {
1400
1639
  const next = this.queue.shift();
@@ -1404,7 +1643,7 @@ var Semaphore = class {
1404
1643
  };
1405
1644
 
1406
1645
  // src/config.ts
1407
- import * as path4 from "path";
1646
+ import * as path6 from "path";
1408
1647
  import * as fs2 from "fs/promises";
1409
1648
  var CONFIG_FILES = [
1410
1649
  "vibgrate.config.ts",
@@ -1427,7 +1666,7 @@ var DEFAULT_CONFIG = {
1427
1666
  };
1428
1667
  async function loadConfig(rootDir) {
1429
1668
  for (const file of CONFIG_FILES) {
1430
- const configPath = path4.join(rootDir, file);
1669
+ const configPath = path6.join(rootDir, file);
1431
1670
  if (await pathExists(configPath)) {
1432
1671
  if (file.endsWith(".json")) {
1433
1672
  const txt = await readTextFile(configPath);
@@ -1443,7 +1682,7 @@ async function loadConfig(rootDir) {
1443
1682
  return DEFAULT_CONFIG;
1444
1683
  }
1445
1684
  async function writeDefaultConfig(rootDir) {
1446
- const configPath = path4.join(rootDir, "vibgrate.config.ts");
1685
+ const configPath = path6.join(rootDir, "vibgrate.config.ts");
1447
1686
  const content = `import type { VibgrateConfig } from '@vibgrate/cli';
1448
1687
 
1449
1688
  const config: VibgrateConfig = {
@@ -1468,7 +1707,7 @@ export default config;
1468
1707
  }
1469
1708
 
1470
1709
  // src/utils/vcs.ts
1471
- import * as path5 from "path";
1710
+ import * as path7 from "path";
1472
1711
  import * as fs3 from "fs/promises";
1473
1712
  async function detectVcs(rootDir) {
1474
1713
  try {
@@ -1482,7 +1721,7 @@ async function detectGit(rootDir) {
1482
1721
  if (!gitDir) {
1483
1722
  return { type: "unknown" };
1484
1723
  }
1485
- const headPath = path5.join(gitDir, "HEAD");
1724
+ const headPath = path7.join(gitDir, "HEAD");
1486
1725
  let headContent;
1487
1726
  try {
1488
1727
  headContent = (await fs3.readFile(headPath, "utf8")).trim();
@@ -1506,10 +1745,10 @@ async function detectGit(rootDir) {
1506
1745
  };
1507
1746
  }
1508
1747
  async function findGitDir(startDir) {
1509
- let dir = path5.resolve(startDir);
1510
- const root = path5.parse(dir).root;
1748
+ let dir = path7.resolve(startDir);
1749
+ const root = path7.parse(dir).root;
1511
1750
  while (dir !== root) {
1512
- const gitPath = path5.join(dir, ".git");
1751
+ const gitPath = path7.join(dir, ".git");
1513
1752
  try {
1514
1753
  const stat3 = await fs3.stat(gitPath);
1515
1754
  if (stat3.isDirectory()) {
@@ -1518,18 +1757,18 @@ async function findGitDir(startDir) {
1518
1757
  if (stat3.isFile()) {
1519
1758
  const content = (await fs3.readFile(gitPath, "utf8")).trim();
1520
1759
  if (content.startsWith("gitdir: ")) {
1521
- const resolved = path5.resolve(dir, content.slice(8));
1760
+ const resolved = path7.resolve(dir, content.slice(8));
1522
1761
  return resolved;
1523
1762
  }
1524
1763
  }
1525
1764
  } catch {
1526
1765
  }
1527
- dir = path5.dirname(dir);
1766
+ dir = path7.dirname(dir);
1528
1767
  }
1529
1768
  return null;
1530
1769
  }
1531
1770
  async function resolveRef(gitDir, refPath) {
1532
- const loosePath = path5.join(gitDir, refPath);
1771
+ const loosePath = path7.join(gitDir, refPath);
1533
1772
  try {
1534
1773
  const sha = (await fs3.readFile(loosePath, "utf8")).trim();
1535
1774
  if (/^[0-9a-f]{40}$/i.test(sha)) {
@@ -1537,7 +1776,7 @@ async function resolveRef(gitDir, refPath) {
1537
1776
  }
1538
1777
  } catch {
1539
1778
  }
1540
- const packedPath = path5.join(gitDir, "packed-refs");
1779
+ const packedPath = path7.join(gitDir, "packed-refs");
1541
1780
  try {
1542
1781
  const packed = await fs3.readFile(packedPath, "utf8");
1543
1782
  for (const line of packed.split("\n")) {
@@ -1553,16 +1792,16 @@ async function resolveRef(gitDir, refPath) {
1553
1792
  }
1554
1793
 
1555
1794
  // src/ui/progress.ts
1556
- import chalk2 from "chalk";
1795
+ import chalk4 from "chalk";
1557
1796
  var ROBOT = [
1558
- chalk2.cyan(" \u256D\u2500\u2500\u2500\u256E") + chalk2.greenBright("\u279C"),
1559
- chalk2.cyan(" \u256D\u2524") + chalk2.greenBright("\u25C9 \u25C9") + chalk2.cyan("\u251C\u256E"),
1560
- chalk2.cyan(" \u2570\u2524") + chalk2.dim("\u2500\u2500\u2500") + chalk2.cyan("\u251C\u256F"),
1561
- chalk2.cyan(" \u2570\u2500\u2500\u2500\u256F")
1797
+ chalk4.cyan(" \u256D\u2500\u2500\u2500\u256E") + chalk4.greenBright("\u279C"),
1798
+ chalk4.cyan(" \u256D\u2524") + chalk4.greenBright("\u25C9 \u25C9") + chalk4.cyan("\u251C\u256E"),
1799
+ chalk4.cyan(" \u2570\u2524") + chalk4.dim("\u2500\u2500\u2500") + chalk4.cyan("\u251C\u256F"),
1800
+ chalk4.cyan(" \u2570\u2500\u2500\u2500\u256F")
1562
1801
  ];
1563
1802
  var BRAND = [
1564
- chalk2.bold.white(" V I B G R A T E"),
1565
- chalk2.dim(` Drift Intelligence Engine`) + chalk2.dim(` v${VERSION}`)
1803
+ chalk4.bold.white(" V I B G R A T E"),
1804
+ chalk4.dim(` Drift Intelligence Engine`) + chalk4.dim(` v${VERSION}`)
1566
1805
  ];
1567
1806
  var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1568
1807
  var ScanProgress = class {
@@ -1656,7 +1895,7 @@ var ScanProgress = class {
1656
1895
  const elapsed = ((Date.now() - this.startTime) / 1e3).toFixed(1);
1657
1896
  const doneCount = this.steps.filter((s) => s.status === "done").length;
1658
1897
  process.stderr.write(
1659
- chalk2.dim(` \u2714 ${doneCount} scanners completed in ${elapsed}s
1898
+ chalk4.dim(` \u2714 ${doneCount} scanners completed in ${elapsed}s
1660
1899
 
1661
1900
  `)
1662
1901
  );
@@ -1688,16 +1927,16 @@ var ScanProgress = class {
1688
1927
  lines.push(` ${ROBOT[0]} ${BRAND[0]}`);
1689
1928
  lines.push(` ${ROBOT[1]} ${BRAND[1]}`);
1690
1929
  lines.push(` ${ROBOT[2]}`);
1691
- lines.push(` ${ROBOT[3]} ${chalk2.dim(this.rootDir)}`);
1930
+ lines.push(` ${ROBOT[3]} ${chalk4.dim(this.rootDir)}`);
1692
1931
  lines.push("");
1693
1932
  const totalSteps = this.steps.length;
1694
1933
  const doneSteps = this.steps.filter((s) => s.status === "done" || s.status === "skipped").length;
1695
1934
  const pct = totalSteps > 0 ? Math.round(doneSteps / totalSteps * 100) : 0;
1696
1935
  const barWidth = 30;
1697
1936
  const filled = Math.round(doneSteps / Math.max(totalSteps, 1) * barWidth);
1698
- const bar = chalk2.greenBright("\u2501".repeat(filled)) + chalk2.dim("\u254C".repeat(barWidth - filled));
1937
+ const bar = chalk4.greenBright("\u2501".repeat(filled)) + chalk4.dim("\u254C".repeat(barWidth - filled));
1699
1938
  const elapsed = ((Date.now() - this.startTime) / 1e3).toFixed(1);
1700
- lines.push(` ${bar} ${chalk2.bold.white(`${pct}%`)} ${chalk2.dim(`${elapsed}s`)}`);
1939
+ lines.push(` ${bar} ${chalk4.bold.white(`${pct}%`)} ${chalk4.dim(`${elapsed}s`)}`);
1701
1940
  lines.push("");
1702
1941
  for (const step of this.steps) {
1703
1942
  lines.push(this.renderStep(step));
@@ -1716,27 +1955,27 @@ var ScanProgress = class {
1716
1955
  let detail = "";
1717
1956
  switch (step.status) {
1718
1957
  case "done":
1719
- icon = chalk2.green("\u2714");
1720
- label = chalk2.white(step.label);
1958
+ icon = chalk4.green("\u2714");
1959
+ label = chalk4.white(step.label);
1721
1960
  break;
1722
1961
  case "active":
1723
- icon = chalk2.cyan(spinner);
1724
- label = chalk2.bold.white(step.label);
1962
+ icon = chalk4.cyan(spinner);
1963
+ label = chalk4.bold.white(step.label);
1725
1964
  break;
1726
1965
  case "skipped":
1727
- icon = chalk2.dim("\u25CC");
1728
- label = chalk2.dim.strikethrough(step.label);
1966
+ icon = chalk4.dim("\u25CC");
1967
+ label = chalk4.dim.strikethrough(step.label);
1729
1968
  break;
1730
1969
  default:
1731
- icon = chalk2.dim("\u25CB");
1732
- label = chalk2.dim(step.label);
1970
+ icon = chalk4.dim("\u25CB");
1971
+ label = chalk4.dim(step.label);
1733
1972
  break;
1734
1973
  }
1735
1974
  if (step.detail) {
1736
- detail = chalk2.dim(` \xB7 ${step.detail}`);
1975
+ detail = chalk4.dim(` \xB7 ${step.detail}`);
1737
1976
  }
1738
1977
  if (step.count !== void 0 && step.count > 0) {
1739
- detail += chalk2.cyan(` (${step.count})`);
1978
+ detail += chalk4.cyan(` (${step.count})`);
1740
1979
  }
1741
1980
  return ` ${icon} ${label}${detail}`;
1742
1981
  }
@@ -1748,18 +1987,18 @@ var ScanProgress = class {
1748
1987
  const e = this.stats.findings.errors;
1749
1988
  const n = this.stats.findings.notes;
1750
1989
  const parts = [
1751
- chalk2.bold.white(` ${p}`) + chalk2.dim(` project${p !== 1 ? "s" : ""}`),
1752
- chalk2.white(`${d}`) + chalk2.dim(` dep${d !== 1 ? "s" : ""}`),
1753
- chalk2.white(`${f}`) + chalk2.dim(` framework${f !== 1 ? "s" : ""}`)
1990
+ chalk4.bold.white(` ${p}`) + chalk4.dim(` project${p !== 1 ? "s" : ""}`),
1991
+ chalk4.white(`${d}`) + chalk4.dim(` dep${d !== 1 ? "s" : ""}`),
1992
+ chalk4.white(`${f}`) + chalk4.dim(` framework${f !== 1 ? "s" : ""}`)
1754
1993
  ];
1755
1994
  const findingParts = [];
1756
- if (e > 0) findingParts.push(chalk2.red(`${e} \u2716`));
1757
- if (w > 0) findingParts.push(chalk2.yellow(`${w} \u26A0`));
1758
- if (n > 0) findingParts.push(chalk2.blue(`${n} \u2139`));
1995
+ if (e > 0) findingParts.push(chalk4.red(`${e} \u2716`));
1996
+ if (w > 0) findingParts.push(chalk4.yellow(`${w} \u26A0`));
1997
+ if (n > 0) findingParts.push(chalk4.blue(`${n} \u2139`));
1759
1998
  if (findingParts.length > 0) {
1760
- parts.push(findingParts.join(chalk2.dim(" \xB7 ")));
1999
+ parts.push(findingParts.join(chalk4.dim(" \xB7 ")));
1761
2000
  }
1762
- return ` ${chalk2.dim("\u2503")} ${parts.join(chalk2.dim(" \u2502 "))}`;
2001
+ return ` ${chalk4.dim("\u2503")} ${parts.join(chalk4.dim(" \u2502 "))}`;
1763
2002
  }
1764
2003
  /** Simple CI-friendly output (no ANSI rewriting) */
1765
2004
  lastCIStep = null;
@@ -1778,7 +2017,7 @@ var ScanProgress = class {
1778
2017
  };
1779
2018
 
1780
2019
  // src/scanners/platform-matrix.ts
1781
- import * as path6 from "path";
2020
+ import * as path8 from "path";
1782
2021
  var NATIVE_MODULE_PACKAGES = /* @__PURE__ */ new Set([
1783
2022
  // Image / media processing
1784
2023
  "sharp",
@@ -2055,7 +2294,7 @@ async function scanPlatformMatrix(rootDir) {
2055
2294
  }
2056
2295
  result.dockerBaseImages = [...baseImages].sort();
2057
2296
  for (const file of [".nvmrc", ".node-version", ".tool-versions"]) {
2058
- if (await pathExists(path6.join(rootDir, file))) {
2297
+ if (await pathExists(path8.join(rootDir, file))) {
2059
2298
  result.nodeVersionFiles.push(file);
2060
2299
  }
2061
2300
  }
@@ -2131,7 +2370,7 @@ function scanDependencyRisk(projects) {
2131
2370
  }
2132
2371
 
2133
2372
  // src/scanners/dependency-graph.ts
2134
- import * as path7 from "path";
2373
+ import * as path9 from "path";
2135
2374
  function parsePnpmLock(content) {
2136
2375
  const entries = [];
2137
2376
  const regex = /^\s+\/?(@?[^@\s][^@\s]*?)@(\d+\.\d+\.\d+[^:\s]*)\s*:/gm;
@@ -2190,9 +2429,9 @@ async function scanDependencyGraph(rootDir) {
2190
2429
  phantomDependencies: []
2191
2430
  };
2192
2431
  let entries = [];
2193
- const pnpmLock = path7.join(rootDir, "pnpm-lock.yaml");
2194
- const npmLock = path7.join(rootDir, "package-lock.json");
2195
- const yarnLock = path7.join(rootDir, "yarn.lock");
2432
+ const pnpmLock = path9.join(rootDir, "pnpm-lock.yaml");
2433
+ const npmLock = path9.join(rootDir, "package-lock.json");
2434
+ const yarnLock = path9.join(rootDir, "yarn.lock");
2196
2435
  if (await pathExists(pnpmLock)) {
2197
2436
  result.lockfileType = "pnpm";
2198
2437
  const content = await readTextFile(pnpmLock);
@@ -2237,7 +2476,7 @@ async function scanDependencyGraph(rootDir) {
2237
2476
  for (const pjPath of pkgFiles) {
2238
2477
  try {
2239
2478
  const pj = await readJsonFile(pjPath);
2240
- const relPath = path7.relative(rootDir, pjPath);
2479
+ const relPath = path9.relative(rootDir, pjPath);
2241
2480
  for (const section of ["dependencies", "devDependencies"]) {
2242
2481
  const deps = pj[section];
2243
2482
  if (!deps) continue;
@@ -2583,7 +2822,7 @@ function scanToolingInventory(projects) {
2583
2822
  }
2584
2823
 
2585
2824
  // src/scanners/build-deploy.ts
2586
- import * as path8 from "path";
2825
+ import * as path10 from "path";
2587
2826
  var CI_FILES = {
2588
2827
  ".github/workflows": "github-actions",
2589
2828
  ".gitlab-ci.yml": "gitlab-ci",
@@ -2633,12 +2872,12 @@ async function scanBuildDeploy(rootDir) {
2633
2872
  };
2634
2873
  const ciSystems = /* @__PURE__ */ new Set();
2635
2874
  for (const [file, system] of Object.entries(CI_FILES)) {
2636
- const fullPath = path8.join(rootDir, file);
2875
+ const fullPath = path10.join(rootDir, file);
2637
2876
  if (await pathExists(fullPath)) {
2638
2877
  ciSystems.add(system);
2639
2878
  }
2640
2879
  }
2641
- const ghWorkflowDir = path8.join(rootDir, ".github", "workflows");
2880
+ const ghWorkflowDir = path10.join(rootDir, ".github", "workflows");
2642
2881
  if (await pathExists(ghWorkflowDir)) {
2643
2882
  try {
2644
2883
  const files = await findFiles(
@@ -2686,11 +2925,11 @@ async function scanBuildDeploy(rootDir) {
2686
2925
  (name) => name.endsWith(".cfn.json") || name.endsWith(".cfn.yaml")
2687
2926
  );
2688
2927
  if (cfnFiles.length > 0) iacSystems.add("cloudformation");
2689
- if (await pathExists(path8.join(rootDir, "Pulumi.yaml"))) iacSystems.add("pulumi");
2928
+ if (await pathExists(path10.join(rootDir, "Pulumi.yaml"))) iacSystems.add("pulumi");
2690
2929
  result.iac = [...iacSystems].sort();
2691
2930
  const releaseTools = /* @__PURE__ */ new Set();
2692
2931
  for (const [file, tool] of Object.entries(RELEASE_FILES)) {
2693
- if (await pathExists(path8.join(rootDir, file))) releaseTools.add(tool);
2932
+ if (await pathExists(path10.join(rootDir, file))) releaseTools.add(tool);
2694
2933
  }
2695
2934
  const pkgFiles = await findPackageJsonFiles(rootDir);
2696
2935
  for (const pjPath of pkgFiles) {
@@ -2715,19 +2954,19 @@ async function scanBuildDeploy(rootDir) {
2715
2954
  };
2716
2955
  const managers = /* @__PURE__ */ new Set();
2717
2956
  for (const [file, manager] of Object.entries(lockfileMap)) {
2718
- if (await pathExists(path8.join(rootDir, file))) managers.add(manager);
2957
+ if (await pathExists(path10.join(rootDir, file))) managers.add(manager);
2719
2958
  }
2720
2959
  result.packageManagers = [...managers].sort();
2721
2960
  const monoTools = /* @__PURE__ */ new Set();
2722
2961
  for (const [file, tool] of Object.entries(MONOREPO_FILES)) {
2723
- if (await pathExists(path8.join(rootDir, file))) monoTools.add(tool);
2962
+ if (await pathExists(path10.join(rootDir, file))) monoTools.add(tool);
2724
2963
  }
2725
2964
  result.monorepoTools = [...monoTools].sort();
2726
2965
  return result;
2727
2966
  }
2728
2967
 
2729
2968
  // src/scanners/ts-modernity.ts
2730
- import * as path9 from "path";
2969
+ import * as path11 from "path";
2731
2970
  async function scanTsModernity(rootDir) {
2732
2971
  const result = {
2733
2972
  typescriptVersion: null,
@@ -2765,7 +3004,7 @@ async function scanTsModernity(rootDir) {
2765
3004
  if (hasEsm && hasCjs) result.moduleType = "mixed";
2766
3005
  else if (hasEsm) result.moduleType = "esm";
2767
3006
  else if (hasCjs) result.moduleType = "cjs";
2768
- let tsConfigPath = path9.join(rootDir, "tsconfig.json");
3007
+ let tsConfigPath = path11.join(rootDir, "tsconfig.json");
2769
3008
  if (!await pathExists(tsConfigPath)) {
2770
3009
  const tsConfigs = await findFiles(rootDir, (name) => name === "tsconfig.json");
2771
3010
  if (tsConfigs.length > 0) {
@@ -3111,7 +3350,7 @@ function scanBreakingChangeExposure(projects) {
3111
3350
 
3112
3351
  // src/scanners/file-hotspots.ts
3113
3352
  import * as fs4 from "fs/promises";
3114
- import * as path10 from "path";
3353
+ import * as path12 from "path";
3115
3354
  var SKIP_DIRS2 = /* @__PURE__ */ new Set([
3116
3355
  "node_modules",
3117
3356
  ".git",
@@ -3166,15 +3405,15 @@ async function scanFileHotspots(rootDir) {
3166
3405
  for (const e of entries) {
3167
3406
  if (e.isDirectory) {
3168
3407
  if (SKIP_DIRS2.has(e.name)) continue;
3169
- await walk(path10.join(dir, e.name), depth + 1);
3408
+ await walk(path12.join(dir, e.name), depth + 1);
3170
3409
  } else if (e.isFile) {
3171
- const ext = path10.extname(e.name).toLowerCase();
3410
+ const ext = path12.extname(e.name).toLowerCase();
3172
3411
  if (SKIP_EXTENSIONS.has(ext)) continue;
3173
3412
  extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
3174
3413
  try {
3175
- const stat3 = await fs4.stat(path10.join(dir, e.name));
3414
+ const stat3 = await fs4.stat(path12.join(dir, e.name));
3176
3415
  allFiles.push({
3177
- path: path10.relative(rootDir, path10.join(dir, e.name)),
3416
+ path: path12.relative(rootDir, path12.join(dir, e.name)),
3178
3417
  bytes: stat3.size
3179
3418
  });
3180
3419
  } catch {
@@ -3196,7 +3435,7 @@ async function scanFileHotspots(rootDir) {
3196
3435
  }
3197
3436
 
3198
3437
  // src/scanners/security-posture.ts
3199
- import * as path11 from "path";
3438
+ import * as path13 from "path";
3200
3439
  var LOCKFILES = {
3201
3440
  "pnpm-lock.yaml": "pnpm",
3202
3441
  "package-lock.json": "npm",
@@ -3215,14 +3454,14 @@ async function scanSecurityPosture(rootDir) {
3215
3454
  };
3216
3455
  const foundLockfiles = [];
3217
3456
  for (const [file, type] of Object.entries(LOCKFILES)) {
3218
- if (await pathExists(path11.join(rootDir, file))) {
3457
+ if (await pathExists(path13.join(rootDir, file))) {
3219
3458
  foundLockfiles.push(type);
3220
3459
  }
3221
3460
  }
3222
3461
  result.lockfilePresent = foundLockfiles.length > 0;
3223
3462
  result.multipleLockfileTypes = foundLockfiles.length > 1;
3224
3463
  result.lockfileTypes = foundLockfiles.sort();
3225
- const gitignorePath = path11.join(rootDir, ".gitignore");
3464
+ const gitignorePath = path13.join(rootDir, ".gitignore");
3226
3465
  if (await pathExists(gitignorePath)) {
3227
3466
  try {
3228
3467
  const content = await readTextFile(gitignorePath);
@@ -3237,7 +3476,7 @@ async function scanSecurityPosture(rootDir) {
3237
3476
  }
3238
3477
  }
3239
3478
  for (const envFile of [".env", ".env.local", ".env.development", ".env.production"]) {
3240
- if (await pathExists(path11.join(rootDir, envFile))) {
3479
+ if (await pathExists(path13.join(rootDir, envFile))) {
3241
3480
  if (!result.gitignoreCoversEnv) {
3242
3481
  result.envFilesTracked = true;
3243
3482
  break;
@@ -3715,6 +3954,13 @@ async function runScan(rootDir, opts) {
3715
3954
  progress.addProjects(dotnetProjects.length);
3716
3955
  progress.completeStep("dotnet", `${dotnetProjects.length} project${dotnetProjects.length !== 1 ? "s" : ""}`, dotnetProjects.length);
3717
3956
  const allProjects = [...nodeProjects, ...dotnetProjects];
3957
+ const dsn = opts.dsn || process.env.VIBGRATE_DSN;
3958
+ const parsedDsn = dsn ? parseDsn(dsn) : null;
3959
+ const workspaceId = parsedDsn?.workspaceId;
3960
+ for (const project of allProjects) {
3961
+ project.drift = computeDriftScore([project]);
3962
+ project.projectId = computeProjectId(project.path, project.name, workspaceId);
3963
+ }
3718
3964
  const extended = {};
3719
3965
  if (scanners !== false) {
3720
3966
  const scannerTasks = [];
@@ -3852,7 +4098,7 @@ async function runScan(rootDir, opts) {
3852
4098
  progress.completeStep("findings", findingParts.join(", ") || "none");
3853
4099
  progress.finish();
3854
4100
  if (allProjects.length === 0) {
3855
- console.log(chalk3.yellow("No projects found."));
4101
+ console.log(chalk5.yellow("No projects found."));
3856
4102
  }
3857
4103
  if (extended.fileHotspots) filesScanned += extended.fileHotspots.totalFiles;
3858
4104
  if (extended.securityPosture) filesScanned += 1;
@@ -3867,7 +4113,7 @@ async function runScan(rootDir, opts) {
3867
4113
  schemaVersion: "1.0",
3868
4114
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3869
4115
  vibgrateVersion: VERSION,
3870
- rootPath: path12.basename(rootDir),
4116
+ rootPath: path14.basename(rootDir),
3871
4117
  ...vcs.type !== "unknown" ? { vcs } : {},
3872
4118
  projects: allProjects,
3873
4119
  drift,
@@ -3877,25 +4123,44 @@ async function runScan(rootDir, opts) {
3877
4123
  filesScanned
3878
4124
  };
3879
4125
  if (opts.baseline) {
3880
- const baselinePath = path12.resolve(opts.baseline);
4126
+ const baselinePath = path14.resolve(opts.baseline);
3881
4127
  if (await pathExists(baselinePath)) {
3882
4128
  try {
3883
4129
  const baseline = await readJsonFile(baselinePath);
3884
4130
  artifact.baseline = baselinePath;
3885
4131
  artifact.delta = artifact.drift.score - baseline.drift.score;
3886
4132
  } catch {
3887
- console.error(chalk3.yellow(`Warning: Could not read baseline file: ${baselinePath}`));
4133
+ console.error(chalk5.yellow(`Warning: Could not read baseline file: ${baselinePath}`));
3888
4134
  }
3889
4135
  }
3890
4136
  }
3891
- const vibgrateDir = path12.join(rootDir, ".vibgrate");
4137
+ const vibgrateDir = path14.join(rootDir, ".vibgrate");
3892
4138
  await ensureDir(vibgrateDir);
3893
- await writeJsonFile(path12.join(vibgrateDir, "scan_result.json"), artifact);
4139
+ await writeJsonFile(path14.join(vibgrateDir, "scan_result.json"), artifact);
4140
+ for (const project of allProjects) {
4141
+ if (project.drift && project.path) {
4142
+ const projectDir = path14.resolve(rootDir, project.path);
4143
+ const projectVibgrateDir = path14.join(projectDir, ".vibgrate");
4144
+ await ensureDir(projectVibgrateDir);
4145
+ await writeJsonFile(path14.join(projectVibgrateDir, "project_score.json"), {
4146
+ projectId: project.projectId,
4147
+ name: project.name,
4148
+ type: project.type,
4149
+ path: project.path,
4150
+ score: project.drift.score,
4151
+ riskLevel: project.drift.riskLevel,
4152
+ components: project.drift.components,
4153
+ measured: project.drift.measured,
4154
+ scannedAt: artifact.timestamp,
4155
+ vibgrateVersion: VERSION
4156
+ });
4157
+ }
4158
+ }
3894
4159
  if (opts.format === "json") {
3895
4160
  const jsonStr = JSON.stringify(artifact, null, 2);
3896
4161
  if (opts.out) {
3897
- await writeTextFile(path12.resolve(opts.out), jsonStr);
3898
- console.log(chalk3.green("\u2714") + ` JSON written to ${opts.out}`);
4162
+ await writeTextFile(path14.resolve(opts.out), jsonStr);
4163
+ console.log(chalk5.green("\u2714") + ` JSON written to ${opts.out}`);
3899
4164
  } else {
3900
4165
  console.log(jsonStr);
3901
4166
  }
@@ -3903,8 +4168,8 @@ async function runScan(rootDir, opts) {
3903
4168
  const sarif = formatSarif(artifact);
3904
4169
  const sarifStr = JSON.stringify(sarif, null, 2);
3905
4170
  if (opts.out) {
3906
- await writeTextFile(path12.resolve(opts.out), sarifStr);
3907
- console.log(chalk3.green("\u2714") + ` SARIF written to ${opts.out}`);
4171
+ await writeTextFile(path14.resolve(opts.out), sarifStr);
4172
+ console.log(chalk5.green("\u2714") + ` SARIF written to ${opts.out}`);
3908
4173
  } else {
3909
4174
  console.log(sarifStr);
3910
4175
  }
@@ -3912,15 +4177,66 @@ async function runScan(rootDir, opts) {
3912
4177
  const text = formatText(artifact);
3913
4178
  console.log(text);
3914
4179
  if (opts.out) {
3915
- await writeTextFile(path12.resolve(opts.out), text);
4180
+ await writeTextFile(path14.resolve(opts.out), text);
3916
4181
  }
3917
4182
  }
3918
4183
  return artifact;
3919
4184
  }
3920
- var scanCommand = new Command("scan").description("Scan a project for upgrade drift").argument("[path]", "Path to scan", ".").option("--out <file>", "Output file path").option("--format <format>", "Output format (text|json|sarif)", "text").option("--fail-on <level>", "Fail on warn or error").option("--baseline <file>", "Compare against baseline").option("--changed-only", "Only scan changed files").option("--concurrency <n>", "Max concurrent npm calls", "8").action(async (targetPath, opts) => {
3921
- const rootDir = path12.resolve(targetPath);
4185
+ async function autoPush(artifact, rootDir, opts) {
4186
+ const dsn = opts.dsn || process.env.VIBGRATE_DSN;
4187
+ if (!dsn) {
4188
+ console.error(chalk5.red("No DSN provided for push."));
4189
+ console.error(chalk5.dim("Set VIBGRATE_DSN environment variable or use --dsn flag."));
4190
+ if (opts.strict) process.exit(1);
4191
+ return;
4192
+ }
4193
+ const parsed = parseDsn(dsn);
4194
+ if (!parsed) {
4195
+ console.error(chalk5.red("Invalid DSN format."));
4196
+ if (opts.strict) process.exit(1);
4197
+ return;
4198
+ }
4199
+ const body = JSON.stringify(artifact);
4200
+ const timestamp = String(Date.now());
4201
+ const hmac = computeHmac(body, parsed.secret);
4202
+ let host = parsed.host;
4203
+ if (opts.region) {
4204
+ try {
4205
+ host = resolveIngestHost(opts.region);
4206
+ } catch (e) {
4207
+ console.error(chalk5.red(e instanceof Error ? e.message : String(e)));
4208
+ if (opts.strict) process.exit(1);
4209
+ return;
4210
+ }
4211
+ }
4212
+ const url = `https://${host}/v1/ingest/scan`;
4213
+ console.log(chalk5.dim(`Uploading to ${host}...`));
4214
+ try {
4215
+ const response = await fetch(url, {
4216
+ method: "POST",
4217
+ headers: {
4218
+ "Content-Type": "application/json",
4219
+ "X-Vibgrate-Timestamp": timestamp,
4220
+ "Authorization": `VibgrateDSN ${parsed.keyId}:${hmac}`
4221
+ },
4222
+ body
4223
+ });
4224
+ if (!response.ok) {
4225
+ const text = await response.text();
4226
+ throw new Error(`HTTP ${response.status}: ${text}`);
4227
+ }
4228
+ const result = await response.json();
4229
+ console.log(chalk5.green("\u2714") + ` Uploaded successfully (${result.ingestId ?? "ok"})`);
4230
+ } catch (e) {
4231
+ const msg = e instanceof Error ? e.message : String(e);
4232
+ console.error(chalk5.red(`Upload failed: ${msg}`));
4233
+ if (opts.strict) process.exit(1);
4234
+ }
4235
+ }
4236
+ var scanCommand = new Command3("scan").description("Scan a project for upgrade drift").argument("[path]", "Path to scan", ".").option("--out <file>", "Output file path").option("--format <format>", "Output format (text|json|sarif)", "text").option("--fail-on <level>", "Fail on warn or error").option("--baseline <file>", "Compare against baseline").option("--changed-only", "Only scan changed files").option("--concurrency <n>", "Max concurrent npm calls", "8").option("--push", "Auto-push results to Vibgrate API after scan").option("--dsn <dsn>", "DSN token for push (or use VIBGRATE_DSN env)").option("--region <region>", "Override data residency region for push (us, eu)").option("--strict", "Fail on push errors").action(async (targetPath, opts) => {
4237
+ const rootDir = path14.resolve(targetPath);
3922
4238
  if (!await pathExists(rootDir)) {
3923
- console.error(chalk3.red(`Path does not exist: ${rootDir}`));
4239
+ console.error(chalk5.red(`Path does not exist: ${rootDir}`));
3924
4240
  process.exit(1);
3925
4241
  }
3926
4242
  const scanOpts = {
@@ -3929,38 +4245,45 @@ var scanCommand = new Command("scan").description("Scan a project for upgrade dr
3929
4245
  failOn: opts.failOn,
3930
4246
  baseline: opts.baseline,
3931
4247
  changedOnly: opts.changedOnly,
3932
- concurrency: parseInt(opts.concurrency, 10) || 8
4248
+ concurrency: parseInt(opts.concurrency, 10) || 8,
4249
+ push: opts.push,
4250
+ dsn: opts.dsn,
4251
+ region: opts.region,
4252
+ strict: opts.strict
3933
4253
  };
3934
4254
  const artifact = await runScan(rootDir, scanOpts);
3935
4255
  if (opts.failOn) {
3936
4256
  const hasErrors = artifact.findings.some((f) => f.level === "error");
3937
4257
  const hasWarnings = artifact.findings.some((f) => f.level === "warning");
3938
4258
  if (opts.failOn === "error" && hasErrors) {
3939
- console.error(chalk3.red(`
4259
+ console.error(chalk5.red(`
3940
4260
  Failing: ${artifact.findings.filter((f) => f.level === "error").length} error finding(s) detected.`));
3941
4261
  process.exit(2);
3942
4262
  }
3943
4263
  if (opts.failOn === "warn" && (hasErrors || hasWarnings)) {
3944
- console.error(chalk3.red(`
4264
+ console.error(chalk5.red(`
3945
4265
  Failing: findings detected at warn level or above.`));
3946
4266
  process.exit(2);
3947
4267
  }
3948
4268
  }
4269
+ if (opts.push) {
4270
+ await autoPush(artifact, rootDir, scanOpts);
4271
+ }
3949
4272
  });
3950
4273
 
3951
4274
  export {
3952
4275
  readJsonFile,
3953
- readTextFile,
3954
4276
  pathExists,
3955
4277
  ensureDir,
3956
4278
  writeJsonFile,
3957
- writeTextFile,
3958
4279
  writeDefaultConfig,
3959
4280
  computeDriftScore,
3960
4281
  generateFindings,
3961
4282
  VERSION,
3962
4283
  formatText,
3963
4284
  formatSarif,
4285
+ dsnCommand,
4286
+ pushCommand,
3964
4287
  runScan,
3965
4288
  scanCommand
3966
4289
  };