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 +404 -36
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { createRequire } from "module";
|
|
5
|
-
import
|
|
5
|
+
import process2 from "process";
|
|
6
6
|
import { Command, Option } from "commander";
|
|
7
7
|
import ora from "ora";
|
|
8
|
-
import
|
|
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
|
|
2170
|
-
const
|
|
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 (
|
|
2174
|
-
result.capped = { at: 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
|
|
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 =
|
|
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
|
|
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 (
|
|
2703
|
-
await run(
|
|
3052
|
+
).option("--no-color", "disable colors").action(async (path13, opts) => {
|
|
3053
|
+
await run(path13, opts);
|
|
2704
3054
|
});
|
|
2705
|
-
|
|
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
|
-
|
|
2708
|
-
|
|
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
|
-
|
|
3081
|
+
process2.stderr.write(`error: ${err.message}
|
|
2716
3082
|
`);
|
|
2717
|
-
|
|
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:
|
|
2726
|
-
isEnabled: !machineOutput &&
|
|
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:
|
|
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 &&
|
|
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
|
-
|
|
3114
|
+
process2.stdout.write(output);
|
|
2747
3115
|
if (opts.json) {
|
|
2748
|
-
for (const warning of result.warnings)
|
|
3116
|
+
for (const warning of result.warnings) process2.stderr.write(`warning: ${warning}
|
|
2749
3117
|
`);
|
|
2750
3118
|
}
|
|
2751
3119
|
if (gate?.tripped) {
|
|
2752
|
-
|
|
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
|
-
|
|
3124
|
+
process2.exitCode = 1;
|
|
2757
3125
|
}
|
|
2758
3126
|
} catch (err) {
|
|
2759
3127
|
spinner.stop();
|
|
2760
3128
|
if (err instanceof ScanError) {
|
|
2761
|
-
|
|
3129
|
+
process2.stderr.write(`error (${err.code}): ${err.message}
|
|
2762
3130
|
`);
|
|
2763
3131
|
} else {
|
|
2764
|
-
|
|
3132
|
+
process2.stderr.write(
|
|
2765
3133
|
`error: ${err instanceof Error ? err.stack ?? err.message : String(err)}
|
|
2766
3134
|
`
|
|
2767
3135
|
);
|
|
2768
3136
|
}
|
|
2769
|
-
|
|
3137
|
+
process2.exitCode = 2;
|
|
2770
3138
|
}
|
|
2771
3139
|
}
|
|
2772
3140
|
function filterForDisplay(result, minConfidence) {
|
|
2773
3141
|
if (minConfidence === "low") return result;
|
|
2774
|
-
const min =
|
|
2775
|
-
const findings = result.findings.filter((f) =>
|
|
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(
|
|
3146
|
+
await program.parseAsync(process2.argv);
|
|
2779
3147
|
//# sourceMappingURL=index.js.map
|