@vibgrate/cli 1.0.1 → 1.0.3

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