deadwood-scan 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { createRequire } from "module";
5
- import process from "process";
5
+ import process2 from "process";
6
6
  import { Command, Option } from "commander";
7
7
  import ora from "ora";
8
- import pc2 from "picocolors";
8
+ import pc3 from "picocolors";
9
9
 
10
10
  // ../core/src/index.ts
11
11
  import fs9 from "fs";
@@ -1010,6 +1010,28 @@ var SIGNAL_LIST = [
1010
1010
  30,
1011
1011
  "Re-exported by a package entry point; external consumers are invisible to static analysis"
1012
1012
  ),
1013
+ // Git-history signals (git:*) — computed from the repo's own history, which
1014
+ // the CLI collects and injects via scan options (core never spawns git; see
1015
+ // evidence/gitHistory.ts). Deliberately `source: 'static'` so the static cap
1016
+ // still applies: history sharpens confidence, only PRODUCTION lifts the cap.
1017
+ kill("git:untouched-1y", 10, "No commit has touched this file in over a year"),
1018
+ spare(
1019
+ "git:recently-modified",
1020
+ 15,
1021
+ "This file was modified in the last 30 days \u2014 likely active work"
1022
+ ),
1023
+ // Dev/staging/test-traffic evidence (dev:*) — runtime evidence from
1024
+ // NON-production collectors (a probe pointed at a dev server or running
1025
+ // during an e2e suite). Only the spare direction ships for now: traffic
1026
+ // seen anywhere proves the route is wired (revival errs safe); a dev
1027
+ // zero-requests kill would need its own coverage-guarantee story.
1028
+ {
1029
+ id: "dev:request-seen",
1030
+ source: "development",
1031
+ direction: "spare",
1032
+ weight: -60,
1033
+ description: "Traffic observed in a development/staging/test environment"
1034
+ },
1013
1035
  // Reserved for the paid evidence layer — registered now so the scorer and
1014
1036
  // JSON contract already understand them.
1015
1037
  {
@@ -1041,6 +1063,8 @@ var STATIC_CAPS = {
1041
1063
  "unreachable-file": 85
1042
1064
  };
1043
1065
  var STATIC_CAP_REASON = "no runtime evidence \u2014 connect telemetry to confirm";
1066
+ var DEV_CAP = 90;
1067
+ var DEV_CAP_REASON = "dev traffic cannot confirm \u2014 production evidence completes it";
1044
1068
  function evidenceFor(signalId, detail, location) {
1045
1069
  const spec = SIGNALS.get(signalId);
1046
1070
  if (!spec) {
@@ -1789,9 +1813,6 @@ function findPathMatch2(routePath, literals) {
1789
1813
  );
1790
1814
  }
1791
1815
 
1792
- // ../core/src/evidence/dynamicImports.ts
1793
- import path10 from "path";
1794
-
1795
1816
  // ../core/src/evidence/types.ts
1796
1817
  function addOnce(finding, evidence) {
1797
1818
  if (!finding.evidence.some((e) => e.signal === evidence.signal)) {
@@ -1799,7 +1820,59 @@ function addOnce(finding, evidence) {
1799
1820
  }
1800
1821
  }
1801
1822
 
1823
+ // ../core/src/evidence/gitHistory.ts
1824
+ var DAY = 24 * 60 * 60;
1825
+ var STALE_SECONDS = 365 * DAY;
1826
+ var FRESH_SECONDS = 30 * DAY;
1827
+ function ageInWords(seconds) {
1828
+ const days = Math.floor(seconds / DAY);
1829
+ if (days < 1) return "today";
1830
+ if (days === 1) return "yesterday";
1831
+ if (days < 60) return `${days} days ago`;
1832
+ return `${Math.floor(days / 30)} months ago`;
1833
+ }
1834
+ function gitHistory(facts) {
1835
+ return {
1836
+ name: "git-history",
1837
+ provide(_ctx, findings) {
1838
+ if (!facts) return;
1839
+ for (const finding of findings) {
1840
+ if (finding.category === "unused-dependency") continue;
1841
+ const last = facts.lastModified[finding.location.file];
1842
+ if (last === void 0) {
1843
+ if (!facts.truncated) {
1844
+ addOnce(
1845
+ finding,
1846
+ evidenceFor("git:recently-modified", "not committed yet \u2014 likely work in progress")
1847
+ );
1848
+ }
1849
+ continue;
1850
+ }
1851
+ const age = facts.now - last;
1852
+ if (age <= FRESH_SECONDS) {
1853
+ addOnce(
1854
+ finding,
1855
+ evidenceFor(
1856
+ "git:recently-modified",
1857
+ `last commit touching this file was ${ageInWords(age)}`
1858
+ )
1859
+ );
1860
+ } else if (age >= STALE_SECONDS) {
1861
+ addOnce(
1862
+ finding,
1863
+ evidenceFor(
1864
+ "git:untouched-1y",
1865
+ `last commit touching this file was ${ageInWords(age)}`
1866
+ )
1867
+ );
1868
+ }
1869
+ }
1870
+ }
1871
+ };
1872
+ }
1873
+
1802
1874
  // ../core/src/evidence/dynamicImports.ts
1875
+ import path10 from "path";
1803
1876
  var dynamicImports = {
1804
1877
  name: "dynamic-imports",
1805
1878
  provide(ctx, findings) {
@@ -2166,12 +2239,13 @@ function levelFor(score) {
2166
2239
  }
2167
2240
  function scoreFinding(category, evidence) {
2168
2241
  const raw = evidence.reduce((sum, e) => sum + e.weight, 0);
2169
- const allStatic = evidence.every((e) => e.source === "static");
2170
- const cap = allStatic ? STATIC_CAPS[category] : 100;
2242
+ const hasProduction = evidence.some((e) => e.source === "production" || e.source === "tombstone");
2243
+ const hasDev = evidence.some((e) => e.source === "development");
2244
+ const [cap, reason] = hasProduction ? [100, ""] : hasDev ? [DEV_CAP, DEV_CAP_REASON] : [STATIC_CAPS[category], STATIC_CAP_REASON];
2171
2245
  const score = Math.max(0, Math.min(cap, Math.round(raw)));
2172
2246
  const result = { score, level: levelFor(score) };
2173
- if (allStatic && raw > cap) {
2174
- result.capped = { at: cap, reason: STATIC_CAP_REASON };
2247
+ if (!hasProduction && raw > cap) {
2248
+ result.capped = { at: cap, reason };
2175
2249
  }
2176
2250
  return result;
2177
2251
  }
@@ -2356,7 +2430,7 @@ async function scan(options) {
2356
2430
  rel: (file) => relPosix(root, file),
2357
2431
  abs: (relPath) => `${root}/${relPath}`
2358
2432
  };
2359
- for (const provider of PROVIDERS) {
2433
+ for (const provider of [...PROVIDERS, gitHistory(options.gitFacts)]) {
2360
2434
  provider.provide(providerCtx, findings);
2361
2435
  }
2362
2436
  for (const finding of findings) {
@@ -2495,6 +2569,282 @@ function evaluateFailOn(findings, spec) {
2495
2569
  return { count, tripped: count >= spec.min };
2496
2570
  }
2497
2571
 
2572
+ // src/git.ts
2573
+ import { spawnSync } from "child_process";
2574
+ var MAX_COMMITS = 5e4;
2575
+ var MAX_BUFFER = 128 * 1024 * 1024;
2576
+ var MARK = "";
2577
+ function collectGitFacts(dir, now = /* @__PURE__ */ new Date()) {
2578
+ const out = runGit(dir, [
2579
+ "log",
2580
+ `--max-count=${MAX_COMMITS}`,
2581
+ "--format=%x01%ct",
2582
+ "--name-only",
2583
+ "--no-renames",
2584
+ "--relative",
2585
+ "--",
2586
+ "."
2587
+ ]);
2588
+ if (out === void 0) return void 0;
2589
+ const lastModified = {};
2590
+ let commits = 0;
2591
+ let current = 0;
2592
+ for (const rawLine of out.split("\n")) {
2593
+ const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
2594
+ if (line.length === 0) continue;
2595
+ if (line.startsWith(MARK)) {
2596
+ const ts7 = Number(line.slice(1));
2597
+ if (!Number.isFinite(ts7)) continue;
2598
+ current = ts7;
2599
+ commits++;
2600
+ continue;
2601
+ }
2602
+ if (current === 0) continue;
2603
+ const path13 = normalizeGitPath(line);
2604
+ if (lastModified[path13] === void 0) lastModified[path13] = current;
2605
+ }
2606
+ if (commits === 0) return void 0;
2607
+ return {
2608
+ lastModified,
2609
+ truncated: commits >= MAX_COMMITS,
2610
+ now: Math.floor(now.getTime() / 1e3)
2611
+ };
2612
+ }
2613
+ function normalizeGitPath(line) {
2614
+ if (line.startsWith('"') && line.endsWith('"') && line.length >= 2) {
2615
+ try {
2616
+ return JSON.parse(line);
2617
+ } catch {
2618
+ return line;
2619
+ }
2620
+ }
2621
+ return line;
2622
+ }
2623
+ function runGit(dir, args) {
2624
+ try {
2625
+ const res = spawnSync("git", ["-C", dir, ...args], {
2626
+ encoding: "utf8",
2627
+ maxBuffer: MAX_BUFFER,
2628
+ windowsHide: true
2629
+ });
2630
+ if (res.error || res.status !== 0 || typeof res.stdout !== "string") return void 0;
2631
+ return res.stdout;
2632
+ } catch {
2633
+ return void 0;
2634
+ }
2635
+ }
2636
+
2637
+ // src/clean.ts
2638
+ import fs10 from "fs";
2639
+ import path12 from "path";
2640
+ import readline from "readline/promises";
2641
+ import { spawnSync as spawnSync2 } from "child_process";
2642
+ import process from "process";
2643
+ import pc from "picocolors";
2644
+ var LEVEL_RANK2 = { low: 0, medium: 1, high: 2 };
2645
+ async function runClean(dir, opts) {
2646
+ const root = path12.resolve(dir);
2647
+ const useColor = opts.color && process.stdout.isTTY === true;
2648
+ const paint = {
2649
+ dead: (s) => useColor ? pc.red(s) : s,
2650
+ ok: (s) => useColor ? pc.green(s) : s,
2651
+ dim: (s) => useColor ? pc.dim(s) : s,
2652
+ bold: (s) => useColor ? pc.bold(s) : s
2653
+ };
2654
+ if (!opts.dryRun && !opts.yes && process.stdin.isTTY !== true) {
2655
+ process.stderr.write("error: interactive clean needs a TTY \u2014 use --yes or --dry-run\n");
2656
+ process.exitCode = 2;
2657
+ return;
2658
+ }
2659
+ if (opts.commit && !opts.dryRun) {
2660
+ if (!isGitRepo(root)) {
2661
+ process.stderr.write(
2662
+ "error: not a git repository \u2014 deadwood clean commits each removal so it is revertable.\n Use --no-commit to delete without commits (you own the undo story then).\n"
2663
+ );
2664
+ process.exitCode = 2;
2665
+ return;
2666
+ }
2667
+ if (!workingTreeClean(root)) {
2668
+ process.stderr.write(
2669
+ "error: working tree is not clean \u2014 commit or stash first, so each removal is its own revertable commit.\n"
2670
+ );
2671
+ process.exitCode = 2;
2672
+ return;
2673
+ }
2674
+ }
2675
+ process.stderr.write("scanning\u2026\n");
2676
+ const gitFacts = opts.git ? collectGitFacts(root) : void 0;
2677
+ const result = await scan({ path: root, gitFacts });
2678
+ const min = LEVEL_RANK2[opts.minConfidence];
2679
+ const eligible = result.findings.filter((f) => LEVEL_RANK2[f.confidence.level] >= min);
2680
+ const removals = [];
2681
+ let unsupported = 0;
2682
+ for (const f of eligible) {
2683
+ const removal = planRemoval(root, f, result);
2684
+ if (removal) removals.push(removal);
2685
+ else if (f.category === "dead-route" || f.category === "unused-export") unsupported++;
2686
+ }
2687
+ if (removals.length === 0) {
2688
+ process.stdout.write(
2689
+ `Nothing removable at ${opts.minConfidence}+ confidence.` + (unsupported > 0 ? ` (${unsupported} route/export finding${unsupported === 1 ? "" : "s"} need manual review \u2014 automated line surgery isn't provable yet.)` : "") + "\n"
2690
+ );
2691
+ return;
2692
+ }
2693
+ process.stdout.write(
2694
+ `${paint.bold(String(removals.length))} removable finding${removals.length === 1 ? "" : "s"} at ${opts.minConfidence}+ confidence` + (unsupported > 0 ? paint.dim(` (+${unsupported} route/export need manual review)`) : "") + "\n\n"
2695
+ );
2696
+ const rl = !opts.dryRun && !opts.yes ? readline.createInterface({ input: process.stdin, output: process.stderr }) : void 0;
2697
+ let acceptAll = Boolean(opts.yes);
2698
+ let removed = 0;
2699
+ let skipped = 0;
2700
+ let failed = 0;
2701
+ try {
2702
+ for (const removal of removals) {
2703
+ const f = removal.finding;
2704
+ process.stdout.write(
2705
+ `${paint.dead(describeTarget(f.target))} ${paint.dim(`${f.location.file}:${f.location.line}`)} ${f.confidence.score}% ${f.confidence.level}
2706
+ `
2707
+ );
2708
+ for (const e of f.evidence.slice(0, 3)) {
2709
+ process.stdout.write(paint.dim(` ${e.direction === "spare" ? "~" : "-"} ${e.detail}
2710
+ `));
2711
+ }
2712
+ process.stdout.write(` \u2192 ${removal.describe}
2713
+ `);
2714
+ if (opts.dryRun) {
2715
+ process.stdout.write(paint.dim(" dry-run: not applied\n\n"));
2716
+ continue;
2717
+ }
2718
+ let accepted = acceptAll;
2719
+ if (!accepted && rl) {
2720
+ const answer = (await rl.question(" [d]elete / [s]kip / [a]ll remaining / [q]uit > ")).trim().toLowerCase();
2721
+ if (answer === "q") break;
2722
+ if (answer === "a") {
2723
+ acceptAll = true;
2724
+ accepted = true;
2725
+ } else {
2726
+ accepted = answer === "d" || answer === "y";
2727
+ }
2728
+ }
2729
+ if (!accepted) {
2730
+ skipped++;
2731
+ process.stdout.write(paint.dim(" skipped\n\n"));
2732
+ continue;
2733
+ }
2734
+ try {
2735
+ const staged = removal.apply();
2736
+ if (opts.commit) {
2737
+ commitRemoval(root, staged, `chore(deadwood): remove ${describeTarget(f.target)}`);
2738
+ }
2739
+ removed++;
2740
+ process.stdout.write(paint.ok(` removed${opts.commit ? " + committed" : ""}
2741
+
2742
+ `));
2743
+ } catch (err) {
2744
+ failed++;
2745
+ process.stderr.write(
2746
+ ` error: ${err instanceof Error ? err.message : String(err)} \u2014 leaving it in place
2747
+
2748
+ `
2749
+ );
2750
+ }
2751
+ }
2752
+ } finally {
2753
+ rl?.close();
2754
+ }
2755
+ if (opts.dryRun) {
2756
+ process.stdout.write(
2757
+ `dry run: ${removals.length} removal${removals.length === 1 ? "" : "s"} planned, nothing touched.
2758
+ `
2759
+ );
2760
+ return;
2761
+ }
2762
+ process.stdout.write(
2763
+ `${paint.ok(String(removed))} removed, ${skipped} skipped${failed > 0 ? `, ${paint.dead(String(failed))} failed` : ""}.` + (removed > 0 && opts.commit ? " Each removal is its own commit \u2014 `git revert <sha>` undoes any of them." : "") + "\n"
2764
+ );
2765
+ if (failed > 0) process.exitCode = 2;
2766
+ }
2767
+ function planRemoval(root, f, result) {
2768
+ if (f.category === "unreachable-file" && f.target.kind === "file") {
2769
+ const abs = containedPath(root, f.target.file);
2770
+ if (!abs || !fs10.existsSync(abs)) return null;
2771
+ const file = f.target.file;
2772
+ return {
2773
+ finding: f,
2774
+ describe: `delete file ${file}`,
2775
+ apply: () => {
2776
+ fs10.rmSync(abs);
2777
+ return [file];
2778
+ }
2779
+ };
2780
+ }
2781
+ if (f.category === "unused-dependency" && f.target.kind === "dependency") {
2782
+ const pkgRel = f.location.file;
2783
+ const abs = containedPath(root, pkgRel);
2784
+ if (!abs || !fs10.existsSync(abs) || !pkgRel.endsWith("package.json")) return null;
2785
+ const { name, depType } = f.target;
2786
+ return {
2787
+ finding: f,
2788
+ describe: `remove ${name} from ${depType} in ${pkgRel}`,
2789
+ apply: () => {
2790
+ removeDependency(abs, name, depType);
2791
+ return [pkgRel];
2792
+ }
2793
+ };
2794
+ }
2795
+ void result;
2796
+ return null;
2797
+ }
2798
+ function containedPath(root, rel) {
2799
+ const abs = path12.resolve(root, rel);
2800
+ const normRoot = path12.resolve(root) + path12.sep;
2801
+ return abs.startsWith(normRoot) ? abs : void 0;
2802
+ }
2803
+ function removeDependency(pkgPath, name, depType) {
2804
+ const raw = fs10.readFileSync(pkgPath, "utf8");
2805
+ const pkg2 = JSON.parse(raw);
2806
+ const deps = pkg2[depType];
2807
+ if (typeof deps !== "object" || deps === null || !(name in deps)) {
2808
+ throw new Error(`${name} not found in ${depType}`);
2809
+ }
2810
+ delete deps[name];
2811
+ const indent = /^([ \t]+)"/m.exec(raw)?.[1] ?? " ";
2812
+ const trailingNewline = raw.endsWith("\n") ? "\n" : "";
2813
+ fs10.writeFileSync(pkgPath, JSON.stringify(pkg2, null, indent) + trailingNewline);
2814
+ }
2815
+ function isGitRepo(dir) {
2816
+ return runGit2(dir, ["rev-parse", "--is-inside-work-tree"])?.trim() === "true";
2817
+ }
2818
+ function workingTreeClean(dir) {
2819
+ const out = runGit2(dir, ["status", "--porcelain"]);
2820
+ return out !== void 0 && out.trim().length === 0;
2821
+ }
2822
+ function commitRemoval(root, relPaths, message) {
2823
+ const add = spawnSync2("git", ["-C", root, "add", "--", ...relPaths], {
2824
+ windowsHide: true,
2825
+ encoding: "utf8"
2826
+ });
2827
+ if (add.status !== 0) throw new Error(`git add failed: ${firstLine(add.stderr)}`);
2828
+ const commit = spawnSync2("git", ["-C", root, "commit", "-q", "-m", message, "--", ...relPaths], {
2829
+ windowsHide: true,
2830
+ encoding: "utf8"
2831
+ });
2832
+ if (commit.status !== 0) throw new Error(`git commit failed: ${firstLine(commit.stderr)}`);
2833
+ }
2834
+ function firstLine(s) {
2835
+ const line = (s ?? "").split("\n").find((l) => l.trim().length > 0);
2836
+ return line?.trim() ?? "unknown git error";
2837
+ }
2838
+ function runGit2(dir, args) {
2839
+ try {
2840
+ const res = spawnSync2("git", ["-C", dir, ...args], { encoding: "utf8", windowsHide: true });
2841
+ if (res.error || res.status !== 0) return void 0;
2842
+ return res.stdout;
2843
+ } catch {
2844
+ return void 0;
2845
+ }
2846
+ }
2847
+
2498
2848
  // src/reporters/json.ts
2499
2849
  function renderJson(result) {
2500
2850
  return JSON.stringify(result, null, 2) + "\n";
@@ -2557,7 +2907,7 @@ function escapeMd(s) {
2557
2907
  }
2558
2908
 
2559
2909
  // src/reporters/pretty.ts
2560
- import pc from "picocolors";
2910
+ import pc2 from "picocolors";
2561
2911
  var CATEGORY_TITLES2 = {
2562
2912
  "dead-route": "ROUTES THAT APPEAR DEAD",
2563
2913
  "unused-export": "EXPORTS THAT APPEAR UNUSED",
@@ -2566,7 +2916,7 @@ var CATEGORY_TITLES2 = {
2566
2916
  };
2567
2917
  var RULE_WIDTH = 72;
2568
2918
  function renderPretty(result, useColor) {
2569
- const c = pc.createColors(useColor);
2919
+ const c = pc2.createColors(useColor);
2570
2920
  const lines = [];
2571
2921
  const { stats } = result;
2572
2922
  lines.push("");
@@ -2685,7 +3035,7 @@ var CATEGORIES = [
2685
3035
  "unused-dependency",
2686
3036
  "unreachable-file"
2687
3037
  ];
2688
- var LEVEL_RANK2 = { low: 0, medium: 1, high: 2 };
3038
+ var LEVEL_RANK3 = { low: 0, medium: 1, high: 2 };
2689
3039
  var program = new Command();
2690
3040
  program.name("deadwood").description("Find dead code and prove it \u2014 scans a JS/TS repo and prints a shock report").version(pkg.version).argument("[path]", "directory to scan", ".").option("--json", "machine-readable ScanResult on stdout").option("--md", "markdown report (PR comments / Slack)").option("--fail-on <spec>", "CI gate: 'high', 'medium', or 'high:5' (exit 1 when tripped)").option("--include <glob...>", "extra include globs").option("--exclude <glob...>", "extra exclude globs").option(
2691
3041
  "--no-default-excludes",
@@ -2697,24 +3047,40 @@ program.name("deadwood").description("Find dead code and prove it \u2014 scans a
2697
3047
  "nest",
2698
3048
  "next"
2699
3049
  ])
2700
- ).option("--entry <file...>", "extra entry points (false-positive escape hatch)").addOption(
3050
+ ).option("--entry <file...>", "extra entry points (false-positive escape hatch)").option("--no-git", "skip git-history evidence (untouched-for-a-year / recently-modified nudges)").addOption(
2701
3051
  new Option("--min-confidence <level>", "hide findings below this level").choices(["low", "medium", "high"]).default("low")
2702
- ).option("--no-color", "disable colors").action(async (path12, opts) => {
2703
- await run(path12, opts);
3052
+ ).option("--no-color", "disable colors").action(async (path13, opts) => {
3053
+ await run(path13, opts);
2704
3054
  });
2705
- async function run(path12, opts) {
3055
+ program.command("clean").description("guided deletion: review findings and remove them, one revertable commit each").argument("[path]", "directory to clean", ".").option("--dry-run", "print the removal plan without touching anything").option("--yes", "accept every removal at/above the confidence floor (non-interactive)").addOption(
3056
+ new Option("--min-confidence <level>", "only offer removals at/above this level").choices(["medium", "high"]).default("medium")
3057
+ ).option("--no-commit", "delete without committing (also lifts the clean-tree requirement)").option("--no-git", "skip git-history evidence in the underlying scan").option("--no-color", "disable colors").action(async (path13, opts) => {
3058
+ try {
3059
+ await runClean(path13, opts);
3060
+ } catch (err) {
3061
+ if (err instanceof ScanError) {
3062
+ process2.stderr.write(`error (${err.code}): ${err.message}
3063
+ `);
3064
+ } else {
3065
+ process2.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}
3066
+ `);
3067
+ }
3068
+ process2.exitCode = 2;
3069
+ }
3070
+ });
3071
+ async function run(path13, opts) {
2706
3072
  if (opts.json && opts.md) {
2707
- process.stderr.write("error: --json and --md are mutually exclusive\n");
2708
- process.exitCode = 2;
3073
+ process2.stderr.write("error: --json and --md are mutually exclusive\n");
3074
+ process2.exitCode = 2;
2709
3075
  return;
2710
3076
  }
2711
3077
  const failOnSpec = (() => {
2712
3078
  try {
2713
3079
  return opts.failOn ? parseFailOn(opts.failOn) : void 0;
2714
3080
  } catch (err) {
2715
- process.stderr.write(`error: ${err.message}
3081
+ process2.stderr.write(`error: ${err.message}
2716
3082
  `);
2717
- process.exitCode = 2;
3083
+ process2.exitCode = 2;
2718
3084
  return null;
2719
3085
  }
2720
3086
  })();
@@ -2722,13 +3088,14 @@ async function run(path12, opts) {
2722
3088
  const machineOutput = Boolean(opts.json || opts.md);
2723
3089
  const spinner = ora({
2724
3090
  text: "scanning\u2026",
2725
- stream: process.stderr,
2726
- isEnabled: !machineOutput && process.stderr.isTTY === true
3091
+ stream: process2.stderr,
3092
+ isEnabled: !machineOutput && process2.stderr.isTTY === true
2727
3093
  });
2728
3094
  try {
2729
3095
  spinner.start();
3096
+ const gitFacts = opts.git ? collectGitFacts(path13) : void 0;
2730
3097
  const result = await scan({
2731
- path: path12,
3098
+ path: path13,
2732
3099
  include: opts.include,
2733
3100
  exclude: opts.exclude,
2734
3101
  defaultExcludes: opts.defaultExcludes,
@@ -2736,44 +3103,45 @@ async function run(path12, opts) {
2736
3103
  frameworks: opts.framework,
2737
3104
  entries: opts.entry,
2738
3105
  workspaces: opts.workspace,
2739
- toolVersion: pkg.version
3106
+ toolVersion: pkg.version,
3107
+ gitFacts
2740
3108
  });
2741
3109
  spinner.stop();
2742
3110
  const gate = failOnSpec ? evaluateFailOn(result.findings, failOnSpec) : void 0;
2743
3111
  const displayed = filterForDisplay(result, opts.minConfidence);
2744
- const useColor = opts.color && pc2.isColorSupported && process.stdout.isTTY === true;
3112
+ const useColor = opts.color && pc3.isColorSupported && process2.stdout.isTTY === true;
2745
3113
  const output = opts.json ? renderJson(displayed) : opts.md ? renderMd(displayed) : renderPretty(displayed, useColor);
2746
- process.stdout.write(output);
3114
+ process2.stdout.write(output);
2747
3115
  if (opts.json) {
2748
- for (const warning of result.warnings) process.stderr.write(`warning: ${warning}
3116
+ for (const warning of result.warnings) process2.stderr.write(`warning: ${warning}
2749
3117
  `);
2750
3118
  }
2751
3119
  if (gate?.tripped) {
2752
- process.stderr.write(
3120
+ process2.stderr.write(
2753
3121
  `deadwood: --fail-on ${opts.failOn} tripped (${gate.count} finding${gate.count === 1 ? "" : "s"} at/above '${failOnSpec.level}')
2754
3122
  `
2755
3123
  );
2756
- process.exitCode = 1;
3124
+ process2.exitCode = 1;
2757
3125
  }
2758
3126
  } catch (err) {
2759
3127
  spinner.stop();
2760
3128
  if (err instanceof ScanError) {
2761
- process.stderr.write(`error (${err.code}): ${err.message}
3129
+ process2.stderr.write(`error (${err.code}): ${err.message}
2762
3130
  `);
2763
3131
  } else {
2764
- process.stderr.write(
3132
+ process2.stderr.write(
2765
3133
  `error: ${err instanceof Error ? err.stack ?? err.message : String(err)}
2766
3134
  `
2767
3135
  );
2768
3136
  }
2769
- process.exitCode = 2;
3137
+ process2.exitCode = 2;
2770
3138
  }
2771
3139
  }
2772
3140
  function filterForDisplay(result, minConfidence) {
2773
3141
  if (minConfidence === "low") return result;
2774
- const min = LEVEL_RANK2[minConfidence];
2775
- const findings = result.findings.filter((f) => LEVEL_RANK2[f.confidence.level] >= min);
3142
+ const min = LEVEL_RANK3[minConfidence];
3143
+ const findings = result.findings.filter((f) => LEVEL_RANK3[f.confidence.level] >= min);
2776
3144
  return { ...result, findings };
2777
3145
  }
2778
- await program.parseAsync(process.argv);
3146
+ await program.parseAsync(process2.argv);
2779
3147
  //# sourceMappingURL=index.js.map