pruneguard 0.2.1 → 0.3.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/README.md CHANGED
@@ -1,37 +1,31 @@
1
1
  # pruneguard
2
2
 
3
- **One graph. Every answer.** Find unused exports, dead files, phantom dependencies, import cycles, and boundary violations across your entire JS/TS monorepo in a single, fast Rust-powered pass.
3
+ **One graph. Every answer.** Find unused exports, dead files, phantom dependencies, import cycles, and boundary violations across your entire JS/TS monorepo -- in a single, fast Rust-powered pass.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/pruneguard)](https://www.npmjs.com/package/pruneguard)
6
6
  [![license](https://img.shields.io/npm/l/pruneguard)](https://github.com/ferc/pruneguard/blob/main/LICENSE)
7
7
 
8
8
  ---
9
9
 
10
- ## Why pruneguard?
10
+ ## Install
11
11
 
12
- Large JS/TS monorepos accumulate dead code, orphan files, and undeclared cross-package imports faster than any team can manually track. Existing tools either focus on a single concern, struggle with monorepo workspaces, or sacrifice accuracy for speed.
12
+ ```sh
13
+ npm install -D pruneguard # or: npx pruneguard scan
14
+ ```
15
+
16
+ The package automatically installs the correct platform-specific native binary. No Rust toolchain, no compilation, no native addons -- just `npm install` and go.
13
17
 
14
- Pruneguard builds one complete module graph resolving TypeScript paths, package.json `exports`, workspace links, and framework conventions — then runs every analyzer over it in a single pass. The result: fast, accurate, actionable findings with proof chains you can verify before deleting a single line.
18
+ **Supported platforms:** macOS (ARM64, x64), Linux (x64/ARM64, glibc and musl), Windows (x64, ARM64). Requires Node.js >= 18.
15
19
 
16
- ### Key strengths
20
+ ### How it works
17
21
 
18
- - **Rust-native speed** parses and resolves thousands of files in seconds, powered by [oxc](https://oxc.rs)
19
- - **Monorepo-first** — understands pnpm/npm/yarn/bun workspaces, cross-package imports, and `exports` maps out of the box
20
- - **8 built-in analyzers** — unused exports, unused files, unused packages, unused dependencies, cycles, boundary violations, ownership, and impact analysis
21
- - **Framework-aware** — auto-detects Next.js, Vite, Vitest, Jest, and Storybook entrypoints so you don't over-report
22
- - **Trust model** — every finding carries a confidence level; partial-scope scans are clearly marked as advisory
23
- - **Explainability** — `impact` and `explain` commands let you trace proof chains before acting
24
- - **CI-ready** — SARIF output, deterministic mode, `--changed-since` for incremental PR checks, exit codes for gating
25
- - **Migrate easily** — built-in config converters for knip and dependency-cruiser
22
+ pruneguard ships a compiled Rust binary for each platform. The JS API and CLI both spawn this binary. Locally the daemon keeps the graph warm for instant queries. In CI every invocation is a fresh one-shot run.
26
23
 
27
24
  ---
28
25
 
29
26
  ## Quick start
30
27
 
31
28
  ```sh
32
- # Install
33
- npm install -D pruneguard # or: npx pruneguard scan
34
-
35
29
  # Scan your repo
36
30
  pruneguard scan
37
31
 
@@ -53,28 +47,34 @@ pruneguard init
53
47
  pruneguard [OPTIONS] <COMMAND>
54
48
 
55
49
  Commands:
56
- scan [PATHS...] Analyze the repo (default command)
57
- impact <TARGET> Compute blast radius for a file or export
58
- explain <QUERY> Show proof chain for a finding, file, or export
59
- init Generate a default pruneguard.json
60
- print-config Display the resolved configuration
61
- debug resolve Debug module resolution
62
- debug entrypoints List detected entrypoints
63
- debug runtime Print runtime diagnostics
64
- migrate knip Convert knip config to pruneguard
65
- migrate depcruise Convert dependency-cruiser config to pruneguard
50
+ scan [PATHS...] Analyze the repo (default command)
51
+ impact <TARGET> Compute blast radius for a file or export
52
+ explain <QUERY> Show proof chain for a finding, file, or export
53
+ review Branch review gate (blocking vs advisory findings)
54
+ safe-delete <TARGETS...> Evaluate targets for safe deletion
55
+ fix-plan <TARGETS...> Generate structured remediation plan
56
+ suggest-rules Auto-suggest governance rules from graph analysis
57
+ init Generate a default pruneguard.json
58
+ print-config Display the resolved configuration
59
+ debug resolve Debug module resolution
60
+ debug entrypoints List detected entrypoints
61
+ debug runtime Print runtime diagnostics
62
+ daemon start|stop|status Manage the background daemon
63
+ migrate knip Convert knip config to pruneguard
64
+ migrate depcruise Convert dependency-cruiser config to pruneguard
66
65
 
67
66
  Options:
68
67
  -c, --config <FILE> Config file path [default: pruneguard.json]
69
68
  --format <FORMAT> Output format: text, json, sarif, dot
70
69
  --profile <PROFILE> Analysis profile: production, development, all
71
- --changed-since <REF> Only analyze files changed since a git ref
70
+ --changed-since <REF> Only report findings for changed files
72
71
  --focus <GLOB> Filter findings to matching files
73
72
  --severity <SEVERITY> Minimum severity: error, warn, info
74
73
  --no-cache Disable incremental cache
75
74
  --no-baseline Disable baseline suppression
76
75
  --require-full-scope Fail if scan is partial-scope
77
76
  --max-findings <N> Cap the number of reported findings
77
+ --daemon <MODE> Daemon mode: auto, off, required
78
78
  -V, --version Print version
79
79
  -h, --help Print help
80
80
  ```
@@ -83,24 +83,155 @@ Options:
83
83
 
84
84
  ```sh
85
85
  # Full scan with JSON output for CI
86
- pruneguard scan --format json
86
+ pruneguard --format json scan
87
87
 
88
- # PR check only findings from changed files
88
+ # PR check -- only findings from changed files
89
89
  pruneguard --changed-since origin/main scan
90
90
 
91
91
  # Deterministic CI (no cache, no baseline)
92
92
  pruneguard --no-baseline --no-cache scan
93
93
 
94
+ # Branch review gate
95
+ pruneguard --changed-since origin/main review
96
+
97
+ # Safe-delete check before cleanup
98
+ pruneguard safe-delete src/old.ts src/legacy/widget.ts
99
+
94
100
  # Focus on a specific area without narrowing analysis scope
95
101
  pruneguard --focus "packages/core/**" scan
96
102
 
97
103
  # SARIF for GitHub Code Scanning
98
- pruneguard scan --format sarif > results.sarif
104
+ pruneguard --format sarif scan > results.sarif
99
105
 
100
106
  # Visualize the module graph
101
- pruneguard scan --format dot | dot -Tsvg -o graph.svg
107
+ pruneguard --format dot scan | dot -Tsvg -o graph.svg
108
+ ```
109
+
110
+ ---
111
+
112
+ ## Programmatic API
113
+
114
+ All functions spawn the native binary and return typed results.
115
+
116
+ ```js
117
+ import {
118
+ scan,
119
+ review,
120
+ safeDelete,
121
+ fixPlan,
122
+ impact,
123
+ explain,
124
+ suggestRules,
125
+ run,
126
+ binaryPath,
127
+ loadConfig,
128
+ schemaPath,
129
+ scanDot,
130
+ migrateKnip,
131
+ migrateDepcruise,
132
+ } from "pruneguard";
133
+ ```
134
+
135
+ ### scan
136
+
137
+ ```js
138
+ const report = await scan({
139
+ profile: "production",
140
+ changedSince: "origin/main",
141
+ });
142
+ console.log(`${report.summary.totalFindings} findings`);
143
+ ```
144
+
145
+ ### review
146
+
147
+ ```js
148
+ const result = await review({ baseRef: "origin/main", noCache: true });
149
+ if (result.blockingFindings.length > 0) {
150
+ console.error("Blocking findings exist");
151
+ process.exit(1);
152
+ }
153
+ ```
154
+
155
+ ### safeDelete
156
+
157
+ ```js
158
+ const result = await safeDelete({ targets: ["src/old.ts"] });
159
+ console.log("Safe:", result.safe.map(e => e.target));
160
+ console.log("Blocked:", result.blocked.map(e => e.target));
161
+ ```
162
+
163
+ ### fixPlan
164
+
165
+ ```js
166
+ const plan = await fixPlan({ targets: ["src/old.ts"] });
167
+ for (const action of plan.actions) {
168
+ console.log(`${action.kind}: ${action.targets.join(", ")}`);
169
+ }
170
+ ```
171
+
172
+ ### impact
173
+
174
+ ```js
175
+ const blast = await impact({ target: "src/utils/helpers.ts" });
176
+ console.log(`Affects ${blast.affectedEntrypoints.length} entrypoints`);
177
+ ```
178
+
179
+ ### explain
180
+
181
+ ```js
182
+ const proof = await explain({ query: "src/old.ts#deprecatedFn" });
183
+ console.log(proof.proofs);
184
+ ```
185
+
186
+ ### run
187
+
188
+ ```js
189
+ const result = await run(["--format", "json", "--daemon", "off", "scan"]);
190
+ console.log(result.exitCode, result.durationMs);
191
+ ```
192
+
193
+ ### binaryPath
194
+
195
+ ```js
196
+ console.log(binaryPath());
197
+ // => /path/to/node_modules/@pruneguard/cli-darwin-arm64/bin/pruneguard
198
+ ```
199
+
200
+ ### Other functions
201
+
202
+ ```js
203
+ const config = await loadConfig();
204
+ const schema = schemaPath();
205
+ const dot = await scanDot();
206
+ const rules = await suggestRules();
207
+ const knip = await migrateKnip();
208
+ const dc = await migrateDepcruise();
102
209
  ```
103
210
 
211
+ ### Error handling
212
+
213
+ All functions throw `PruneguardExecutionError` with a `code` field:
214
+
215
+ | Code | Meaning |
216
+ |---|---|
217
+ | `PRUNEGUARD_BINARY_NOT_FOUND` | Native binary not found |
218
+ | `PRUNEGUARD_EXECUTION_FAILED` | Binary exited with unexpected code |
219
+ | `PRUNEGUARD_JSON_PARSE_FAILED` | Output was not valid JSON |
220
+
221
+ ```js
222
+ import { scan, PruneguardExecutionError } from "pruneguard";
223
+
224
+ try {
225
+ await scan();
226
+ } catch (err) {
227
+ if (err instanceof PruneguardExecutionError) {
228
+ console.error(err.code, err.message);
229
+ }
230
+ }
231
+ ```
232
+
233
+ Full TypeScript types are included via `dist/index.d.mts`.
234
+
104
235
  ---
105
236
 
106
237
  ## Configuration
@@ -116,47 +247,22 @@ Create `pruneguard.json` (or `.pruneguardrc.json`) in your project root. Run `pr
116
247
  "packageManager": "pnpm"
117
248
  },
118
249
 
119
- "entrypoints": {
120
- "auto": true,
121
- "include": ["src/index.ts"],
122
- "exclude": ["**/*.test.ts"]
123
- },
124
-
125
250
  "analysis": {
126
251
  "unusedExports": "error",
127
252
  "unusedFiles": "warn",
128
253
  "unusedDependencies": "error",
129
- "unusedPackages": "warn",
130
- "cycles": "warn",
131
- "boundaries": "error"
254
+ "cycles": "warn"
132
255
  },
133
256
 
134
257
  "frameworks": {
135
258
  "next": "auto",
136
259
  "vitest": "auto",
137
260
  "storybook": "auto"
138
- },
139
-
140
- "rules": {
141
- "forbidden": [
142
- {
143
- "name": "no-cross-app-imports",
144
- "severity": "error",
145
- "comment": "Apps must not import from other apps",
146
- "from": { "workspace": ["apps/*"] },
147
- "to": { "workspace": ["apps/*"] }
148
- }
149
- ]
150
- },
151
-
152
- "ownership": {
153
- "importCodeowners": true,
154
- "unownedSeverity": "warn"
155
261
  }
156
262
  }
157
263
  ```
158
264
 
159
- Full schema reference is bundled at `node_modules/pruneguard/configuration_schema.json` — editors with JSON Schema support will provide autocomplete and validation automatically.
265
+ Full schema reference is bundled at `node_modules/pruneguard/configuration_schema.json`.
160
266
 
161
267
  ---
162
268
 
@@ -171,65 +277,116 @@ Full schema reference is bundled at `node_modules/pruneguard/configuration_schem
171
277
  | **Cycles** | `cycles` | Circular dependency chains (strongly connected components) |
172
278
  | **Boundary violations** | `boundaries` | Custom forbidden/required import rules |
173
279
  | **Ownership** | `ownership` | Files without a matching team in CODEOWNERS or config |
174
- | **Impact** | | Reverse-reachability blast radius (via `pruneguard impact`) |
280
+ | **Impact** | -- | Reverse-reachability blast radius (via `pruneguard impact`) |
175
281
 
176
- Each finding includes a **confidence level** (high / medium / low) based on analysis scope and unresolved-specifier pressure, so you always know how much to trust a result.
282
+ Each finding includes a **confidence level** (high / medium / low) so you always know how much to trust a result.
177
283
 
178
284
  ---
179
285
 
180
286
  ## Trust model
181
287
 
182
- Pruneguard is designed for safe, incremental adoption — not surprise bulk deletions.
183
-
184
288
  | Mode | Behavior | Use case |
185
289
  |---|---|---|
186
290
  | `pruneguard scan` | Full-repo analysis, high-confidence findings | Deletion decisions, CI gating |
187
291
  | `--focus "glob"` | Full analysis, findings filtered to matching files | Scoping reports to a team or area |
188
292
  | `scan <paths...>` | Partial-scope, findings marked advisory | Quick local checks |
189
- | `--changed-since ref` | Incremental, only changed files analyzed | PR review, fast CI |
293
+ | `--changed-since ref` | Full graph, only changed-file findings reported | PR review, fast CI |
190
294
  | `--require-full-scope` | Fails if scan would be partial-scope | Strict CI enforcement |
191
295
  | `--no-baseline` | No baseline suppression | Deterministic CI, benchmarks |
192
296
 
193
- **Recommended deletion flow:**
194
-
195
- 1. `pruneguard scan --format json` — identify candidates
196
- 2. `pruneguard impact <target>` — check blast radius
197
- 3. `pruneguard explain <finding>` — review proof chain
198
- 4. Delete with confidence
199
-
200
297
  ---
201
298
 
202
- ## Programmatic API
203
-
204
- ```ts
205
- import { scan, impact, explain, loadConfig } from "pruneguard";
299
+ ## GitHub Actions
300
+
301
+ ### Branch review gate
302
+
303
+ ```yaml
304
+ name: pruneguard
305
+ on: [pull_request]
306
+
307
+ jobs:
308
+ review:
309
+ runs-on: ubuntu-latest
310
+ steps:
311
+ - uses: actions/checkout@v5
312
+ with:
313
+ fetch-depth: 0
314
+ - uses: actions/setup-node@v6
315
+ with:
316
+ node-version: 24
317
+ - run: npm install pruneguard
318
+ - name: Branch review
319
+ run: npx pruneguard --changed-since origin/main --format json review
320
+ ```
206
321
 
207
- // Scan and get structured results
208
- const report = await scan({
209
- profile: "production",
210
- changedSince: "origin/main",
211
- });
322
+ ### Baseline-gated CI
323
+
324
+ ```yaml
325
+ name: pruneguard-baseline
326
+ on:
327
+ push:
328
+ branches: [main]
329
+ pull_request:
330
+
331
+ jobs:
332
+ scan:
333
+ runs-on: ubuntu-latest
334
+ steps:
335
+ - uses: actions/checkout@v5
336
+ with:
337
+ fetch-depth: 0
338
+ - uses: actions/setup-node@v6
339
+ with:
340
+ node-version: 24
341
+ - run: npm install pruneguard
342
+
343
+ - name: Save baseline
344
+ if: github.ref == 'refs/heads/main'
345
+ run: npx pruneguard --no-cache --no-baseline --format json scan > baseline.json
346
+
347
+ - uses: actions/upload-artifact@v6
348
+ if: github.ref == 'refs/heads/main'
349
+ with:
350
+ name: pruneguard-baseline
351
+ path: baseline.json
352
+
353
+ - uses: actions/download-artifact@v7
354
+ if: github.event_name == 'pull_request'
355
+ with:
356
+ name: pruneguard-baseline
357
+ continue-on-error: true
358
+
359
+ - name: Check for new findings
360
+ if: github.event_name == 'pull_request'
361
+ run: |
362
+ npx pruneguard --no-cache --no-baseline --format json scan > current.json
363
+ node -e "
364
+ const fs = require('fs');
365
+ if (!fs.existsSync('baseline.json')) { console.log('No baseline, skipping'); process.exit(0); }
366
+ const base = JSON.parse(fs.readFileSync('baseline.json','utf-8'));
367
+ const curr = JSON.parse(fs.readFileSync('current.json','utf-8'));
368
+ const ids = new Set(base.findings.map(f => f.id));
369
+ const n = curr.findings.filter(f => !ids.has(f.id));
370
+ if (n.length) { n.forEach(f => console.error(f.id+': '+f.message)); process.exit(1); }
371
+ console.log('No new findings.');
372
+ "
373
+ ```
212
374
 
213
- console.log(`${report.summary.totalFindings} findings`);
375
+ ---
214
376
 
215
- // Blast radius analysis
216
- const blast = await impact({ target: "src/utils/helpers.ts" });
217
- console.log(`Affects ${blast.affectedEntrypoints.length} entrypoints`);
377
+ ## Migrating from other tools
218
378
 
219
- // Explain a finding
220
- const proof = await explain({
221
- query: "unused-export:packages/core:src/old.ts#deprecatedFn",
222
- });
379
+ ```sh
380
+ pruneguard migrate knip # reads knip.json or package.json#knip
381
+ pruneguard migrate depcruise # reads .dependency-cruiser.* files
223
382
  ```
224
383
 
225
- Full TypeScript types are included via `dist/index.d.mts`.
384
+ Both commands emit an equivalent `pruneguard.json` with migration notes.
226
385
 
227
386
  ---
228
387
 
229
388
  ## Framework detection
230
389
 
231
- Pruneguard auto-detects popular frameworks and registers their entrypoints and file conventions, so test files, stories, and framework config files aren't flagged as unused.
232
-
233
390
  | Framework | Auto-detected via | Entrypoints added |
234
391
  |---|---|---|
235
392
  | **Next.js** | `next` dependency, `next.config.*` | `app/page.*`, `app/layout.*`, `pages/**` |
@@ -242,31 +399,6 @@ Override with `"frameworks": { "next": "off" }` in config.
242
399
 
243
400
  ---
244
401
 
245
- ## Migrating from other tools
246
-
247
- ```sh
248
- # From knip
249
- pruneguard migrate knip
250
-
251
- # From dependency-cruiser
252
- pruneguard migrate depcruise
253
- ```
254
-
255
- Both commands read your existing config and emit an equivalent `pruneguard.json` with migration notes.
256
-
257
- ---
258
-
259
- ## Output formats
260
-
261
- | Format | Flag | Use case |
262
- |---|---|---|
263
- | **Text** | `--format text` | Human-readable terminal output (default) |
264
- | **JSON** | `--format json` | CI pipelines, scripts, programmatic consumption |
265
- | **SARIF** | `--format sarif` | GitHub Code Scanning, Azure DevOps, IDE integrations |
266
- | **DOT** | `--format dot` | Graph visualization with Graphviz |
267
-
268
- ---
269
-
270
402
  ## Requirements
271
403
 
272
404
  - Node.js >= 18
package/bin/pruneguard CHANGED
File without changes
package/dist/bin.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as binaryPath, t as PruneguardExecutionError } from "./runtime-BxlGT_W-.mjs";
2
+ import { n as binaryPath, t as PruneguardExecutionError } from "./runtime-BSHQsTbN.mjs";
3
3
  import { spawn } from "node:child_process";
4
4
  //#region src-js/bin.ts
5
5
  try {
@@ -9,6 +9,10 @@ try {
9
9
  "SIGTERM",
10
10
  "SIGHUP"
11
11
  ]) process.on(sig, () => child.kill(sig));
12
+ child.on("error", (err) => {
13
+ console.error(`pruneguard: failed to execute binary: ${err.message}`);
14
+ process.exitCode = 2;
15
+ });
12
16
  child.on("close", (code, signal) => {
13
17
  if (signal) process.kill(process.pid, signal);
14
18
  else process.exitCode = code ?? 1;