depwire-cli 0.7.0 โ†’ 0.8.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/LICENSE CHANGED
@@ -1,3 +1,5 @@
1
+ SPDX-License-Identifier: BUSL-1.1
2
+
1
3
  Business Source License 1.1
2
4
 
3
5
  Parameters
package/README.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Depwire
2
2
 
3
+ <p align="center">
4
+ <a href="https://glama.ai/mcp/servers/depwire/depwire">
5
+ <img width="380" height="200" src="https://glama.ai/mcp/servers/depwire/depwire/badge" alt="Depwire MCP server" />
6
+ </a>
7
+ </p>
8
+
9
+ <p align="center">
10
+ <a href="https://www.npmjs.com/package/depwire-cli">
11
+ <img src="https://img.shields.io/npm/v/depwire-cli.svg?style=flat-square" alt="npm version" />
12
+ </a>
13
+ <a href="https://www.npmjs.com/package/depwire-cli">
14
+ <img src="https://img.shields.io/npm/dm/depwire-cli.svg?style=flat-square" alt="npm downloads" />
15
+ </a>
16
+ <a href="LICENSE">
17
+ <img src="https://img.shields.io/badge/license-BUSL--1.1-blue.svg?style=flat-square" alt="License" />
18
+ </a>
19
+ <a href="https://github.com/depwire/depwire/stargazers">
20
+ <img src="https://img.shields.io/github/stars/depwire/depwire.svg?style=flat-square" alt="GitHub stars" />
21
+ </a>
22
+ <a href="https://github.com/depwire/depwire/network/members">
23
+ <img src="https://img.shields.io/github/forks/depwire/depwire.svg?style=flat-square" alt="GitHub forks" />
24
+ </a>
25
+ </p>
26
+
3
27
  ![Depwire - Arc diagram visualization of the Hono framework](./assets/depwire-hero.png)
4
28
 
5
29
  **See how your code connects. Give AI tools full codebase context.**
@@ -10,9 +34,10 @@ Depwire analyzes codebases to build a cross-reference graph showing how every fi
10
34
 
11
35
  - ๐ŸŽจ **Beautiful arc diagram visualization** โ€” Interactive Harrison Bible-style graphic
12
36
  - ๐Ÿค– **MCP server for AI tools** โ€” Cursor, Claude Desktop get full dependency context
13
- - ๐Ÿ“Š **Dependency health score** โ€” 0-100 score across 6 dimensions (coupling, cohesion, circular deps, god files, orphans, depth)
14
- - ๐Ÿ“„ **Auto-generated documentation** โ€” 12 comprehensive documents: architecture, conventions, dependencies, onboarding, file catalog, API surface, error patterns, test coverage, git history, full snapshot, TODO/FIXME inventory, and health report
37
+ - ๐Ÿ“Š **Dependency health score** โ€” 0-100 score across 6 dimensions (coupling, cohesion, circular deps, god files, orphans & dead code, depth)
38
+ - ๐Ÿ“„ **Auto-generated documentation** โ€” 13 comprehensive documents: architecture, conventions, dependencies, onboarding, file catalog, API surface, error patterns, test coverage, git history, full snapshot, TODO/FIXME inventory, health report, and dead code analysis
15
39
  - ๐Ÿ” **Impact analysis** โ€” "What breaks if I rename this function?" answered precisely
40
+ - ๐Ÿงน **Dead code detection** โ€” Find symbols that are defined but never referenced, categorized by confidence level
16
41
  - ๐Ÿ‘€ **Live updates** โ€” Graph stays current as you edit code
17
42
  - ๐ŸŒ **Multi-language** โ€” TypeScript, JavaScript, Python, and Go
18
43
 
@@ -34,14 +59,14 @@ Depwire fixes this by giving AI tools a complete dependency graph of your codeba
34
59
  ### Ship Better Code
35
60
  - **Impact analysis for any change** โ€” renaming a function, moving a file, upgrading a dependency, deleting a module โ€” know the full blast radius before you touch anything
36
61
  - **Refactor with confidence** โ€” see every downstream consumer, every transitive dependency, 2-3 levels deep
37
- - **Catch dead code** โ€” find symbols nobody references anymore
62
+ - **Catch dead code** โ€” find symbols nobody references anymore with confidence-based classification (high/medium/low)
38
63
 
39
64
  ### Stay in Flow
40
65
  - **Live graph, always current** โ€” edit a file and the dependency map updates in real-time. No re-indexing, no waiting.
41
66
  - **Works locally, stays private** โ€” zero cloud accounts, zero data leaving your machine. Just `npm install` and go.
42
67
 
43
- ### 13 MCP Tools, Not Just Visualization
44
- Depwire isn't just a pretty graph. It's a full context engine with 13 tools that AI assistants call autonomously โ€” architecture summaries, dependency tracing, symbol search, file context, health scores, and more. The AI decides which tool to use based on your question.
68
+ ### 15 MCP Tools, Not Just Visualization
69
+ Depwire isn't just a pretty graph. It's a full context engine with 15 tools that AI assistants call autonomously โ€” architecture summaries, dependency tracing, symbol search, file context, health scores, dead code detection, temporal evolution, and more. The AI decides which tool to use based on your question.
45
70
 
46
71
  ## Installation
47
72
 
@@ -66,10 +91,17 @@ depwire viz
66
91
  depwire parse
67
92
  depwire docs
68
93
  depwire health
94
+ depwire dead-code
95
+ depwire temporal
69
96
 
70
97
  # Or specify a directory explicitly
71
98
  npx depwire-cli viz ./my-project
72
99
  npx depwire-cli parse ./my-project
100
+ npx depwire-cli dead-code ./my-project
101
+ npx depwire-cli temporal ./my-project
102
+
103
+ # Temporal visualization options
104
+ npx depwire-cli temporal --commits 20 --strategy monthly --verbose --stats
73
105
 
74
106
  # Exclude test files and node_modules
75
107
  npx depwire-cli parse --exclude "**/*.test.*" "**/node_modules/**"
@@ -134,6 +166,8 @@ Settings โ†’ Features โ†’ Experimental โ†’ Enable MCP โ†’ Add Server:
134
166
  | `get_project_docs` | Retrieve auto-generated codebase documentation |
135
167
  | `update_project_docs` | Regenerate documentation on demand |
136
168
  | `get_health_score` | Get 0-100 dependency health score with recommendations |
169
+ | `find_dead_code` | Find dead code โ€” symbols defined but never referenced |
170
+ | `get_temporal_graph` | Show how the graph evolved over git history |
137
171
 
138
172
  ## Supported Languages
139
173
 
@@ -179,6 +213,74 @@ Opens an interactive arc diagram in your browser:
179
213
  - Export as SVG or PNG
180
214
  - **Port collision handling** โ€” Automatically finds an available port if default is in use
181
215
 
216
+ ## Temporal Graph
217
+
218
+ Visualize how your codebase architecture evolved over git history. Scrub through time with an interactive timeline slider.
219
+
220
+ ![Depwire Temporal Graph](assets/depwire-temporal-hono.gif)
221
+
222
+ ```bash
223
+ # Auto-detects project root
224
+ depwire temporal
225
+
226
+ # Sample 20 commits with monthly snapshots
227
+ depwire temporal --commits 20 --strategy monthly
228
+
229
+ # Verbose mode with detailed progress
230
+ depwire temporal --verbose --stats
231
+
232
+ # Custom port
233
+ depwire temporal --port 3335
234
+ ```
235
+
236
+ **Options:**
237
+ - `--commits <number>` โ€” Number of commits to sample (default: 20)
238
+ - `--strategy <type>` โ€” Sampling strategy: `even`, `weekly`, `monthly` (default: `even`)
239
+ - `-p, --port <number>` โ€” Server port (default: 3334)
240
+ - `--output <path>` โ€” Save snapshots to custom path (default: `.depwire/temporal/`)
241
+ - `--verbose` โ€” Show progress for each commit being parsed
242
+ - `--stats` โ€” Show summary statistics at end
243
+
244
+ Opens an interactive temporal visualization in your browser:
245
+ - Timeline slider showing all sampled commits
246
+ - Arc diagram morphing between snapshots
247
+ - Play/pause animation with speed controls (0.5ร—, 1ร—, 2ร—)
248
+ - Statistics panel with growth deltas
249
+ - Evolution chart tracking files/symbols/edges over time
250
+ - Auto-zoom to fit all arcs on snapshot change
251
+ - Search to highlight specific files across time
252
+
253
+ ## ๐Ÿชฆ Dead Code Detection
254
+
255
+ Find unused symbols across your codebase before they become technical debt.
256
+
257
+ - Detects symbols with zero incoming references (never called, never imported)
258
+ - Confidence scoring: **high** (definitely dead), **medium** (probably dead), **low** (might be dead)
259
+ - Smart exclusion rules โ€” ignores entry points, test files, barrel files, and config files to reduce false positives
260
+ - Filter by confidence level, export as JSON for CI pipelines
261
+ - Integrated into the health score (orphans dimension)
262
+ - New MCP tool: `find_dead_code` โ€” AI assistants can query dead code directly
263
+ - New document generator: `DEAD_CODE.md` โ€” auto-generated dead code report
264
+
265
+ ```bash
266
+ depwire dead-code
267
+ depwire dead-code --confidence high
268
+ depwire dead-code --stats
269
+ depwire dead-code --json
270
+ ```
271
+
272
+ **Confidence Levels:**
273
+ - ๐Ÿ”ด **High confidence (definitely dead)**: Not exported with zero references, or exported but never used
274
+ - ๐ŸŸก **Medium confidence (probably dead)**: Exported from barrel files with zero dependents, or only used in test files
275
+ - โšช **Low confidence (might be dead)**: Exported from package entry points, types with zero dependents, or in dynamic-use directories (routes, middleware, etc.)
276
+
277
+ The dead code detector automatically excludes:
278
+ - Entry point files (index.ts, main.ts, server.ts, etc.)
279
+ - Test files (*.test.*, *.spec.*, __tests__/)
280
+ - Config files (*.config.*)
281
+ - Type declarations (*.d.ts)
282
+ - Framework auto-loaded directories (pages/, routes/, middleware/, commands/)
283
+
182
284
  ## How It Works
183
285
 
184
286
  1. **Parser** โ€” tree-sitter extracts every symbol and reference
@@ -316,6 +418,7 @@ depwire docs --update --only conventions
316
418
  | `CURRENT.md` | Complete codebase snapshot (every file, symbol, connection) |
317
419
  | `STATUS.md` | TODO/FIXME/HACK inventory with priority matrix |
318
420
  | `HEALTH.md` | Dependency health score (0-100) across 6 dimensions with recommendations |
421
+ | `DEAD_CODE.md` | Dead code analysis โ€” unused symbols by confidence level (high/medium/low) |
319
422
 
320
423
  Documents are stored in `.depwire/` with `metadata.json` tracking generation timestamps for staleness detection.
321
424
 
@@ -360,6 +463,94 @@ depwire health --json
360
463
 
361
464
  Health history is stored in `.depwire/health-history.json` (last 50 checks).
362
465
 
466
+ ### `depwire dead-code [directory]`
467
+
468
+ Detect unused symbols across your codebase with confidence-based classification.
469
+
470
+ **Directory argument is optional** โ€” Auto-detects project root.
471
+
472
+ **Options:**
473
+ - `--confidence <level>` โ€” Minimum confidence level to show: `high`, `medium`, `low` (default: `medium`)
474
+ - `--include-low` โ€” Shortcut for `--confidence low`
475
+ - `--verbose` โ€” Show detailed info for each dead symbol (file, line, kind, reason)
476
+ - `--stats` โ€” Show summary statistics
477
+ - `--include-tests` โ€” Include test files in analysis (excluded by default)
478
+ - `--json` โ€” Output as JSON for CI/automation
479
+
480
+ **Confidence Levels:**
481
+ - ๐Ÿ”ด **High confidence (definitely dead)**: Not exported with zero references, or exported but never used
482
+ - ๐ŸŸก **Medium confidence (probably dead)**: Exported from barrel files with zero dependents, or only used in test files
483
+ - โšช **Low confidence (might be dead)**: Exported from package entry points, types with zero dependents, or in dynamic-use directories (routes, middleware, etc.)
484
+
485
+ **Examples:**
486
+ ```bash
487
+ # Analyze dead code (default: medium confidence and above)
488
+ depwire dead-code
489
+
490
+ # Show only high-confidence dead code
491
+ depwire dead-code --confidence high
492
+
493
+ # Show all potential dead code (including low confidence)
494
+ depwire dead-code --confidence low
495
+ # Or use shortcut
496
+ depwire dead-code --include-low
497
+
498
+ # Detailed analysis with reasons and statistics
499
+ depwire dead-code --verbose --stats
500
+
501
+ # Include test files in analysis (excluded by default)
502
+ depwire dead-code --include-tests
503
+
504
+ # JSON output for CI/automation
505
+ depwire dead-code --json
506
+ ```
507
+
508
+ **Automatic Exclusions:**
509
+ The dead code detector automatically excludes:
510
+ - Entry point files (index.ts, main.ts, server.ts, etc.)
511
+ - Test files (*.test.*, *.spec.*, __tests__/)
512
+ - Config files (*.config.*)
513
+ - Type declarations (*.d.ts)
514
+ - Framework auto-loaded directories (pages/, routes/, middleware/, commands/)
515
+
516
+ ### `depwire temporal [directory]`
517
+
518
+ Visualize how the dependency graph evolved over git history.
519
+
520
+ **Directory argument is optional** โ€” Auto-detects project root.
521
+
522
+ **Options:**
523
+ - `--commits <number>` โ€” Number of commits to sample (default: 20)
524
+ - `--strategy <type>` โ€” Sampling strategy: `even` (every Nth), `weekly`, `monthly` (default: `even`)
525
+ - `-p, --port <number>` โ€” Server port (default: 3334)
526
+ - `--output <path>` โ€” Save snapshots to custom path (default: `.depwire/temporal/`)
527
+ - `--verbose` โ€” Show progress for each commit being parsed
528
+ - `--stats` โ€” Show summary statistics at end
529
+
530
+ **Examples:**
531
+ ```bash
532
+ # Auto-detect and analyze 20 commits
533
+ depwire temporal
534
+
535
+ # Sample 50 commits with monthly snapshots
536
+ depwire temporal --commits 50 --strategy monthly
537
+
538
+ # Verbose mode with stats
539
+ depwire temporal --verbose --stats
540
+
541
+ # Custom output directory
542
+ depwire temporal --output ./temp-snapshots
543
+ ```
544
+
545
+ **Output:**
546
+ - Interactive browser visualization at `http://127.0.0.1:3334`
547
+ - Timeline slider to scrub through git history
548
+ - Arc diagram morphing between snapshots
549
+ - Growth statistics showing files/symbols/edges evolution
550
+ - Auto-zoom to fit full diagram on each snapshot change
551
+
552
+ Snapshots are cached in `.depwire/temporal/` for fast re-rendering.
553
+
363
554
  ### Error Handling
364
555
 
365
556
  Depwire gracefully handles parse errors:
@@ -2009,10 +2009,10 @@ async function parseProject(projectRoot, options) {
2009
2009
  try {
2010
2010
  const fullPath = join6(projectRoot, file);
2011
2011
  if (options?.exclude) {
2012
- const shouldExclude = options.exclude.some(
2012
+ const shouldExclude2 = options.exclude.some(
2013
2013
  (pattern) => minimatch(file, pattern, { matchBase: true })
2014
2014
  );
2015
- if (shouldExclude) {
2015
+ if (shouldExclude2) {
2016
2016
  if (options.verbose) {
2017
2017
  console.error(`[Parser] Excluded: ${file}`);
2018
2018
  }
@@ -2820,11 +2820,11 @@ function calculateCircularDepsScore(graph) {
2820
2820
  const visited = /* @__PURE__ */ new Set();
2821
2821
  const recStack = /* @__PURE__ */ new Set();
2822
2822
  const cycles = [];
2823
- function dfs(node, path2) {
2823
+ function dfs(node, path6) {
2824
2824
  if (recStack.has(node)) {
2825
- const cycleStart = path2.indexOf(node);
2825
+ const cycleStart = path6.indexOf(node);
2826
2826
  if (cycleStart >= 0) {
2827
- cycles.push(path2.slice(cycleStart));
2827
+ cycles.push(path6.slice(cycleStart));
2828
2828
  }
2829
2829
  return;
2830
2830
  }
@@ -2833,11 +2833,11 @@ function calculateCircularDepsScore(graph) {
2833
2833
  }
2834
2834
  visited.add(node);
2835
2835
  recStack.add(node);
2836
- path2.push(node);
2836
+ path6.push(node);
2837
2837
  const neighbors = fileGraph.get(node);
2838
2838
  if (neighbors) {
2839
2839
  for (const neighbor of neighbors) {
2840
- dfs(neighbor, [...path2]);
2840
+ dfs(neighbor, [...path6]);
2841
2841
  }
2842
2842
  }
2843
2843
  recStack.delete(node);
@@ -2939,25 +2939,40 @@ function calculateOrphansScore(graph) {
2939
2939
  });
2940
2940
  const orphanCount = files.size - connectedFiles.size;
2941
2941
  const orphanPercent = files.size > 0 ? orphanCount / files.size * 100 : 0;
2942
+ const totalSymbols = graph.order;
2943
+ let deadSymbolCount = 0;
2944
+ graph.forEachNode((node) => {
2945
+ const inDegree = graph.inDegree(node);
2946
+ if (inDegree === 0) {
2947
+ deadSymbolCount++;
2948
+ }
2949
+ });
2950
+ const deadCodePercent = totalSymbols > 0 ? deadSymbolCount / totalSymbols * 100 : 0;
2951
+ const combinedScore = (orphanPercent + deadCodePercent) / 2;
2942
2952
  let score = 100;
2943
- if (orphanPercent === 0) {
2953
+ if (combinedScore === 0) {
2944
2954
  score = 100;
2945
- } else if (orphanPercent <= 5) {
2955
+ } else if (combinedScore <= 5) {
2946
2956
  score = 80;
2947
- } else if (orphanPercent <= 10) {
2957
+ } else if (combinedScore <= 10) {
2948
2958
  score = 60;
2949
- } else if (orphanPercent <= 20) {
2959
+ } else if (combinedScore <= 20) {
2950
2960
  score = 40;
2951
2961
  } else {
2952
2962
  score = 20;
2953
2963
  }
2954
2964
  return {
2955
- name: "Orphan Files",
2965
+ name: "Orphans & Dead Code",
2956
2966
  score,
2957
2967
  weight: 0.1,
2958
2968
  grade: scoreToGrade(score),
2959
- details: orphanCount === 0 ? "No orphan files" : `${orphanCount} orphan file${orphanCount === 1 ? "" : "s"} (${orphanPercent.toFixed(0)}%)`,
2960
- metrics: { orphans: orphanCount, percentage: parseFloat(orphanPercent.toFixed(1)) }
2969
+ details: `${orphanCount} orphan file${orphanCount === 1 ? "" : "s"} (${orphanPercent.toFixed(0)}%), ${deadSymbolCount} dead symbols (${deadCodePercent.toFixed(1)}%)`,
2970
+ metrics: {
2971
+ orphans: orphanCount,
2972
+ orphanPercentage: parseFloat(orphanPercent.toFixed(1)),
2973
+ deadSymbols: deadSymbolCount,
2974
+ deadCodePercentage: parseFloat(deadCodePercent.toFixed(1))
2975
+ }
2961
2976
  };
2962
2977
  }
2963
2978
  function calculateDepthScore(graph) {
@@ -3159,6 +3174,304 @@ function loadHealthHistory(projectRoot) {
3159
3174
  }
3160
3175
  }
3161
3176
 
3177
+ // src/dead-code/detector.ts
3178
+ import path2 from "path";
3179
+ function findDeadSymbols(graph, projectRoot, includeTests = false) {
3180
+ const deadSymbols = [];
3181
+ const context = { graph, projectRoot };
3182
+ for (const node of graph.nodes()) {
3183
+ const attrs = graph.getNodeAttributes(node);
3184
+ if (!attrs.file || !attrs.name) continue;
3185
+ const inDegree = graph.inDegree(node);
3186
+ if (inDegree === 0) {
3187
+ if (shouldExclude(attrs, context, includeTests)) {
3188
+ continue;
3189
+ }
3190
+ deadSymbols.push({
3191
+ name: attrs.name,
3192
+ kind: attrs.kind || "unknown",
3193
+ file: attrs.file,
3194
+ line: attrs.startLine || 0,
3195
+ exported: attrs.exported || false,
3196
+ dependents: 0,
3197
+ confidence: "high",
3198
+ reason: "Zero dependents"
3199
+ });
3200
+ }
3201
+ }
3202
+ return deadSymbols;
3203
+ }
3204
+ function shouldExclude(attrs, context, includeTests) {
3205
+ const filePath = attrs.file;
3206
+ const relativePath = path2.relative(context.projectRoot, filePath);
3207
+ if (!includeTests && isTestFile(relativePath)) {
3208
+ return true;
3209
+ }
3210
+ if (isEntryPoint(relativePath)) {
3211
+ return true;
3212
+ }
3213
+ if (isConfigFile(relativePath)) {
3214
+ return true;
3215
+ }
3216
+ if (isTypeDeclarationFile(relativePath)) {
3217
+ return true;
3218
+ }
3219
+ if (attrs.kind === "default") {
3220
+ return true;
3221
+ }
3222
+ if (isFrameworkAutoLoadedFile(relativePath)) {
3223
+ return true;
3224
+ }
3225
+ return false;
3226
+ }
3227
+ function isTestFile(filePath) {
3228
+ return filePath.includes("__tests__/") || filePath.includes(".test.") || filePath.includes(".spec.") || filePath.includes("/test/") || filePath.includes("/tests/");
3229
+ }
3230
+ function isEntryPoint(filePath) {
3231
+ const basename6 = path2.basename(filePath);
3232
+ return basename6 === "index.ts" || basename6 === "index.js" || basename6 === "main.ts" || basename6 === "main.js" || basename6 === "app.ts" || basename6 === "app.js" || basename6 === "server.ts" || basename6 === "server.js";
3233
+ }
3234
+ function isConfigFile(filePath) {
3235
+ return filePath.includes(".config.") || filePath.includes("config/") || filePath.includes("vite.config") || filePath.includes("rollup.config") || filePath.includes("webpack.config");
3236
+ }
3237
+ function isTypeDeclarationFile(filePath) {
3238
+ return filePath.endsWith(".d.ts");
3239
+ }
3240
+ function isFrameworkAutoLoadedFile(filePath) {
3241
+ return filePath.includes("/pages/") || filePath.includes("/routes/") || filePath.includes("/middleware/") || filePath.includes("/commands/") || filePath.includes("/api/") || filePath.includes("/app/");
3242
+ }
3243
+
3244
+ // src/dead-code/classifier.ts
3245
+ import path3 from "path";
3246
+ function classifyDeadSymbols(symbols, graph) {
3247
+ return symbols.map((symbol) => {
3248
+ const confidence = calculateConfidence(symbol, graph);
3249
+ const reason = generateReason(symbol, confidence);
3250
+ return {
3251
+ ...symbol,
3252
+ confidence,
3253
+ reason
3254
+ };
3255
+ });
3256
+ }
3257
+ function calculateConfidence(symbol, graph) {
3258
+ if (!symbol.exported && symbol.dependents === 0) {
3259
+ return "high";
3260
+ }
3261
+ if (symbol.exported && symbol.dependents === 0 && !isBarrelFile(symbol.file)) {
3262
+ return "high";
3263
+ }
3264
+ if (symbol.exported && symbol.dependents === 0 && isBarrelFile(symbol.file)) {
3265
+ return "medium";
3266
+ }
3267
+ const dependents = getSymbolDependents(symbol, graph);
3268
+ if (dependents.length === 1 && isTestFile2(dependents[0])) {
3269
+ return "medium";
3270
+ }
3271
+ if (symbol.exported && isPackageEntryPoint(symbol.file)) {
3272
+ return "low";
3273
+ }
3274
+ if ((symbol.kind === "interface" || symbol.kind === "type") && symbol.dependents === 0) {
3275
+ return "low";
3276
+ }
3277
+ if (isLikelyDynamicUsage(symbol)) {
3278
+ return "low";
3279
+ }
3280
+ return "medium";
3281
+ }
3282
+ function generateReason(symbol, confidence) {
3283
+ if (!symbol.exported && symbol.dependents === 0) {
3284
+ return "Not exported, zero references";
3285
+ }
3286
+ if (symbol.exported && symbol.dependents === 0 && !isBarrelFile(symbol.file)) {
3287
+ return "Exported, zero dependents";
3288
+ }
3289
+ if (symbol.exported && symbol.dependents === 0 && isBarrelFile(symbol.file)) {
3290
+ return "Exported from barrel file, zero dependents (might be used externally)";
3291
+ }
3292
+ if (confidence === "medium") {
3293
+ return "Low usage, might be dead";
3294
+ }
3295
+ if (confidence === "low") {
3296
+ if (symbol.kind === "interface" || symbol.kind === "type") {
3297
+ return "Type with zero dependents (might be used via import type)";
3298
+ }
3299
+ if (isPackageEntryPoint(symbol.file)) {
3300
+ return "Exported from package entry point (might be public API)";
3301
+ }
3302
+ if (isLikelyDynamicUsage(symbol)) {
3303
+ return "In dynamic-use pattern directory (might be auto-loaded)";
3304
+ }
3305
+ }
3306
+ return "Potentially unused";
3307
+ }
3308
+ function isBarrelFile(filePath) {
3309
+ const basename6 = path3.basename(filePath);
3310
+ return basename6 === "index.ts" || basename6 === "index.js";
3311
+ }
3312
+ function isTestFile2(filePath) {
3313
+ return filePath.includes("__tests__/") || filePath.includes(".test.") || filePath.includes(".spec.") || filePath.includes("/test/") || filePath.includes("/tests/");
3314
+ }
3315
+ function isPackageEntryPoint(filePath) {
3316
+ return filePath.includes("/src/index.") || filePath.includes("/lib/index.") || filePath.endsWith("/index.ts") || filePath.endsWith("/index.js");
3317
+ }
3318
+ function isLikelyDynamicUsage(symbol) {
3319
+ const filePath = symbol.file;
3320
+ return filePath.includes("/routes/") || filePath.includes("/pages/") || filePath.includes("/middleware/") || filePath.includes("/commands/") || filePath.includes("/handlers/") || filePath.includes("/api/");
3321
+ }
3322
+ function getSymbolDependents(symbol, graph) {
3323
+ const dependents = [];
3324
+ for (const node of graph.nodes()) {
3325
+ const attrs = graph.getNodeAttributes(node);
3326
+ if (attrs.file === symbol.file && attrs.name === symbol.name) {
3327
+ const inNeighbors = graph.inNeighbors(node);
3328
+ for (const neighbor of inNeighbors) {
3329
+ const neighborAttrs = graph.getNodeAttributes(neighbor);
3330
+ if (neighborAttrs.file) {
3331
+ dependents.push(neighborAttrs.file);
3332
+ }
3333
+ }
3334
+ break;
3335
+ }
3336
+ }
3337
+ return dependents;
3338
+ }
3339
+
3340
+ // src/dead-code/display.ts
3341
+ import chalk from "chalk";
3342
+ import path4 from "path";
3343
+ function displayDeadCodeReport(report, options, projectRoot) {
3344
+ if (options.json) {
3345
+ console.log(JSON.stringify(report, null, 2));
3346
+ return;
3347
+ }
3348
+ console.log(chalk.cyan.bold("\n\u{1F50D} Dead Code Analysis\n"));
3349
+ const { high, medium, low } = report.byConfidence;
3350
+ console.log(
3351
+ `Found ${chalk.yellow.bold(report.deadSymbols)} potentially dead symbols (${chalk.red(high)} high, ${chalk.yellow(medium)} medium, ${chalk.gray(low)} low confidence)
3352
+ `
3353
+ );
3354
+ const symbolsByConfidence = groupByConfidence(report.symbols);
3355
+ if (symbolsByConfidence.high.length > 0) {
3356
+ displayConfidenceGroup("HIGH", symbolsByConfidence.high, options.verbose, projectRoot);
3357
+ }
3358
+ if (symbolsByConfidence.medium.length > 0) {
3359
+ displayConfidenceGroup("MEDIUM", symbolsByConfidence.medium, options.verbose, projectRoot);
3360
+ }
3361
+ if (symbolsByConfidence.low.length > 0) {
3362
+ displayConfidenceGroup("LOW", symbolsByConfidence.low, options.verbose, projectRoot);
3363
+ }
3364
+ if (options.stats) {
3365
+ displayStats(report);
3366
+ }
3367
+ }
3368
+ function groupByConfidence(symbols) {
3369
+ return symbols.reduce(
3370
+ (acc, symbol) => {
3371
+ acc[symbol.confidence].push(symbol);
3372
+ return acc;
3373
+ },
3374
+ { high: [], medium: [], low: [] }
3375
+ );
3376
+ }
3377
+ function displayConfidenceGroup(level, symbols, verbose, projectRoot) {
3378
+ const emoji = level === "HIGH" ? "\u{1F534}" : level === "MEDIUM" ? "\u{1F7E1}" : "\u26AA";
3379
+ const color = level === "HIGH" ? chalk.red : level === "MEDIUM" ? chalk.yellow : chalk.gray;
3380
+ console.log(
3381
+ color.bold(`
3382
+ ${emoji} ${level} CONFIDENCE `) + chalk.gray(`(${level === "HIGH" ? "definitely" : level === "MEDIUM" ? "probably" : "might be"} dead)`)
3383
+ );
3384
+ const headers = ["Symbol", "Kind", "File", verbose ? "Reason" : ""];
3385
+ const rows = symbols.map((symbol) => {
3386
+ const relativePath = path4.relative(projectRoot, symbol.file);
3387
+ return [
3388
+ chalk.bold(symbol.name),
3389
+ symbol.kind,
3390
+ `${relativePath}:${symbol.line}`,
3391
+ verbose ? symbol.reason : ""
3392
+ ];
3393
+ });
3394
+ displayTable(headers, rows.filter((row) => row[3] !== ""));
3395
+ }
3396
+ function displayTable(headers, rows) {
3397
+ if (rows.length === 0) return;
3398
+ const columnWidths = headers.map((header2, i) => {
3399
+ const maxRowWidth = Math.max(...rows.map((row) => stripAnsi(row[i]).length));
3400
+ return Math.max(header2.length, maxRowWidth);
3401
+ });
3402
+ const separator = "\u250C" + columnWidths.map((w) => "\u2500".repeat(w + 2)).join("\u252C") + "\u2510";
3403
+ const headerRow = "\u2502 " + headers.map((h, i) => h.padEnd(columnWidths[i])).join(" \u2502 ") + " \u2502";
3404
+ const divider = "\u251C" + columnWidths.map((w) => "\u2500".repeat(w + 2)).join("\u253C") + "\u2524";
3405
+ const footer = "\u2514" + columnWidths.map((w) => "\u2500".repeat(w + 2)).join("\u2534") + "\u2518";
3406
+ console.log(separator);
3407
+ console.log(headerRow);
3408
+ console.log(divider);
3409
+ for (const row of rows) {
3410
+ const formattedRow = "\u2502 " + row.map((cell, i) => {
3411
+ const stripped = stripAnsi(cell);
3412
+ const padding = columnWidths[i] - stripped.length;
3413
+ return cell + " ".repeat(padding);
3414
+ }).join(" \u2502 ") + " \u2502";
3415
+ console.log(formattedRow);
3416
+ }
3417
+ console.log(footer);
3418
+ }
3419
+ function displayStats(report) {
3420
+ console.log(chalk.cyan.bold("\n\u{1F4CA} Summary\n"));
3421
+ console.log(` Total symbols analyzed: ${chalk.bold(report.totalSymbols.toLocaleString())}`);
3422
+ console.log(` Potentially dead: ${chalk.yellow.bold(report.deadSymbols)} (${report.deadPercentage.toFixed(1)}%)`);
3423
+ console.log(
3424
+ ` By confidence: ${chalk.red(report.byConfidence.high)} high, ${chalk.yellow(report.byConfidence.medium)} medium, ${chalk.gray(report.byConfidence.low)} low`
3425
+ );
3426
+ const estimatedLines = report.deadSymbols * 18;
3427
+ console.log(` Estimated dead code: ${chalk.gray(`~${estimatedLines.toLocaleString()} lines`)}
3428
+ `);
3429
+ }
3430
+ function stripAnsi(str) {
3431
+ return str.replace(
3432
+ /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
3433
+ ""
3434
+ );
3435
+ }
3436
+
3437
+ // src/dead-code/index.ts
3438
+ function analyzeDeadCode(graph, projectRoot, options = {}) {
3439
+ const opts = {
3440
+ confidence: options.confidence || "medium",
3441
+ includeTests: options.includeTests || false,
3442
+ verbose: options.verbose || false,
3443
+ stats: options.stats || false,
3444
+ json: options.json || false
3445
+ };
3446
+ const rawDeadSymbols = findDeadSymbols(graph, projectRoot, opts.includeTests);
3447
+ const classifiedSymbols = classifyDeadSymbols(rawDeadSymbols, graph);
3448
+ const filteredSymbols = filterByConfidence(classifiedSymbols, opts.confidence);
3449
+ const totalSymbols = graph.order;
3450
+ const byConfidence = {
3451
+ high: classifiedSymbols.filter((s) => s.confidence === "high").length,
3452
+ medium: classifiedSymbols.filter((s) => s.confidence === "medium").length,
3453
+ low: classifiedSymbols.filter((s) => s.confidence === "low").length
3454
+ };
3455
+ const report = {
3456
+ totalSymbols,
3457
+ deadSymbols: filteredSymbols.length,
3458
+ deadPercentage: filteredSymbols.length / totalSymbols * 100,
3459
+ byConfidence,
3460
+ symbols: filteredSymbols
3461
+ };
3462
+ if (!opts.json) {
3463
+ displayDeadCodeReport(report, opts, projectRoot);
3464
+ }
3465
+ return report;
3466
+ }
3467
+ function filterByConfidence(symbols, minConfidence) {
3468
+ const confidenceLevels = { high: 3, medium: 2, low: 1 };
3469
+ const minLevel = confidenceLevels[minConfidence];
3470
+ return symbols.filter(
3471
+ (s) => confidenceLevels[s.confidence] >= minLevel
3472
+ );
3473
+ }
3474
+
3162
3475
  // src/docs/generator.ts
3163
3476
  import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync9 } from "fs";
3164
3477
  import { join as join12 } from "path";
@@ -4208,20 +4521,20 @@ function findLongestPaths(graph, limit) {
4208
4521
  }
4209
4522
  const allPaths = [];
4210
4523
  const visited = /* @__PURE__ */ new Set();
4211
- function dfs(file, path2) {
4524
+ function dfs(file, path6) {
4212
4525
  visited.add(file);
4213
- path2.push(file);
4526
+ path6.push(file);
4214
4527
  const neighbors = fileGraph.get(file);
4215
4528
  if (!neighbors || neighbors.size === 0) {
4216
- allPaths.push([...path2]);
4529
+ allPaths.push([...path6]);
4217
4530
  } else {
4218
4531
  for (const neighbor of neighbors) {
4219
4532
  if (!visited.has(neighbor)) {
4220
- dfs(neighbor, path2);
4533
+ dfs(neighbor, path6);
4221
4534
  }
4222
4535
  }
4223
4536
  }
4224
- path2.pop();
4537
+ path6.pop();
4225
4538
  visited.delete(file);
4226
4539
  }
4227
4540
  for (const root of roots.slice(0, 10)) {
@@ -5499,7 +5812,7 @@ function getFileCount8(graph) {
5499
5812
  });
5500
5813
  return files.size;
5501
5814
  }
5502
- function isTestFile(filePath) {
5815
+ function isTestFile3(filePath) {
5503
5816
  const fileName = basename4(filePath).toLowerCase();
5504
5817
  const dirPath = dirname11(filePath).toLowerCase();
5505
5818
  if (dirPath.includes("test") || dirPath.includes("spec") || dirPath.includes("__tests__")) {
@@ -5513,7 +5826,7 @@ function isTestFile(filePath) {
5513
5826
  function getTestFiles(graph) {
5514
5827
  const testFiles = /* @__PURE__ */ new Map();
5515
5828
  graph.forEachNode((node, attrs) => {
5516
- if (isTestFile(attrs.filePath)) {
5829
+ if (isTestFile3(attrs.filePath)) {
5517
5830
  if (!testFiles.has(attrs.filePath)) {
5518
5831
  testFiles.set(attrs.filePath, {
5519
5832
  filePath: attrs.filePath,
@@ -5572,9 +5885,9 @@ function matchTestToSource(testFile) {
5572
5885
  const srcDir = testDir.replace(/test[s]?/g, "src");
5573
5886
  possiblePaths.push(srcDir + "/" + sourceFileName);
5574
5887
  }
5575
- for (const path2 of possiblePaths) {
5576
- if (!isTestFile(path2)) {
5577
- return path2;
5888
+ for (const path6 of possiblePaths) {
5889
+ if (!isTestFile3(path6)) {
5890
+ return path6;
5578
5891
  }
5579
5892
  }
5580
5893
  return null;
@@ -5629,7 +5942,7 @@ function generateUntestedFiles(graph) {
5629
5942
  allFiles.add(attrs.filePath);
5630
5943
  });
5631
5944
  for (const file of allFiles) {
5632
- if (!isTestFile(file)) {
5945
+ if (!isTestFile3(file)) {
5633
5946
  sourceFiles.push(file);
5634
5947
  }
5635
5948
  }
@@ -5688,7 +6001,7 @@ function generateTestCoverageMap(graph) {
5688
6001
  allFiles.add(attrs.filePath);
5689
6002
  });
5690
6003
  for (const file of allFiles) {
5691
- if (!isTestFile(file)) {
6004
+ if (!isTestFile3(file)) {
5692
6005
  sourceFiles.push(file);
5693
6006
  }
5694
6007
  }
@@ -5740,7 +6053,7 @@ function generateTestStatistics(graph) {
5740
6053
  allFiles.add(attrs.filePath);
5741
6054
  });
5742
6055
  for (const file of allFiles) {
5743
- if (!isTestFile(file)) {
6056
+ if (!isTestFile3(file)) {
5744
6057
  sourceFiles.push(file);
5745
6058
  }
5746
6059
  }
@@ -7005,6 +7318,98 @@ function generateDetailedMetrics(dimensions) {
7005
7318
  return output;
7006
7319
  }
7007
7320
 
7321
+ // src/docs/dead-code.ts
7322
+ import path5 from "path";
7323
+ function generateDeadCode(graph, projectRoot, projectName) {
7324
+ const report = analyzeDeadCode(graph, projectRoot, {
7325
+ confidence: "low",
7326
+ includeTests: false,
7327
+ verbose: true,
7328
+ stats: true,
7329
+ json: true
7330
+ });
7331
+ let output = "";
7332
+ output += header(`${projectName} - Dead Code Analysis`, 1);
7333
+ output += timestamp();
7334
+ output += "\n";
7335
+ output += header("Summary", 2);
7336
+ output += `Total symbols analyzed: **${formatNumber(report.totalSymbols)}**
7337
+
7338
+ `;
7339
+ output += `Potentially dead symbols: **${formatNumber(report.deadSymbols)}** (${report.deadPercentage.toFixed(1)}%)
7340
+
7341
+ `;
7342
+ output += `- \u{1F534} High confidence (definitely dead): **${report.byConfidence.high}**
7343
+ `;
7344
+ output += `- \u{1F7E1} Medium confidence (probably dead): **${report.byConfidence.medium}**
7345
+ `;
7346
+ output += `- \u26AA Low confidence (might be dead): **${report.byConfidence.low}**
7347
+
7348
+ `;
7349
+ const estimatedLines = report.deadSymbols * 18;
7350
+ output += `Estimated dead code: **~${formatNumber(estimatedLines)} lines**
7351
+
7352
+ `;
7353
+ const symbolsByConfidence = groupByConfidence2(report.symbols);
7354
+ if (symbolsByConfidence.high.length > 0) {
7355
+ output += generateConfidenceSection(
7356
+ "High Confidence",
7357
+ "definitely dead",
7358
+ symbolsByConfidence.high,
7359
+ projectRoot
7360
+ );
7361
+ }
7362
+ if (symbolsByConfidence.medium.length > 0) {
7363
+ output += generateConfidenceSection(
7364
+ "Medium Confidence",
7365
+ "probably dead",
7366
+ symbolsByConfidence.medium,
7367
+ projectRoot
7368
+ );
7369
+ }
7370
+ if (symbolsByConfidence.low.length > 0) {
7371
+ output += generateConfidenceSection(
7372
+ "Low Confidence",
7373
+ "might be dead",
7374
+ symbolsByConfidence.low,
7375
+ projectRoot
7376
+ );
7377
+ }
7378
+ output += "\n---\n\n";
7379
+ output += "_This document was auto-generated by Depwire. It reflects the current state of the codebase and should be reviewed before taking action._\n";
7380
+ return output;
7381
+ }
7382
+ function groupByConfidence2(symbols) {
7383
+ return symbols.reduce(
7384
+ (acc, symbol) => {
7385
+ acc[symbol.confidence].push(symbol);
7386
+ return acc;
7387
+ },
7388
+ { high: [], medium: [], low: [] }
7389
+ );
7390
+ }
7391
+ function generateConfidenceSection(title, description, symbols, projectRoot) {
7392
+ let output = "";
7393
+ output += header(`${title} (${description})`, 2);
7394
+ output += `Found **${symbols.length}** symbol${symbols.length === 1 ? "" : "s"}.
7395
+
7396
+ `;
7397
+ const headers = ["Symbol", "Kind", "File", "Exported", "Reason"];
7398
+ const rows = symbols.map((s) => {
7399
+ const relativePath = path5.relative(projectRoot, s.file);
7400
+ return [
7401
+ code(s.name),
7402
+ s.kind,
7403
+ `${relativePath}:${s.line}`,
7404
+ s.exported ? "Yes" : "No",
7405
+ s.reason
7406
+ ];
7407
+ });
7408
+ output += table(headers, rows);
7409
+ output += "\n";
7410
+ return output;
7411
+ }
7412
+
7008
7413
  // src/docs/metadata.ts
7009
7414
  import { existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
7010
7415
  import { join as join11 } from "path";
@@ -7088,7 +7493,8 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
7088
7493
  "history",
7089
7494
  "current",
7090
7495
  "status",
7091
- "health"
7496
+ "health",
7497
+ "dead_code"
7092
7498
  ];
7093
7499
  }
7094
7500
  let metadata = null;
@@ -7231,6 +7637,17 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
7231
7637
  errors.push(`Failed to generate HEALTH.md: ${err}`);
7232
7638
  }
7233
7639
  }
7640
+ if (docsToGenerate.includes("dead_code")) {
7641
+ try {
7642
+ if (options.verbose) console.log("Generating DEAD_CODE.md...");
7643
+ const content = generateDeadCode(graph, projectRoot, version);
7644
+ const filePath = join12(options.outputDir, "DEAD_CODE.md");
7645
+ writeFileSync3(filePath, content, "utf-8");
7646
+ generated.push("DEAD_CODE.md");
7647
+ } catch (err) {
7648
+ errors.push(`Failed to generate DEAD_CODE.md: ${err}`);
7649
+ }
7650
+ }
7234
7651
  } else if (options.format === "json") {
7235
7652
  errors.push("JSON format not yet supported");
7236
7653
  }
@@ -7689,8 +8106,8 @@ function createSnapshot(graph, commitHash, commitDate, commitMessage, commitAuth
7689
8106
  }
7690
8107
  }
7691
8108
  }
7692
- const files = Array.from(fileMap.entries()).map(([path2, data]) => ({
7693
- path: path2,
8109
+ const files = Array.from(fileMap.entries()).map(([path6, data]) => ({
8110
+ path: path6,
7694
8111
  symbols: data.symbols,
7695
8112
  connections: data.inbound + data.outbound
7696
8113
  }));
@@ -7927,6 +8344,21 @@ function getToolsList() {
7927
8344
  }
7928
8345
  }
7929
8346
  }
8347
+ },
8348
+ {
8349
+ name: "find_dead_code",
8350
+ description: "Find potentially dead code \u2014 symbols that are defined but never referenced anywhere in the codebase. Returns symbols categorized by confidence level (high, medium, low). High confidence means definitely unused. Use this to identify cleanup opportunities.",
8351
+ inputSchema: {
8352
+ type: "object",
8353
+ properties: {
8354
+ confidence: {
8355
+ type: "string",
8356
+ enum: ["high", "medium", "low"],
8357
+ description: "Minimum confidence level to return (default: medium)",
8358
+ default: "medium"
8359
+ }
8360
+ }
8361
+ }
7930
8362
  }
7931
8363
  ];
7932
8364
  }
@@ -7989,6 +8421,15 @@ async function handleToolCall(name, args, state) {
7989
8421
  } else {
7990
8422
  result = await handleGetTemporalGraph(state, args.commits || 10, args.strategy || "even");
7991
8423
  }
8424
+ } else if (name === "find_dead_code") {
8425
+ if (!isProjectLoaded(state)) {
8426
+ result = {
8427
+ error: "No project loaded",
8428
+ message: "Use connect_repo to connect to a codebase first"
8429
+ };
8430
+ } else {
8431
+ result = handleFindDeadCode(state, args.confidence || "medium");
8432
+ }
7992
8433
  } else {
7993
8434
  if (!isProjectLoaded(state)) {
7994
8435
  result = {
@@ -8604,6 +9045,46 @@ async function handleGetTemporalGraph(state, commits, strategy) {
8604
9045
  };
8605
9046
  }
8606
9047
  }
9048
+ function handleFindDeadCode(state, confidence) {
9049
+ if (!state.graph || !state.projectRoot) {
9050
+ return {
9051
+ error: "No project loaded",
9052
+ message: "Use connect_repo to connect to a codebase first"
9053
+ };
9054
+ }
9055
+ try {
9056
+ const report = analyzeDeadCode(state.graph, state.projectRoot, {
9057
+ confidence,
9058
+ includeTests: false,
9059
+ verbose: false,
9060
+ stats: false,
9061
+ json: true
9062
+ });
9063
+ return {
9064
+ status: "success",
9065
+ totalSymbols: report.totalSymbols,
9066
+ deadSymbols: report.deadSymbols,
9067
+ deadPercentage: report.deadPercentage,
9068
+ byConfidence: report.byConfidence,
9069
+ symbols: report.symbols.map((s) => ({
9070
+ name: s.name,
9071
+ kind: s.kind,
9072
+ file: s.file,
9073
+ line: s.line,
9074
+ exported: s.exported,
9075
+ dependents: s.dependents,
9076
+ confidence: s.confidence,
9077
+ reason: s.reason
9078
+ })),
9079
+ summary: `Found ${report.deadSymbols} potentially dead symbols (${report.byConfidence.high} high, ${report.byConfidence.medium} medium, ${report.byConfidence.low} low confidence) out of ${report.totalSymbols} total symbols (${report.deadPercentage.toFixed(1)}% dead code).`
9080
+ };
9081
+ } catch (error) {
9082
+ return {
9083
+ error: "Failed to analyze dead code",
9084
+ message: String(error)
9085
+ };
9086
+ }
9087
+ }
8607
9088
 
8608
9089
  // src/mcp/server.ts
8609
9090
  async function startMcpServer(state) {
@@ -8652,6 +9133,7 @@ export {
8652
9133
  updateFileInGraph,
8653
9134
  calculateHealthScore,
8654
9135
  getHealthTrend,
9136
+ analyzeDeadCode,
8655
9137
  generateDocs,
8656
9138
  getCommitLog,
8657
9139
  getCurrentBranch,
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ analyzeDeadCode,
3
4
  buildGraph,
4
5
  calculateHealthScore,
5
6
  checkoutCommit,
@@ -26,7 +27,7 @@ import {
26
27
  stashChanges,
27
28
  updateFileInGraph,
28
29
  watchProject
29
- } from "./chunk-65H7HCM4.js";
30
+ } from "./chunk-IGRFC3MQ.js";
30
31
 
31
32
  // src/index.ts
32
33
  import { Command } from "commander";
@@ -418,6 +419,7 @@ async function runTemporalAnalysis(projectDir, options) {
418
419
  if (hadStash) {
419
420
  await popStash(projectDir);
420
421
  }
422
+ snapshots.reverse();
421
423
  console.log(`\u2713 Created ${snapshots.length} snapshots`);
422
424
  if (options.stats) {
423
425
  printStats(snapshots);
@@ -786,4 +788,32 @@ program.command("health").description("Analyze dependency architecture health (0
786
788
  process.exit(1);
787
789
  }
788
790
  });
791
+ program.command("dead-code").description("Identify dead code - symbols defined but never referenced").argument("[directory]", "Project directory to analyze (defaults to current directory or auto-detected project root)").option("--confidence <level>", "Minimum confidence level to show: high, medium, low (default: medium)", "medium").option("--json", "Output as JSON (for CI/automation)").option("--verbose", "Show detailed info for each dead symbol").option("--stats", "Show summary statistics").option("--include-tests", "Include test files in analysis").option("--include-low", "Shortcut for --confidence low").action(async (directory, options) => {
792
+ try {
793
+ const projectRoot = directory ? resolve(directory) : findProjectRoot();
794
+ const startTime = Date.now();
795
+ const parsedFiles = await parseProject(projectRoot);
796
+ const graph = buildGraph(parsedFiles);
797
+ const confidence = options.includeLow ? "low" : options.confidence || "medium";
798
+ const report = analyzeDeadCode(graph, projectRoot, {
799
+ confidence,
800
+ includeTests: options.includeTests || false,
801
+ verbose: options.verbose || false,
802
+ stats: options.stats || false,
803
+ json: options.json || false
804
+ });
805
+ if (options.json) {
806
+ console.log(JSON.stringify(report, null, 2));
807
+ }
808
+ const totalTime = Date.now() - startTime;
809
+ if (!options.json) {
810
+ console.log(`
811
+ Analysis completed in ${(totalTime / 1e3).toFixed(2)}s
812
+ `);
813
+ }
814
+ } catch (err) {
815
+ console.error("Error analyzing dead code:", err);
816
+ process.exit(1);
817
+ }
818
+ });
789
819
  program.parse();
@@ -6,7 +6,7 @@ import {
6
6
  startMcpServer,
7
7
  updateFileInGraph,
8
8
  watchProject
9
- } from "./chunk-65H7HCM4.js";
9
+ } from "./chunk-IGRFC3MQ.js";
10
10
 
11
11
  // src/mcpb-entry.ts
12
12
  import { resolve } from "path";
@@ -352,6 +352,39 @@ function renderArcDiagram(snapshot) {
352
352
  });
353
353
  }, 1000);
354
354
  }
355
+
356
+ fitToViewport(zoom, width, height, margin);
357
+ }
358
+
359
+ function fitToViewport(zoom, width, height, margin) {
360
+ setTimeout(() => {
361
+ try {
362
+ const bounds = g.node().getBBox();
363
+
364
+ if (!bounds || bounds.width === 0 || bounds.height === 0) {
365
+ return;
366
+ }
367
+
368
+ const fullWidth = bounds.width;
369
+ const fullHeight = bounds.height;
370
+
371
+ const padding = 0.9;
372
+ const scale = Math.min(
373
+ (width * padding) / fullWidth,
374
+ (height * padding) / fullHeight,
375
+ 1.0
376
+ );
377
+
378
+ svg.transition()
379
+ .duration(750)
380
+ .call(
381
+ zoom.transform,
382
+ d3.zoomIdentity.scale(scale)
383
+ );
384
+ } catch (e) {
385
+ console.warn('Could not fit to viewport:', e);
386
+ }
387
+ }, 100);
355
388
  }
356
389
 
357
390
  function highlightConnections(filePath) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "depwire-cli",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Code cross-reference visualization and AI context engine for TypeScript, JavaScript, Python, and Go. Zero native dependencies โ€” works on Windows, macOS, and Linux.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -31,7 +31,7 @@
31
31
  "cross-reference"
32
32
  ],
33
33
  "author": "Atef Ataya (https://www.youtube.com/@atefataya)",
34
- "license": "BSL-1.1",
34
+ "license": "BUSL-1.1",
35
35
  "repository": {
36
36
  "type": "git",
37
37
  "url": "git+https://github.com/depwire/depwire.git"
@@ -51,6 +51,7 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "@modelcontextprotocol/sdk": "1.26.0",
54
+ "chalk": "^5.6.2",
54
55
  "chokidar": "5.0.0",
55
56
  "commander": "14.0.3",
56
57
  "express": "5.2.1",