pruneguard 0.2.1 → 0.3.1

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,31 @@ 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
66
-
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
67
63
  Options:
68
64
  -c, --config <FILE> Config file path [default: pruneguard.json]
69
65
  --format <FORMAT> Output format: text, json, sarif, dot
70
66
  --profile <PROFILE> Analysis profile: production, development, all
71
- --changed-since <REF> Only analyze files changed since a git ref
67
+ --changed-since <REF> Only report findings for changed files
72
68
  --focus <GLOB> Filter findings to matching files
73
69
  --severity <SEVERITY> Minimum severity: error, warn, info
74
70
  --no-cache Disable incremental cache
75
71
  --no-baseline Disable baseline suppression
76
72
  --require-full-scope Fail if scan is partial-scope
77
73
  --max-findings <N> Cap the number of reported findings
74
+ --daemon <MODE> Daemon mode: auto, off, required
78
75
  -V, --version Print version
79
76
  -h, --help Print help
80
77
  ```
@@ -83,24 +80,151 @@ Options:
83
80
 
84
81
  ```sh
85
82
  # Full scan with JSON output for CI
86
- pruneguard scan --format json
83
+ pruneguard --format json scan
87
84
 
88
- # PR check only findings from changed files
85
+ # PR check -- only findings from changed files
89
86
  pruneguard --changed-since origin/main scan
90
87
 
91
88
  # Deterministic CI (no cache, no baseline)
92
89
  pruneguard --no-baseline --no-cache scan
93
90
 
91
+ # Branch review gate
92
+ pruneguard --changed-since origin/main review
93
+
94
+ # Safe-delete check before cleanup
95
+ pruneguard safe-delete src/old.ts src/legacy/widget.ts
96
+
94
97
  # Focus on a specific area without narrowing analysis scope
95
98
  pruneguard --focus "packages/core/**" scan
96
99
 
97
100
  # SARIF for GitHub Code Scanning
98
- pruneguard scan --format sarif > results.sarif
101
+ pruneguard --format sarif scan > results.sarif
99
102
 
100
103
  # Visualize the module graph
101
- pruneguard scan --format dot | dot -Tsvg -o graph.svg
104
+ pruneguard --format dot scan | dot -Tsvg -o graph.svg
105
+ ```
106
+
107
+ ---
108
+
109
+ ## Programmatic API
110
+
111
+ All functions spawn the native binary and return typed results.
112
+
113
+ ```js
114
+ import {
115
+ scan,
116
+ review,
117
+ safeDelete,
118
+ fixPlan,
119
+ impact,
120
+ explain,
121
+ suggestRules,
122
+ run,
123
+ binaryPath,
124
+ loadConfig,
125
+ schemaPath,
126
+ scanDot,
127
+ } from "pruneguard";
128
+ ```
129
+
130
+ ### scan
131
+
132
+ ```js
133
+ const report = await scan({
134
+ profile: "production",
135
+ changedSince: "origin/main",
136
+ });
137
+ console.log(`${report.summary.totalFindings} findings`);
138
+ ```
139
+
140
+ ### review
141
+
142
+ ```js
143
+ const result = await review({ baseRef: "origin/main", noCache: true });
144
+ if (result.blockingFindings.length > 0) {
145
+ console.error("Blocking findings exist");
146
+ process.exit(1);
147
+ }
148
+ ```
149
+
150
+ ### safeDelete
151
+
152
+ ```js
153
+ const result = await safeDelete({ targets: ["src/old.ts"] });
154
+ console.log("Safe:", result.safe.map(e => e.target));
155
+ console.log("Blocked:", result.blocked.map(e => e.target));
156
+ ```
157
+
158
+ ### fixPlan
159
+
160
+ ```js
161
+ const plan = await fixPlan({ targets: ["src/old.ts"] });
162
+ for (const action of plan.actions) {
163
+ console.log(`${action.kind}: ${action.targets.join(", ")}`);
164
+ }
165
+ ```
166
+
167
+ ### impact
168
+
169
+ ```js
170
+ const blast = await impact({ target: "src/utils/helpers.ts" });
171
+ console.log(`Affects ${blast.affectedEntrypoints.length} entrypoints`);
172
+ ```
173
+
174
+ ### explain
175
+
176
+ ```js
177
+ const proof = await explain({ query: "src/old.ts#deprecatedFn" });
178
+ console.log(proof.proofs);
179
+ ```
180
+
181
+ ### run
182
+
183
+ ```js
184
+ const result = await run(["--format", "json", "--daemon", "off", "scan"]);
185
+ console.log(result.exitCode, result.durationMs);
186
+ ```
187
+
188
+ ### binaryPath
189
+
190
+ ```js
191
+ console.log(binaryPath());
192
+ // => /path/to/node_modules/@pruneguard/cli-darwin-arm64/bin/pruneguard
193
+ ```
194
+
195
+ ### Other functions
196
+
197
+ ```js
198
+ const config = await loadConfig();
199
+ const schema = schemaPath();
200
+ const dot = await scanDot();
201
+ const rules = await suggestRules();
102
202
  ```
103
203
 
204
+ ### Error handling
205
+
206
+ All functions throw `PruneguardExecutionError` with a `code` field:
207
+
208
+ | Code | Meaning |
209
+ |---|---|
210
+ | `PRUNEGUARD_BINARY_NOT_FOUND` | Native binary not found |
211
+ | `PRUNEGUARD_EXECUTION_FAILED` | Binary exited with unexpected code |
212
+ | `PRUNEGUARD_JSON_PARSE_FAILED` | Output was not valid JSON |
213
+
214
+ ```js
215
+ import { scan, PruneguardExecutionError } from "pruneguard";
216
+
217
+ try {
218
+ await scan();
219
+ } catch (err) {
220
+ if (err instanceof PruneguardExecutionError) {
221
+ console.error(err.code, err.message);
222
+ }
223
+ }
224
+ ```
225
+
226
+ Full TypeScript types are included via `dist/index.d.mts`.
227
+
104
228
  ---
105
229
 
106
230
  ## Configuration
@@ -116,47 +240,22 @@ Create `pruneguard.json` (or `.pruneguardrc.json`) in your project root. Run `pr
116
240
  "packageManager": "pnpm"
117
241
  },
118
242
 
119
- "entrypoints": {
120
- "auto": true,
121
- "include": ["src/index.ts"],
122
- "exclude": ["**/*.test.ts"]
123
- },
124
-
125
243
  "analysis": {
126
244
  "unusedExports": "error",
127
245
  "unusedFiles": "warn",
128
246
  "unusedDependencies": "error",
129
- "unusedPackages": "warn",
130
- "cycles": "warn",
131
- "boundaries": "error"
247
+ "cycles": "warn"
132
248
  },
133
249
 
134
250
  "frameworks": {
135
251
  "next": "auto",
136
252
  "vitest": "auto",
137
253
  "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
254
  }
156
255
  }
157
256
  ```
158
257
 
159
- Full schema reference is bundled at `node_modules/pruneguard/configuration_schema.json` — editors with JSON Schema support will provide autocomplete and validation automatically.
258
+ Full schema reference is bundled at `node_modules/pruneguard/configuration_schema.json`.
160
259
 
161
260
  ---
162
261
 
@@ -171,65 +270,105 @@ Full schema reference is bundled at `node_modules/pruneguard/configuration_schem
171
270
  | **Cycles** | `cycles` | Circular dependency chains (strongly connected components) |
172
271
  | **Boundary violations** | `boundaries` | Custom forbidden/required import rules |
173
272
  | **Ownership** | `ownership` | Files without a matching team in CODEOWNERS or config |
174
- | **Impact** | | Reverse-reachability blast radius (via `pruneguard impact`) |
273
+ | **Impact** | -- | Reverse-reachability blast radius (via `pruneguard impact`) |
175
274
 
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.
275
+ Each finding includes a **confidence level** (high / medium / low) so you always know how much to trust a result.
177
276
 
178
277
  ---
179
278
 
180
279
  ## Trust model
181
280
 
182
- Pruneguard is designed for safe, incremental adoption — not surprise bulk deletions.
183
-
184
281
  | Mode | Behavior | Use case |
185
282
  |---|---|---|
186
283
  | `pruneguard scan` | Full-repo analysis, high-confidence findings | Deletion decisions, CI gating |
187
284
  | `--focus "glob"` | Full analysis, findings filtered to matching files | Scoping reports to a team or area |
188
285
  | `scan <paths...>` | Partial-scope, findings marked advisory | Quick local checks |
189
- | `--changed-since ref` | Incremental, only changed files analyzed | PR review, fast CI |
286
+ | `--changed-since ref` | Full graph, only changed-file findings reported | PR review, fast CI |
190
287
  | `--require-full-scope` | Fails if scan would be partial-scope | Strict CI enforcement |
191
288
  | `--no-baseline` | No baseline suppression | Deterministic CI, benchmarks |
192
289
 
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
290
  ---
201
291
 
202
- ## Programmatic API
203
-
204
- ```ts
205
- import { scan, impact, explain, loadConfig } from "pruneguard";
206
-
207
- // Scan and get structured results
208
- const report = await scan({
209
- profile: "production",
210
- changedSince: "origin/main",
211
- });
212
-
213
- console.log(`${report.summary.totalFindings} findings`);
214
-
215
- // Blast radius analysis
216
- const blast = await impact({ target: "src/utils/helpers.ts" });
217
- console.log(`Affects ${blast.affectedEntrypoints.length} entrypoints`);
218
-
219
- // Explain a finding
220
- const proof = await explain({
221
- query: "unused-export:packages/core:src/old.ts#deprecatedFn",
222
- });
292
+ ## GitHub Actions
293
+
294
+ ### Branch review gate
295
+
296
+ ```yaml
297
+ name: pruneguard
298
+ on: [pull_request]
299
+
300
+ jobs:
301
+ review:
302
+ runs-on: ubuntu-latest
303
+ steps:
304
+ - uses: actions/checkout@v5
305
+ with:
306
+ fetch-depth: 0
307
+ - uses: actions/setup-node@v6
308
+ with:
309
+ node-version: 24
310
+ - run: npm install pruneguard
311
+ - name: Branch review
312
+ run: npx pruneguard --changed-since origin/main --format json review
223
313
  ```
224
314
 
225
- Full TypeScript types are included via `dist/index.d.mts`.
315
+ ### Baseline-gated CI
316
+
317
+ ```yaml
318
+ name: pruneguard-baseline
319
+ on:
320
+ push:
321
+ branches: [main]
322
+ pull_request:
323
+
324
+ jobs:
325
+ scan:
326
+ runs-on: ubuntu-latest
327
+ steps:
328
+ - uses: actions/checkout@v5
329
+ with:
330
+ fetch-depth: 0
331
+ - uses: actions/setup-node@v6
332
+ with:
333
+ node-version: 24
334
+ - run: npm install pruneguard
335
+
336
+ - name: Save baseline
337
+ if: github.ref == 'refs/heads/main'
338
+ run: npx pruneguard --no-cache --no-baseline --format json scan > baseline.json
339
+
340
+ - uses: actions/upload-artifact@v6
341
+ if: github.ref == 'refs/heads/main'
342
+ with:
343
+ name: pruneguard-baseline
344
+ path: baseline.json
345
+
346
+ - uses: actions/download-artifact@v7
347
+ if: github.event_name == 'pull_request'
348
+ with:
349
+ name: pruneguard-baseline
350
+ continue-on-error: true
351
+
352
+ - name: Check for new findings
353
+ if: github.event_name == 'pull_request'
354
+ run: |
355
+ npx pruneguard --no-cache --no-baseline --format json scan > current.json
356
+ node -e "
357
+ const fs = require('fs');
358
+ if (!fs.existsSync('baseline.json')) { console.log('No baseline, skipping'); process.exit(0); }
359
+ const base = JSON.parse(fs.readFileSync('baseline.json','utf-8'));
360
+ const curr = JSON.parse(fs.readFileSync('current.json','utf-8'));
361
+ const ids = new Set(base.findings.map(f => f.id));
362
+ const n = curr.findings.filter(f => !ids.has(f.id));
363
+ if (n.length) { n.forEach(f => console.error(f.id+': '+f.message)); process.exit(1); }
364
+ console.log('No new findings.');
365
+ "
366
+ ```
226
367
 
227
368
  ---
228
369
 
229
370
  ## Framework detection
230
371
 
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
372
  | Framework | Auto-detected via | Entrypoints added |
234
373
  |---|---|---|
235
374
  | **Next.js** | `next` dependency, `next.config.*` | `app/page.*`, `app/layout.*`, `pages/**` |
@@ -242,31 +381,6 @@ Override with `"frameworks": { "next": "off" }` in config.
242
381
 
243
382
  ---
244
383
 
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
384
  ## Requirements
271
385
 
272
386
  - Node.js >= 18
package/bin/pruneguard CHANGED
File without changes
@@ -259,6 +259,46 @@
259
259
  "FrameworksConfig": {
260
260
  "type": "object",
261
261
  "properties": {
262
+ "angular": {
263
+ "anyOf": [
264
+ {
265
+ "$ref": "#/definitions/FrameworkToggle"
266
+ },
267
+ {
268
+ "type": "null"
269
+ }
270
+ ]
271
+ },
272
+ "astro": {
273
+ "anyOf": [
274
+ {
275
+ "$ref": "#/definitions/FrameworkToggle"
276
+ },
277
+ {
278
+ "type": "null"
279
+ }
280
+ ]
281
+ },
282
+ "cypress": {
283
+ "anyOf": [
284
+ {
285
+ "$ref": "#/definitions/FrameworkToggle"
286
+ },
287
+ {
288
+ "type": "null"
289
+ }
290
+ ]
291
+ },
292
+ "docusaurus": {
293
+ "anyOf": [
294
+ {
295
+ "$ref": "#/definitions/FrameworkToggle"
296
+ },
297
+ {
298
+ "type": "null"
299
+ }
300
+ ]
301
+ },
262
302
  "jest": {
263
303
  "anyOf": [
264
304
  {
@@ -279,6 +319,46 @@
279
319
  }
280
320
  ]
281
321
  },
322
+ "nuxt": {
323
+ "anyOf": [
324
+ {
325
+ "$ref": "#/definitions/FrameworkToggle"
326
+ },
327
+ {
328
+ "type": "null"
329
+ }
330
+ ]
331
+ },
332
+ "nx": {
333
+ "anyOf": [
334
+ {
335
+ "$ref": "#/definitions/FrameworkToggle"
336
+ },
337
+ {
338
+ "type": "null"
339
+ }
340
+ ]
341
+ },
342
+ "playwright": {
343
+ "anyOf": [
344
+ {
345
+ "$ref": "#/definitions/FrameworkToggle"
346
+ },
347
+ {
348
+ "type": "null"
349
+ }
350
+ ]
351
+ },
352
+ "remix": {
353
+ "anyOf": [
354
+ {
355
+ "$ref": "#/definitions/FrameworkToggle"
356
+ },
357
+ {
358
+ "type": "null"
359
+ }
360
+ ]
361
+ },
282
362
  "storybook": {
283
363
  "anyOf": [
284
364
  {
@@ -289,6 +369,26 @@
289
369
  }
290
370
  ]
291
371
  },
372
+ "sveltekit": {
373
+ "anyOf": [
374
+ {
375
+ "$ref": "#/definitions/FrameworkToggle"
376
+ },
377
+ {
378
+ "type": "null"
379
+ }
380
+ ]
381
+ },
382
+ "turborepo": {
383
+ "anyOf": [
384
+ {
385
+ "$ref": "#/definitions/FrameworkToggle"
386
+ },
387
+ {
388
+ "type": "null"
389
+ }
390
+ ]
391
+ },
292
392
  "vite": {
293
393
  "anyOf": [
294
394
  {
@@ -299,6 +399,16 @@
299
399
  }
300
400
  ]
301
401
  },
402
+ "vitepress": {
403
+ "anyOf": [
404
+ {
405
+ "$ref": "#/definitions/FrameworkToggle"
406
+ },
407
+ {
408
+ "type": "null"
409
+ }
410
+ ]
411
+ },
302
412
  "vitest": {
303
413
  "anyOf": [
304
414
  {
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;