depwire-cli 0.9.19 → 0.9.20
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 +88 -33
- package/dist/{chunk-S3NZMIIU.js → chunk-H6Q2OEGP.js} +47 -0
- package/dist/index.js +502 -22
- package/dist/mcpb-entry.js +1 -1
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
|
|
10
10
|

|
|
11
11
|
|
|
12
|
-
**
|
|
12
|
+
**The missing context layer for AI coding assistants.**
|
|
13
|
+
|
|
14
|
+
Deterministic dependency graph. 16 MCP tools. Architecture health. What If simulation.
|
|
15
|
+
|
|
16
|
+
The context layer that turns vibe coding into software engineering.
|
|
13
17
|
|
|
14
18
|
⭐ **If Depwire helps you, please [star the repo](https://github.com/depwire/depwire)** — it helps this open-source project grow into an enterprise tool.
|
|
15
19
|
|
|
@@ -24,33 +28,6 @@ Depwire analyzes codebases to build a cross-reference graph showing how every fi
|
|
|
24
28
|
- 👀 **Live updates** — Graph stays current as you edit code
|
|
25
29
|
- 🌍 **Multi-language** — TypeScript, JavaScript, Python, Go, Rust, and C
|
|
26
30
|
|
|
27
|
-
## Why Depwire?
|
|
28
|
-
|
|
29
|
-
AI coding tools are flying blind. Every time Claude, Cursor, or Copilot touches your code, it's guessing about dependencies, imports, and impact. The result: broken refactors, hallucinated imports, and wasted tokens re-scanning files it already saw.
|
|
30
|
-
|
|
31
|
-
**Lost context = lost money + lost time + bad code.**
|
|
32
|
-
|
|
33
|
-
**Depwire parsed the entire Hono framework — 352 files, 5,971 symbols, 1,565 dependency edges — in ~3 seconds.** 40% fewer tool calls, 56% fewer file reads vs. no context layer.
|
|
34
|
-
|
|
35
|
-
Depwire fixes this by giving AI tools a complete dependency graph of your codebase — not a fuzzy embedding, not a keyword search, but a deterministic, tree-sitter-parsed map of every symbol and connection.
|
|
36
|
-
|
|
37
|
-
### Stop Losing Context
|
|
38
|
-
- **No more "start from scratch" chats** — Depwire is the shared knowledge layer that every AI session inherits. New chat? Your AI already knows the architecture.
|
|
39
|
-
- **Stop burning tokens** — AI tools query the graph instead of scanning hundreds of files blindly
|
|
40
|
-
- **One command, every AI tool** — Claude Desktop, Cursor, VS Code, any MCP-compatible tool gets the same complete picture
|
|
41
|
-
|
|
42
|
-
### Ship Better Code
|
|
43
|
-
- **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
|
|
44
|
-
- **Refactor with confidence** — see every downstream consumer, every transitive dependency, 2-3 levels deep
|
|
45
|
-
- **Catch dead code** — find symbols nobody references anymore with confidence-based classification (high/medium/low)
|
|
46
|
-
|
|
47
|
-
### Stay in Flow
|
|
48
|
-
- **Live graph, always current** — edit a file and the dependency map updates in real-time. No re-indexing, no waiting.
|
|
49
|
-
- **Works locally, stays private** — zero cloud accounts, zero data leaving your machine. Just `npm install` and go.
|
|
50
|
-
|
|
51
|
-
### 15 MCP Tools, Not Just Visualization
|
|
52
|
-
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.
|
|
53
|
-
|
|
54
31
|
## Installation
|
|
55
32
|
|
|
56
33
|

|
|
@@ -91,6 +68,7 @@ depwire docs
|
|
|
91
68
|
depwire health
|
|
92
69
|
depwire dead-code
|
|
93
70
|
depwire temporal
|
|
71
|
+
depwire whatif
|
|
94
72
|
|
|
95
73
|
# Or specify a directory explicitly
|
|
96
74
|
npx depwire-cli viz ./my-project
|
|
@@ -166,6 +144,43 @@ Settings → Features → Experimental → Enable MCP → Add Server:
|
|
|
166
144
|
| `get_health_score` | Get 0-100 dependency health score with recommendations |
|
|
167
145
|
| `find_dead_code` | Find dead code — symbols defined but never referenced |
|
|
168
146
|
| `get_temporal_graph` | Show how the graph evolved over git history |
|
|
147
|
+
| `simulate_change` | Simulate architectural changes before touching code |
|
|
148
|
+
|
|
149
|
+
## What If Simulation
|
|
150
|
+
|
|
151
|
+
Simulate architectural changes before touching a single line of code.
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
# What breaks if I delete this file?
|
|
155
|
+
depwire whatif . --simulate delete --target src/services/auth.ts
|
|
156
|
+
|
|
157
|
+
# What happens if I move this file?
|
|
158
|
+
depwire whatif . --simulate move --target src/utils/helpers.ts --destination src/core/helpers.ts
|
|
159
|
+
|
|
160
|
+
# What happens if I rename this file?
|
|
161
|
+
depwire whatif . --simulate rename --target src/router.ts --new-name routes.ts
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Each simulation returns:
|
|
165
|
+
- **Health score delta** — does this change improve or degrade your architecture?
|
|
166
|
+
- **Broken imports** — exactly which files would break and why
|
|
167
|
+
- **Affected nodes** — full blast radius of the change
|
|
168
|
+
- **Circular deps introduced or resolved**
|
|
169
|
+
- **Edge changes** — added and removed dependency connections
|
|
170
|
+
|
|
171
|
+
Supported actions: `move`, `delete`, `rename`, `split`, `merge`
|
|
172
|
+
|
|
173
|
+
## Why Depwire
|
|
174
|
+
|
|
175
|
+
| Feature | Depwire | Standard RAG (Fuzzy Search) | LLM Native Scanning |
|
|
176
|
+
|---------|---------|----------------------------|---------------------|
|
|
177
|
+
| Logic | Deterministic Graph | Probabilistic Match | Brute Force Reading |
|
|
178
|
+
| Precision | 100% (Tree-sitter AST) | ~70% (Embedding match) | Varies — hallucination prone |
|
|
179
|
+
| Refactor Safety | High — traces full call chains | Low — misses indirect refs | Zero — blind edits |
|
|
180
|
+
| Token Cost | Ultra-low — surgical reads | High — context stuffing | Extreme — scans everything |
|
|
181
|
+
| Circular Detection | Built-in | Not possible | Occasional |
|
|
182
|
+
| What If Simulation | Before touching code | Not possible | Not possible |
|
|
183
|
+
| Architecture Health Score | 0-100 with dimensions | Not possible | Not possible |
|
|
169
184
|
|
|
170
185
|
## GitHub Action — PR Impact Analysis
|
|
171
186
|
|
|
@@ -586,6 +601,46 @@ depwire temporal --output ./temp-snapshots
|
|
|
586
601
|
|
|
587
602
|
Snapshots are cached in `.depwire/temporal/` for fast re-rendering.
|
|
588
603
|
|
|
604
|
+
### `depwire whatif [directory]`
|
|
605
|
+
|
|
606
|
+
Simulate architectural changes before touching code.
|
|
607
|
+
|
|
608
|
+
**Directory argument is optional** — Auto-detects project root.
|
|
609
|
+
|
|
610
|
+
**Options:**
|
|
611
|
+
- `--simulate <action>` — Action to simulate: `move`, `delete`, `rename`, `split`, `merge`
|
|
612
|
+
- `--target <file>` — File to apply the action to
|
|
613
|
+
- `--destination <file>` — Destination path (for move action)
|
|
614
|
+
- `--new-name <name>` — New name (for rename action)
|
|
615
|
+
- `--source <file>` — Source file (for merge action)
|
|
616
|
+
- `--new-file <file>` — New file path (for split action)
|
|
617
|
+
- `--symbols <symbols>` — Comma-separated symbol names (for split action)
|
|
618
|
+
|
|
619
|
+
**Examples:**
|
|
620
|
+
```bash
|
|
621
|
+
# What breaks if I delete this file?
|
|
622
|
+
depwire whatif --simulate delete --target src/auth/service.ts
|
|
623
|
+
|
|
624
|
+
# What happens if I move this module?
|
|
625
|
+
depwire whatif --simulate move --target src/utils.ts --destination src/core/utils.ts
|
|
626
|
+
|
|
627
|
+
# Rename a file
|
|
628
|
+
depwire whatif --simulate rename --target src/router.ts --new-name routes.ts
|
|
629
|
+
|
|
630
|
+
# Split symbols into a new file
|
|
631
|
+
depwire whatif --simulate split --target src/utils.ts --new-file src/helpers.ts --symbols "formatDate,parseUrl"
|
|
632
|
+
|
|
633
|
+
# Merge two files
|
|
634
|
+
depwire whatif --simulate merge --target src/auth.ts --source src/login.ts
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
**Output:**
|
|
638
|
+
- Health score delta (before/after with improvement indicator)
|
|
639
|
+
- Broken imports with file and symbol details
|
|
640
|
+
- Affected nodes count
|
|
641
|
+
- Circular dependencies introduced or resolved
|
|
642
|
+
- Added and removed edge counts
|
|
643
|
+
|
|
589
644
|
### Error Handling
|
|
590
645
|
|
|
591
646
|
Depwire gracefully handles parse errors:
|
|
@@ -649,7 +704,7 @@ See [SECURITY.md](SECURITY.md) for full details.
|
|
|
649
704
|
|
|
650
705
|
### ✅ Shipped
|
|
651
706
|
- [x] Arc diagram visualization
|
|
652
|
-
- [x] MCP server (
|
|
707
|
+
- [x] MCP server (16 tools)
|
|
653
708
|
- [x] Multi-language support (TypeScript, JavaScript, Python, Go, Rust, C)
|
|
654
709
|
- [x] File watching + live refresh
|
|
655
710
|
- [x] Auto-generated documentation (13 documents)
|
|
@@ -660,18 +715,18 @@ See [SECURITY.md](SECURITY.md) for full details.
|
|
|
660
715
|
- [x] Auto-detect project root (no path needed)
|
|
661
716
|
- [x] WASM migration (Windows support)
|
|
662
717
|
- [x] Cloud dashboard — [app.depwire.dev](https://app.depwire.dev)
|
|
718
|
+
- [x] What If simulation — simulate refactors before touching code
|
|
663
719
|
|
|
664
|
-
###
|
|
720
|
+
### Coming Next
|
|
665
721
|
- [ ] New language support (Java, C++, Ruby — community requested)
|
|
666
|
-
- [ ] "What If" simulation — simulate refactors before touching code
|
|
667
722
|
- [ ] Cross-language edge detection (API routes ↔ frontend calls)
|
|
668
723
|
- [ ] AI-suggested refactors
|
|
669
724
|
- [ ] Natural language architecture queries
|
|
670
725
|
- [ ] VSCode extension
|
|
671
726
|
|
|
672
|
-
##
|
|
727
|
+
## Cloud Dashboard
|
|
673
728
|
|
|
674
|
-
|
|
729
|
+
Prefer a browser interface? [app.depwire.dev](https://app.depwire.dev) gives you the full dependency graph, health score, dead code report, and AI codebase chat — without any local setup. Free tier available.
|
|
675
730
|
|
|
676
731
|
- **Free** for public repos
|
|
677
732
|
- **Pro** ($19/month) — unlimited repos + private repo support
|
|
@@ -9371,6 +9371,42 @@ function getToolsList() {
|
|
|
9371
9371
|
}
|
|
9372
9372
|
}
|
|
9373
9373
|
}
|
|
9374
|
+
},
|
|
9375
|
+
{
|
|
9376
|
+
name: "simulate_change",
|
|
9377
|
+
description: "Simulate an architectural change and see the impact before touching code. Returns health score delta, broken imports, and affected files.",
|
|
9378
|
+
inputSchema: {
|
|
9379
|
+
type: "object",
|
|
9380
|
+
properties: {
|
|
9381
|
+
action: {
|
|
9382
|
+
type: "string",
|
|
9383
|
+
enum: ["move", "delete", "rename", "split", "merge"],
|
|
9384
|
+
description: "Type of change to simulate"
|
|
9385
|
+
},
|
|
9386
|
+
target: {
|
|
9387
|
+
type: "string",
|
|
9388
|
+
description: "File path to apply the action to"
|
|
9389
|
+
},
|
|
9390
|
+
destination: {
|
|
9391
|
+
type: "string",
|
|
9392
|
+
description: "Destination path (for move action)"
|
|
9393
|
+
},
|
|
9394
|
+
newName: {
|
|
9395
|
+
type: "string",
|
|
9396
|
+
description: "New name (for rename action)"
|
|
9397
|
+
},
|
|
9398
|
+
source: {
|
|
9399
|
+
type: "string",
|
|
9400
|
+
description: "Source file (for merge action)"
|
|
9401
|
+
},
|
|
9402
|
+
symbols: {
|
|
9403
|
+
type: "array",
|
|
9404
|
+
items: { type: "string" },
|
|
9405
|
+
description: "Symbols to move (for split action)"
|
|
9406
|
+
}
|
|
9407
|
+
},
|
|
9408
|
+
required: ["action", "target"]
|
|
9409
|
+
}
|
|
9374
9410
|
}
|
|
9375
9411
|
];
|
|
9376
9412
|
}
|
|
@@ -9442,6 +9478,11 @@ async function handleToolCall(name, args, state) {
|
|
|
9442
9478
|
} else {
|
|
9443
9479
|
result = handleFindDeadCode(state, args.confidence || "medium");
|
|
9444
9480
|
}
|
|
9481
|
+
} else if (name === "simulate_change") {
|
|
9482
|
+
result = {
|
|
9483
|
+
status: "coming_soon",
|
|
9484
|
+
message: "simulate_change will be fully available in v1.0.0. Use the CLI command 'depwire whatif' for simulation in the meantime."
|
|
9485
|
+
};
|
|
9445
9486
|
} else {
|
|
9446
9487
|
if (!isProjectLoaded(state)) {
|
|
9447
9488
|
result = {
|
|
@@ -10157,6 +10198,12 @@ export {
|
|
|
10157
10198
|
startVizServer,
|
|
10158
10199
|
createEmptyState,
|
|
10159
10200
|
updateFileInGraph,
|
|
10201
|
+
calculateCouplingScore,
|
|
10202
|
+
calculateCohesionScore,
|
|
10203
|
+
calculateCircularDepsScore,
|
|
10204
|
+
calculateGodFilesScore,
|
|
10205
|
+
calculateOrphansScore,
|
|
10206
|
+
calculateDepthScore,
|
|
10160
10207
|
calculateHealthScore,
|
|
10161
10208
|
getHealthTrend,
|
|
10162
10209
|
analyzeDeadCode,
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
import {
|
|
3
3
|
analyzeDeadCode,
|
|
4
4
|
buildGraph,
|
|
5
|
+
calculateCircularDepsScore,
|
|
6
|
+
calculateCohesionScore,
|
|
7
|
+
calculateCouplingScore,
|
|
8
|
+
calculateDepthScore,
|
|
9
|
+
calculateGodFilesScore,
|
|
5
10
|
calculateHealthScore,
|
|
11
|
+
calculateOrphansScore,
|
|
6
12
|
checkoutCommit,
|
|
7
13
|
createEmptyState,
|
|
8
14
|
createSnapshot,
|
|
@@ -27,11 +33,11 @@ import {
|
|
|
27
33
|
stashChanges,
|
|
28
34
|
updateFileInGraph,
|
|
29
35
|
watchProject
|
|
30
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-H6Q2OEGP.js";
|
|
31
37
|
|
|
32
38
|
// src/index.ts
|
|
33
39
|
import { Command } from "commander";
|
|
34
|
-
import { resolve, dirname as
|
|
40
|
+
import { resolve as resolve2, dirname as dirname3, join as join4 } from "path";
|
|
35
41
|
import { writeFileSync, readFileSync as readFileSync2, existsSync } from "fs";
|
|
36
42
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
37
43
|
|
|
@@ -302,10 +308,10 @@ async function findAvailablePort(startPort) {
|
|
|
302
308
|
const net = await import("net");
|
|
303
309
|
for (let attempt = 0; attempt < 10; attempt++) {
|
|
304
310
|
const testPort = startPort + attempt;
|
|
305
|
-
const isAvailable = await new Promise((
|
|
306
|
-
const server = net.createServer().once("error", () =>
|
|
311
|
+
const isAvailable = await new Promise((resolve3) => {
|
|
312
|
+
const server = net.createServer().once("error", () => resolve3(false)).once("listening", () => {
|
|
307
313
|
server.close();
|
|
308
|
-
|
|
314
|
+
resolve3(true);
|
|
309
315
|
}).listen(testPort, "127.0.0.1");
|
|
310
316
|
});
|
|
311
317
|
if (isAvailable) {
|
|
@@ -346,13 +352,13 @@ async function startTemporalServer(snapshots, projectRoot, preferredPort = 3334)
|
|
|
346
352
|
console.log(" (Could not open browser automatically)");
|
|
347
353
|
});
|
|
348
354
|
});
|
|
349
|
-
await new Promise((
|
|
355
|
+
await new Promise((resolve3, reject) => {
|
|
350
356
|
server.on("error", reject);
|
|
351
357
|
process.on("SIGINT", () => {
|
|
352
358
|
console.log("\n\nShutting down temporal server...");
|
|
353
359
|
server.close(() => {
|
|
354
360
|
console.log("Server stopped");
|
|
355
|
-
|
|
361
|
+
resolve3();
|
|
356
362
|
process.exit(0);
|
|
357
363
|
});
|
|
358
364
|
});
|
|
@@ -496,10 +502,475 @@ async function trackCommand(command, version = "unknown") {
|
|
|
496
502
|
});
|
|
497
503
|
}
|
|
498
504
|
|
|
505
|
+
// src/commands/whatif.ts
|
|
506
|
+
import { resolve } from "path";
|
|
507
|
+
import chalk from "chalk";
|
|
508
|
+
|
|
509
|
+
// src/simulation/engine.ts
|
|
510
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
511
|
+
function normalizePath(p) {
|
|
512
|
+
return p.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
513
|
+
}
|
|
514
|
+
function fileMatch(nodeFilePath, target) {
|
|
515
|
+
const a = normalizePath(nodeFilePath);
|
|
516
|
+
const b = normalizePath(target);
|
|
517
|
+
return a === b || a.endsWith("/" + b) || b.endsWith("/" + a);
|
|
518
|
+
}
|
|
519
|
+
var SimulationEngine = class {
|
|
520
|
+
original;
|
|
521
|
+
constructor(graph) {
|
|
522
|
+
this.original = graph;
|
|
523
|
+
}
|
|
524
|
+
simulate(action) {
|
|
525
|
+
const clone = this.original.copy();
|
|
526
|
+
const brokenImports = [];
|
|
527
|
+
switch (action.type) {
|
|
528
|
+
case "move":
|
|
529
|
+
this.applyMove(clone, action.target, action.destination, brokenImports);
|
|
530
|
+
break;
|
|
531
|
+
case "delete":
|
|
532
|
+
this.applyDelete(clone, action.target, brokenImports);
|
|
533
|
+
break;
|
|
534
|
+
case "rename":
|
|
535
|
+
this.applyRename(clone, action.target, action.newName, brokenImports);
|
|
536
|
+
break;
|
|
537
|
+
case "split":
|
|
538
|
+
this.applySplit(clone, action.target, action.newFile, action.symbols, brokenImports);
|
|
539
|
+
break;
|
|
540
|
+
case "merge":
|
|
541
|
+
this.applyMerge(clone, action.target, action.source, brokenImports);
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
const diff = this.computeDiff(this.original, clone, brokenImports);
|
|
545
|
+
const beforeHealth = this.computeHealthScore(this.original);
|
|
546
|
+
const afterHealth = this.computeHealthScore(clone);
|
|
547
|
+
const dimensionChanges = beforeHealth.dimensions.map((bd, i) => {
|
|
548
|
+
const ad = afterHealth.dimensions[i];
|
|
549
|
+
return {
|
|
550
|
+
name: bd.name,
|
|
551
|
+
before: bd.score,
|
|
552
|
+
after: ad ? ad.score : bd.score,
|
|
553
|
+
delta: (ad ? ad.score : bd.score) - bd.score
|
|
554
|
+
};
|
|
555
|
+
});
|
|
556
|
+
const healthDelta = {
|
|
557
|
+
before: beforeHealth.score,
|
|
558
|
+
after: afterHealth.score,
|
|
559
|
+
delta: afterHealth.score - beforeHealth.score,
|
|
560
|
+
improved: afterHealth.score > beforeHealth.score,
|
|
561
|
+
dimensionChanges
|
|
562
|
+
};
|
|
563
|
+
return {
|
|
564
|
+
action,
|
|
565
|
+
originalGraph: {
|
|
566
|
+
nodeCount: this.original.order,
|
|
567
|
+
edgeCount: this.original.size,
|
|
568
|
+
healthScore: beforeHealth.score
|
|
569
|
+
},
|
|
570
|
+
simulatedGraph: {
|
|
571
|
+
nodeCount: clone.order,
|
|
572
|
+
edgeCount: clone.size,
|
|
573
|
+
healthScore: afterHealth.score
|
|
574
|
+
},
|
|
575
|
+
diff,
|
|
576
|
+
healthDelta
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
// ── Action implementations ─────────────────────────────────────
|
|
580
|
+
applyMove(clone, target, destination, brokenImports) {
|
|
581
|
+
const normalizedTarget = normalizePath(target);
|
|
582
|
+
const normalizedDest = normalizePath(destination);
|
|
583
|
+
const nodesToMove = clone.filterNodes(
|
|
584
|
+
(_node, attrs) => fileMatch(attrs.filePath, target)
|
|
585
|
+
);
|
|
586
|
+
if (nodesToMove.length === 0) return;
|
|
587
|
+
for (const oldId of nodesToMove) {
|
|
588
|
+
const attrs = clone.getNodeAttributes(oldId);
|
|
589
|
+
const symbolName = oldId.includes("::") ? oldId.split("::").slice(1).join("::") : attrs.name;
|
|
590
|
+
const newId = `${normalizedDest}::${symbolName}`;
|
|
591
|
+
clone.forEachInEdge(oldId, (edge, edgeAttrs, source) => {
|
|
592
|
+
const sourceAttrs = clone.getNodeAttributes(source);
|
|
593
|
+
if (!fileMatch(sourceAttrs.filePath, target)) {
|
|
594
|
+
brokenImports.push({
|
|
595
|
+
file: sourceAttrs.filePath,
|
|
596
|
+
importedSymbol: attrs.name,
|
|
597
|
+
reason: `imports ${attrs.name} from ${target} (path would break)`
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
if (!clone.hasNode(newId)) {
|
|
602
|
+
clone.addNode(newId, { ...attrs, filePath: normalizedDest });
|
|
603
|
+
}
|
|
604
|
+
clone.forEachInEdge(oldId, (edge, edgeAttrs, source) => {
|
|
605
|
+
const newSource = nodesToMove.includes(source) ? `${normalizedDest}::${source.includes("::") ? source.split("::").slice(1).join("::") : clone.getNodeAttributes(source).name}` : source;
|
|
606
|
+
if (clone.hasNode(newSource) && clone.hasNode(newId)) {
|
|
607
|
+
clone.mergeEdge(newSource, newId, edgeAttrs);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
clone.forEachOutEdge(oldId, (edge, edgeAttrs, _source, outTarget) => {
|
|
611
|
+
const newTarget = nodesToMove.includes(outTarget) ? `${normalizedDest}::${outTarget.includes("::") ? outTarget.split("::").slice(1).join("::") : clone.getNodeAttributes(outTarget).name}` : outTarget;
|
|
612
|
+
if (clone.hasNode(newId) && clone.hasNode(newTarget)) {
|
|
613
|
+
clone.mergeEdge(newId, newTarget, edgeAttrs);
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
clone.dropNode(oldId);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
applyDelete(clone, target, brokenImports) {
|
|
620
|
+
const nodesToDelete = clone.filterNodes(
|
|
621
|
+
(_node, attrs) => fileMatch(attrs.filePath, target)
|
|
622
|
+
);
|
|
623
|
+
for (const nodeId of nodesToDelete) {
|
|
624
|
+
const attrs = clone.getNodeAttributes(nodeId);
|
|
625
|
+
clone.forEachInEdge(nodeId, (_edge, _edgeAttrs, source) => {
|
|
626
|
+
const sourceAttrs = clone.getNodeAttributes(source);
|
|
627
|
+
if (!fileMatch(sourceAttrs.filePath, target)) {
|
|
628
|
+
brokenImports.push({
|
|
629
|
+
file: sourceAttrs.filePath,
|
|
630
|
+
importedSymbol: attrs.name,
|
|
631
|
+
reason: `imports ${attrs.name} from ${target} (file deleted)`
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
for (const nodeId of nodesToDelete) {
|
|
637
|
+
clone.dropNode(nodeId);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
applyRename(clone, target, newName, brokenImports) {
|
|
641
|
+
const destination = join3(dirname2(target), newName);
|
|
642
|
+
this.applyMove(clone, target, destination, brokenImports);
|
|
643
|
+
}
|
|
644
|
+
applySplit(clone, target, newFile, symbols, brokenImports) {
|
|
645
|
+
const normalizedNewFile = normalizePath(newFile);
|
|
646
|
+
const nodesToSplit = clone.filterNodes((_node, attrs) => {
|
|
647
|
+
return fileMatch(attrs.filePath, target) && symbols.includes(attrs.name);
|
|
648
|
+
});
|
|
649
|
+
if (nodesToSplit.length === 0) return;
|
|
650
|
+
for (const oldId of nodesToSplit) {
|
|
651
|
+
const attrs = clone.getNodeAttributes(oldId);
|
|
652
|
+
const symbolName = oldId.includes("::") ? oldId.split("::").slice(1).join("::") : attrs.name;
|
|
653
|
+
const newId = `${normalizedNewFile}::${symbolName}`;
|
|
654
|
+
clone.forEachInEdge(oldId, (_edge, _edgeAttrs, source) => {
|
|
655
|
+
const sourceAttrs = clone.getNodeAttributes(source);
|
|
656
|
+
if (!fileMatch(sourceAttrs.filePath, target) && !fileMatch(sourceAttrs.filePath, newFile)) {
|
|
657
|
+
brokenImports.push({
|
|
658
|
+
file: sourceAttrs.filePath,
|
|
659
|
+
importedSymbol: attrs.name,
|
|
660
|
+
reason: `imports ${attrs.name} from ${target} (symbol moved to ${newFile})`
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
if (!clone.hasNode(newId)) {
|
|
665
|
+
clone.addNode(newId, { ...attrs, filePath: normalizedNewFile });
|
|
666
|
+
}
|
|
667
|
+
clone.forEachInEdge(oldId, (_edge, edgeAttrs, source) => {
|
|
668
|
+
if (clone.hasNode(source) && clone.hasNode(newId)) {
|
|
669
|
+
clone.mergeEdge(source, newId, edgeAttrs);
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
clone.forEachOutEdge(oldId, (_edge, edgeAttrs, _source, outTarget) => {
|
|
673
|
+
if (clone.hasNode(newId) && clone.hasNode(outTarget)) {
|
|
674
|
+
clone.mergeEdge(newId, outTarget, edgeAttrs);
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
clone.dropNode(oldId);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
applyMerge(clone, target, source, brokenImports) {
|
|
681
|
+
const normalizedTarget = normalizePath(target);
|
|
682
|
+
const sourceNodes = clone.filterNodes(
|
|
683
|
+
(_node, attrs) => fileMatch(attrs.filePath, source)
|
|
684
|
+
);
|
|
685
|
+
const targetNodes = clone.filterNodes(
|
|
686
|
+
(_node, attrs) => fileMatch(attrs.filePath, target)
|
|
687
|
+
);
|
|
688
|
+
const targetSymbols = new Set(
|
|
689
|
+
targetNodes.map((n) => clone.getNodeAttributes(n).name)
|
|
690
|
+
);
|
|
691
|
+
for (const nodeId of sourceNodes) {
|
|
692
|
+
const name = clone.getNodeAttributes(nodeId).name;
|
|
693
|
+
if (name !== "__file__" && targetSymbols.has(name)) {
|
|
694
|
+
throw new Error(
|
|
695
|
+
`Merge conflict: symbol "${name}" exists in both ${target} and ${source}`
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
for (const oldId of sourceNodes) {
|
|
700
|
+
const attrs = clone.getNodeAttributes(oldId);
|
|
701
|
+
const symbolName = oldId.includes("::") ? oldId.split("::").slice(1).join("::") : attrs.name;
|
|
702
|
+
const newId = `${normalizedTarget}::${symbolName}`;
|
|
703
|
+
clone.forEachInEdge(oldId, (_edge, _edgeAttrs, inSource) => {
|
|
704
|
+
const srcAttrs = clone.getNodeAttributes(inSource);
|
|
705
|
+
if (!fileMatch(srcAttrs.filePath, source) && !fileMatch(srcAttrs.filePath, target)) {
|
|
706
|
+
brokenImports.push({
|
|
707
|
+
file: srcAttrs.filePath,
|
|
708
|
+
importedSymbol: attrs.name,
|
|
709
|
+
reason: `imports ${attrs.name} from ${source} (merged into ${target})`
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
if (!clone.hasNode(newId)) {
|
|
714
|
+
clone.addNode(newId, { ...attrs, filePath: normalizedTarget });
|
|
715
|
+
}
|
|
716
|
+
clone.forEachInEdge(oldId, (_edge, edgeAttrs, inSource) => {
|
|
717
|
+
const resolvedSource = sourceNodes.includes(inSource) ? `${normalizedTarget}::${inSource.includes("::") ? inSource.split("::").slice(1).join("::") : clone.getNodeAttributes(inSource).name}` : inSource;
|
|
718
|
+
if (clone.hasNode(resolvedSource) && clone.hasNode(newId)) {
|
|
719
|
+
clone.mergeEdge(resolvedSource, newId, edgeAttrs);
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
clone.forEachOutEdge(oldId, (_edge, edgeAttrs, _s, outTarget) => {
|
|
723
|
+
const resolvedTarget = sourceNodes.includes(outTarget) ? `${normalizedTarget}::${outTarget.includes("::") ? outTarget.split("::").slice(1).join("::") : clone.getNodeAttributes(outTarget).name}` : outTarget;
|
|
724
|
+
if (clone.hasNode(newId) && clone.hasNode(resolvedTarget)) {
|
|
725
|
+
clone.mergeEdge(newId, resolvedTarget, edgeAttrs);
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
clone.dropNode(oldId);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
// ── Diff computation ───────────────────────────────────────────
|
|
732
|
+
computeDiff(original, simulated, brokenImports) {
|
|
733
|
+
const originalEdges = this.collectEdges(original);
|
|
734
|
+
const simulatedEdges = this.collectEdges(simulated);
|
|
735
|
+
const originalKeys = new Set(originalEdges.map((e) => this.edgeKey(e)));
|
|
736
|
+
const simulatedKeys = new Set(simulatedEdges.map((e) => this.edgeKey(e)));
|
|
737
|
+
const addedEdges = simulatedEdges.filter((e) => !originalKeys.has(this.edgeKey(e)));
|
|
738
|
+
const removedEdges = originalEdges.filter((e) => !simulatedKeys.has(this.edgeKey(e)));
|
|
739
|
+
const affectedNodeSet = /* @__PURE__ */ new Set();
|
|
740
|
+
for (const e of [...addedEdges, ...removedEdges]) {
|
|
741
|
+
affectedNodeSet.add(e.source);
|
|
742
|
+
affectedNodeSet.add(e.target);
|
|
743
|
+
}
|
|
744
|
+
const originalCycles = this.detectCycles(original);
|
|
745
|
+
const simulatedCycles = this.detectCycles(simulated);
|
|
746
|
+
const originalCycleKeys = new Set(originalCycles.map((c) => [...c].sort().join(",")));
|
|
747
|
+
const simulatedCycleKeys = new Set(simulatedCycles.map((c) => [...c].sort().join(",")));
|
|
748
|
+
const circularDepsIntroduced = simulatedCycles.filter(
|
|
749
|
+
(c) => !originalCycleKeys.has([...c].sort().join(","))
|
|
750
|
+
);
|
|
751
|
+
const circularDepsResolved = originalCycles.filter(
|
|
752
|
+
(c) => !simulatedCycleKeys.has([...c].sort().join(","))
|
|
753
|
+
);
|
|
754
|
+
return {
|
|
755
|
+
addedEdges,
|
|
756
|
+
removedEdges,
|
|
757
|
+
affectedNodes: Array.from(affectedNodeSet),
|
|
758
|
+
brokenImports,
|
|
759
|
+
circularDepsIntroduced,
|
|
760
|
+
circularDepsResolved
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
collectEdges(graph) {
|
|
764
|
+
const edges = [];
|
|
765
|
+
graph.forEachEdge((_edge, attrs, source, target) => {
|
|
766
|
+
edges.push({ source, target, kind: attrs.kind });
|
|
767
|
+
});
|
|
768
|
+
return edges;
|
|
769
|
+
}
|
|
770
|
+
edgeKey(e) {
|
|
771
|
+
return `${e.source}|${e.target}|${e.kind || ""}`;
|
|
772
|
+
}
|
|
773
|
+
// ── Cycle detection (adapted from src/health/metrics.ts) ───────
|
|
774
|
+
detectCycles(graph) {
|
|
775
|
+
const fileGraph = /* @__PURE__ */ new Map();
|
|
776
|
+
graph.forEachEdge((_edge, _attrs, source, target) => {
|
|
777
|
+
const sourceFile = graph.getNodeAttributes(source).filePath;
|
|
778
|
+
const targetFile = graph.getNodeAttributes(target).filePath;
|
|
779
|
+
if (sourceFile !== targetFile) {
|
|
780
|
+
if (!fileGraph.has(sourceFile)) {
|
|
781
|
+
fileGraph.set(sourceFile, /* @__PURE__ */ new Set());
|
|
782
|
+
}
|
|
783
|
+
fileGraph.get(sourceFile).add(targetFile);
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
const visited = /* @__PURE__ */ new Set();
|
|
787
|
+
const recStack = /* @__PURE__ */ new Set();
|
|
788
|
+
const cycles = [];
|
|
789
|
+
const dfs = (node, path) => {
|
|
790
|
+
if (recStack.has(node)) {
|
|
791
|
+
const cycleStart = path.indexOf(node);
|
|
792
|
+
if (cycleStart >= 0) {
|
|
793
|
+
cycles.push(path.slice(cycleStart));
|
|
794
|
+
}
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
if (visited.has(node)) return;
|
|
798
|
+
visited.add(node);
|
|
799
|
+
recStack.add(node);
|
|
800
|
+
path.push(node);
|
|
801
|
+
const neighbors = fileGraph.get(node);
|
|
802
|
+
if (neighbors) {
|
|
803
|
+
for (const neighbor of neighbors) {
|
|
804
|
+
dfs(neighbor, [...path]);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
recStack.delete(node);
|
|
808
|
+
};
|
|
809
|
+
for (const node of fileGraph.keys()) {
|
|
810
|
+
if (!visited.has(node)) {
|
|
811
|
+
dfs(node, []);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
const unique = /* @__PURE__ */ new Map();
|
|
815
|
+
for (const cycle of cycles) {
|
|
816
|
+
const key = [...cycle].sort().join(",");
|
|
817
|
+
if (!unique.has(key)) {
|
|
818
|
+
unique.set(key, cycle);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return Array.from(unique.values());
|
|
822
|
+
}
|
|
823
|
+
// ── Health score (side-effect free) ────────────────────────────
|
|
824
|
+
computeHealthScore(graph) {
|
|
825
|
+
const dimensions = [
|
|
826
|
+
calculateCouplingScore(graph),
|
|
827
|
+
calculateCohesionScore(graph),
|
|
828
|
+
calculateCircularDepsScore(graph),
|
|
829
|
+
calculateGodFilesScore(graph),
|
|
830
|
+
calculateOrphansScore(graph),
|
|
831
|
+
calculateDepthScore(graph)
|
|
832
|
+
];
|
|
833
|
+
const score = Math.round(
|
|
834
|
+
dimensions.reduce((sum, dim) => sum + dim.score * dim.weight, 0)
|
|
835
|
+
);
|
|
836
|
+
return { score, dimensions };
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
// src/commands/whatif.ts
|
|
841
|
+
async function whatif(dir, options) {
|
|
842
|
+
if (!options.simulate) {
|
|
843
|
+
console.log("Usage: depwire whatif [dir] --simulate <action> --target <file> [options]");
|
|
844
|
+
console.log("");
|
|
845
|
+
console.log("Actions: move, delete, rename, split, merge");
|
|
846
|
+
console.log("");
|
|
847
|
+
console.log("Run without --simulate to open interactive browser UI (Phase B)");
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
const validActions = ["move", "delete", "rename", "split", "merge"];
|
|
851
|
+
if (!validActions.includes(options.simulate)) {
|
|
852
|
+
console.error(chalk.red(`Invalid action: ${options.simulate}. Must be one of: ${validActions.join(", ")}`));
|
|
853
|
+
process.exit(1);
|
|
854
|
+
}
|
|
855
|
+
if (!options.target) {
|
|
856
|
+
console.error(chalk.red("--target is required for all simulation actions"));
|
|
857
|
+
process.exit(1);
|
|
858
|
+
}
|
|
859
|
+
const action = buildAction(options);
|
|
860
|
+
const projectRoot = dir === "." ? findProjectRoot() : resolve(dir);
|
|
861
|
+
console.log(`Parsing project: ${projectRoot}`);
|
|
862
|
+
const parsedFiles = await parseProject(projectRoot);
|
|
863
|
+
const graph = buildGraph(parsedFiles);
|
|
864
|
+
console.log(`Built graph: ${graph.order} symbols, ${graph.size} edges`);
|
|
865
|
+
console.log("");
|
|
866
|
+
const engine = new SimulationEngine(graph);
|
|
867
|
+
try {
|
|
868
|
+
const result = engine.simulate(action);
|
|
869
|
+
printResult(result);
|
|
870
|
+
} catch (err) {
|
|
871
|
+
console.error(chalk.red(`Simulation failed: ${err.message}`));
|
|
872
|
+
process.exit(1);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
function buildAction(options) {
|
|
876
|
+
const type = options.simulate;
|
|
877
|
+
const target = options.target;
|
|
878
|
+
switch (type) {
|
|
879
|
+
case "move":
|
|
880
|
+
if (!options.destination) {
|
|
881
|
+
console.error(chalk.red("--destination is required for move action"));
|
|
882
|
+
process.exit(1);
|
|
883
|
+
}
|
|
884
|
+
return { type: "move", target, destination: options.destination };
|
|
885
|
+
case "delete":
|
|
886
|
+
return { type: "delete", target };
|
|
887
|
+
case "rename":
|
|
888
|
+
if (!options.newName) {
|
|
889
|
+
console.error(chalk.red("--new-name is required for rename action"));
|
|
890
|
+
process.exit(1);
|
|
891
|
+
}
|
|
892
|
+
return { type: "rename", target, newName: options.newName };
|
|
893
|
+
case "split":
|
|
894
|
+
if (!options.newFile) {
|
|
895
|
+
console.error(chalk.red("--new-file is required for split action"));
|
|
896
|
+
process.exit(1);
|
|
897
|
+
}
|
|
898
|
+
if (!options.symbols) {
|
|
899
|
+
console.error(chalk.red("--symbols is required for split action (comma-separated)"));
|
|
900
|
+
process.exit(1);
|
|
901
|
+
}
|
|
902
|
+
return {
|
|
903
|
+
type: "split",
|
|
904
|
+
target,
|
|
905
|
+
newFile: options.newFile,
|
|
906
|
+
symbols: options.symbols.split(",").map((s) => s.trim())
|
|
907
|
+
};
|
|
908
|
+
case "merge":
|
|
909
|
+
if (!options.source) {
|
|
910
|
+
console.error(chalk.red("--source is required for merge action"));
|
|
911
|
+
process.exit(1);
|
|
912
|
+
}
|
|
913
|
+
return { type: "merge", target, source: options.source };
|
|
914
|
+
default:
|
|
915
|
+
console.error(chalk.red(`Unknown action: ${type}`));
|
|
916
|
+
process.exit(1);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
function printResult(result) {
|
|
920
|
+
const { action, healthDelta, diff } = result;
|
|
921
|
+
const line = "\u2500".repeat(45);
|
|
922
|
+
console.log(chalk.bold("What If Simulation"));
|
|
923
|
+
console.log(chalk.dim(line));
|
|
924
|
+
const actionStr = formatAction(action);
|
|
925
|
+
console.log(`${chalk.bold("Action:")} ${actionStr}`);
|
|
926
|
+
console.log(chalk.dim(line));
|
|
927
|
+
const deltaSign = healthDelta.delta >= 0 ? "+" : "";
|
|
928
|
+
const deltaColor = healthDelta.improved ? chalk.green : healthDelta.delta === 0 ? chalk.yellow : chalk.red;
|
|
929
|
+
const deltaIcon = healthDelta.improved ? "\u2713 improved" : healthDelta.delta === 0 ? "\u2192 unchanged" : "\u2717 degraded";
|
|
930
|
+
console.log(
|
|
931
|
+
`${chalk.bold("Health Score:")} ${healthDelta.before} \u2192 ${healthDelta.after} ${deltaColor(`(${deltaSign}${healthDelta.delta} ${deltaIcon})`)}`
|
|
932
|
+
);
|
|
933
|
+
const changed = healthDelta.dimensionChanges.filter((d) => d.delta !== 0);
|
|
934
|
+
if (changed.length > 0) {
|
|
935
|
+
for (const d of changed) {
|
|
936
|
+
const dSign = d.delta >= 0 ? "+" : "";
|
|
937
|
+
const dColor = d.delta > 0 ? chalk.green : chalk.red;
|
|
938
|
+
console.log(` ${chalk.dim("\u2022")} ${d.name}: ${d.before} \u2192 ${d.after} ${dColor(`(${dSign}${d.delta})`)}`);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
console.log(`${chalk.bold("Affected Nodes:")} ${diff.affectedNodes.length}`);
|
|
942
|
+
console.log(`${chalk.bold("Broken Imports:")} ${diff.brokenImports.length}`);
|
|
943
|
+
if (diff.brokenImports.length > 0) {
|
|
944
|
+
for (const bi of diff.brokenImports) {
|
|
945
|
+
console.log(` ${chalk.yellow("\u2022")} ${bi.file} ${bi.reason}`);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
console.log(
|
|
949
|
+
`${chalk.bold("Circular Deps:")} ${diff.circularDepsIntroduced.length} introduced, ${diff.circularDepsResolved.length} resolved`
|
|
950
|
+
);
|
|
951
|
+
console.log(`${chalk.bold("Added Edges:")} ${diff.addedEdges.length}`);
|
|
952
|
+
console.log(`${chalk.bold("Removed Edges:")} ${diff.removedEdges.length}`);
|
|
953
|
+
console.log(chalk.dim(line));
|
|
954
|
+
}
|
|
955
|
+
function formatAction(action) {
|
|
956
|
+
switch (action.type) {
|
|
957
|
+
case "move":
|
|
958
|
+
return `MOVE ${action.target} \u2192 ${action.destination}`;
|
|
959
|
+
case "delete":
|
|
960
|
+
return `DELETE ${action.target}`;
|
|
961
|
+
case "rename":
|
|
962
|
+
return `RENAME ${action.target} \u2192 ${action.newName}`;
|
|
963
|
+
case "split":
|
|
964
|
+
return `SPLIT ${action.target} \u2192 ${action.newFile} (${action.symbols.join(", ")})`;
|
|
965
|
+
case "merge":
|
|
966
|
+
return `MERGE ${action.source} \u2192 ${action.target}`;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
499
970
|
// src/index.ts
|
|
500
971
|
var __filename2 = fileURLToPath2(import.meta.url);
|
|
501
|
-
var __dirname2 =
|
|
502
|
-
var packageJsonPath =
|
|
972
|
+
var __dirname2 = dirname3(__filename2);
|
|
973
|
+
var packageJsonPath = join4(__dirname2, "../package.json");
|
|
503
974
|
var packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
|
|
504
975
|
var program = new Command();
|
|
505
976
|
program.name("depwire").description("Code cross-reference graph builder for TypeScript projects").version(packageJson.version);
|
|
@@ -507,7 +978,7 @@ program.command("parse").description("Parse a TypeScript project and build depen
|
|
|
507
978
|
trackCommand("parse", packageJson.version);
|
|
508
979
|
const startTime = Date.now();
|
|
509
980
|
try {
|
|
510
|
-
const projectRoot = directory ?
|
|
981
|
+
const projectRoot = directory ? resolve2(directory) : findProjectRoot();
|
|
511
982
|
console.log(`Parsing project: ${projectRoot}`);
|
|
512
983
|
const parsedFiles = await parseProject(projectRoot, {
|
|
513
984
|
exclude: options.exclude,
|
|
@@ -546,7 +1017,7 @@ Orphan Files (no cross-references): ${summary.orphanFiles.length}`);
|
|
|
546
1017
|
program.command("query").description("Query impact analysis for a symbol").argument("<directory>", "Project directory").argument("<symbol-name>", "Symbol name to query").action(async (directory, symbolName) => {
|
|
547
1018
|
trackCommand("query", packageJson.version);
|
|
548
1019
|
try {
|
|
549
|
-
const projectRoot =
|
|
1020
|
+
const projectRoot = resolve2(directory);
|
|
550
1021
|
const cacheFile = "depwire-output.json";
|
|
551
1022
|
let graph;
|
|
552
1023
|
if (existsSync(cacheFile)) {
|
|
@@ -595,7 +1066,7 @@ Total Transitive Dependents: ${impact.transitiveDependents.length}`);
|
|
|
595
1066
|
program.command("viz").description("Launch interactive arc diagram visualization").argument("[directory]", "Project directory to visualize (defaults to current directory or auto-detected project root)").option("-p, --port <number>", "Server port", "3333").option("--no-open", "Don't auto-open browser").option("--exclude <patterns...>", 'Glob patterns to exclude (e.g., "**/*.test.*" "dist/**")').option("--verbose", "Show detailed parsing progress").action(async (directory, options) => {
|
|
596
1067
|
trackCommand("viz", packageJson.version);
|
|
597
1068
|
try {
|
|
598
|
-
const projectRoot = directory ?
|
|
1069
|
+
const projectRoot = directory ? resolve2(directory) : findProjectRoot();
|
|
599
1070
|
console.log(`Parsing project: ${projectRoot}`);
|
|
600
1071
|
const parsedFiles = await parseProject(projectRoot, {
|
|
601
1072
|
exclude: options.exclude,
|
|
@@ -618,7 +1089,7 @@ program.command("viz").description("Launch interactive arc diagram visualization
|
|
|
618
1089
|
program.command("temporal").description("Visualize how the dependency graph evolved over git history").argument("[directory]", "Project directory to analyze (defaults to current directory or auto-detected project root)").option("--commits <number>", "Number of commits to sample", "20").option("--strategy <type>", "Sampling strategy: even, weekly, monthly", "even").option("-p, --port <number>", "Server port", "3334").option("--output <path>", "Save snapshots to custom path (default: .depwire/temporal/)").option("--verbose", "Show progress for each commit being parsed").option("--stats", "Show summary statistics at end").action(async (directory, options) => {
|
|
619
1090
|
trackCommand("temporal", packageJson.version);
|
|
620
1091
|
try {
|
|
621
|
-
const projectRoot = directory ?
|
|
1092
|
+
const projectRoot = directory ? resolve2(directory) : findProjectRoot();
|
|
622
1093
|
await runTemporalAnalysis(projectRoot, {
|
|
623
1094
|
commits: parseInt(options.commits, 10),
|
|
624
1095
|
strategy: options.strategy,
|
|
@@ -638,11 +1109,11 @@ program.command("mcp").description("Start MCP server for AI coding tools").argum
|
|
|
638
1109
|
const state = createEmptyState();
|
|
639
1110
|
let projectRootToConnect = null;
|
|
640
1111
|
if (directory) {
|
|
641
|
-
projectRootToConnect =
|
|
1112
|
+
projectRootToConnect = resolve2(directory);
|
|
642
1113
|
} else {
|
|
643
1114
|
const detectedRoot = findProjectRoot();
|
|
644
1115
|
const cwd = process.cwd();
|
|
645
|
-
if (detectedRoot !== cwd || existsSync(
|
|
1116
|
+
if (detectedRoot !== cwd || existsSync(join4(cwd, "package.json")) || existsSync(join4(cwd, "tsconfig.json")) || existsSync(join4(cwd, "go.mod")) || existsSync(join4(cwd, "pyproject.toml")) || existsSync(join4(cwd, "setup.py")) || existsSync(join4(cwd, ".git"))) {
|
|
646
1117
|
projectRootToConnect = detectedRoot;
|
|
647
1118
|
}
|
|
648
1119
|
}
|
|
@@ -699,8 +1170,8 @@ program.command("docs").description("Generate comprehensive codebase documentati
|
|
|
699
1170
|
trackCommand("docs", packageJson.version);
|
|
700
1171
|
const startTime = Date.now();
|
|
701
1172
|
try {
|
|
702
|
-
const projectRoot = directory ?
|
|
703
|
-
const outputDir = options.output ?
|
|
1173
|
+
const projectRoot = directory ? resolve2(directory) : findProjectRoot();
|
|
1174
|
+
const outputDir = options.output ? resolve2(options.output) : join4(projectRoot, ".depwire");
|
|
704
1175
|
const includeList = options.include.split(",").map((s) => s.trim());
|
|
705
1176
|
const onlyList = options.only ? options.only.split(",").map((s) => s.trim()) : void 0;
|
|
706
1177
|
if (options.gitignore === void 0 && !existsSyncNode(outputDir)) {
|
|
@@ -762,16 +1233,16 @@ async function promptGitignore() {
|
|
|
762
1233
|
input: process.stdin,
|
|
763
1234
|
output: process.stdout
|
|
764
1235
|
});
|
|
765
|
-
return new Promise((
|
|
1236
|
+
return new Promise((resolve3) => {
|
|
766
1237
|
rl.question("Add .depwire/ to .gitignore? [Y/n] ", (answer) => {
|
|
767
1238
|
rl.close();
|
|
768
1239
|
const normalized = answer.trim().toLowerCase();
|
|
769
|
-
|
|
1240
|
+
resolve3(normalized === "" || normalized === "y" || normalized === "yes");
|
|
770
1241
|
});
|
|
771
1242
|
});
|
|
772
1243
|
}
|
|
773
1244
|
function addToGitignore(projectRoot, pattern) {
|
|
774
|
-
const gitignorePath =
|
|
1245
|
+
const gitignorePath = join4(projectRoot, ".gitignore");
|
|
775
1246
|
try {
|
|
776
1247
|
let content = "";
|
|
777
1248
|
if (existsSyncNode(gitignorePath)) {
|
|
@@ -796,7 +1267,7 @@ ${pattern}
|
|
|
796
1267
|
program.command("health").description("Analyze dependency architecture health (0-100 score)").argument("[directory]", "Project directory to analyze (defaults to current directory or auto-detected project root)").option("--json", "Output as JSON").option("--verbose", "Show detailed breakdown").action(async (directory, options) => {
|
|
797
1268
|
trackCommand("health", packageJson.version);
|
|
798
1269
|
try {
|
|
799
|
-
const projectRoot = directory ?
|
|
1270
|
+
const projectRoot = directory ? resolve2(directory) : findProjectRoot();
|
|
800
1271
|
const startTime = Date.now();
|
|
801
1272
|
const parsedFiles = await parseProject(projectRoot);
|
|
802
1273
|
const graph = buildGraph(parsedFiles);
|
|
@@ -820,7 +1291,7 @@ program.command("health").description("Analyze dependency architecture health (0
|
|
|
820
1291
|
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").option("--debug", "Show debug information (exclusion stats)").action(async (directory, options) => {
|
|
821
1292
|
trackCommand("dead-code", packageJson.version);
|
|
822
1293
|
try {
|
|
823
|
-
const projectRoot = directory ?
|
|
1294
|
+
const projectRoot = directory ? resolve2(directory) : findProjectRoot();
|
|
824
1295
|
const startTime = Date.now();
|
|
825
1296
|
const parsedFiles = await parseProject(projectRoot);
|
|
826
1297
|
const graph = buildGraph(parsedFiles);
|
|
@@ -847,4 +1318,13 @@ Analysis completed in ${(totalTime / 1e3).toFixed(2)}s
|
|
|
847
1318
|
process.exit(1);
|
|
848
1319
|
}
|
|
849
1320
|
});
|
|
1321
|
+
program.command("whatif").description("Simulate architectural changes before touching code").argument("[directory]", "Project directory (defaults to auto-detected project root)").option("--simulate <action>", "Action to simulate: move, delete, rename, split, merge").option("--target <file>", "File to apply the action to").option("--destination <file>", "Destination path (for move action)").option("--new-name <name>", "New name (for rename action)").option("--source <file>", "Source file (for merge action)").option("--new-file <file>", "New file path (for split action)").option("--symbols <symbols>", "Comma-separated symbol names (for split action)").action(async (directory, options) => {
|
|
1322
|
+
trackCommand("whatif", packageJson.version);
|
|
1323
|
+
try {
|
|
1324
|
+
await whatif(directory || ".", options);
|
|
1325
|
+
} catch (err) {
|
|
1326
|
+
console.error("Error running simulation:", err);
|
|
1327
|
+
process.exit(1);
|
|
1328
|
+
}
|
|
1329
|
+
});
|
|
850
1330
|
program.parse();
|
package/dist/mcpb-entry.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "depwire-cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.20",
|
|
4
4
|
"description": "The missing context layer for AI coding assistants. Dependency graph, MCP server, architecture health, dead code detection.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,7 +28,14 @@
|
|
|
28
28
|
"impact-analysis",
|
|
29
29
|
"refactoring",
|
|
30
30
|
"arc-diagram",
|
|
31
|
-
"cross-reference"
|
|
31
|
+
"cross-reference",
|
|
32
|
+
"whatif",
|
|
33
|
+
"simulation",
|
|
34
|
+
"architectural-simulation",
|
|
35
|
+
"what-if-analysis",
|
|
36
|
+
"refactor-simulation",
|
|
37
|
+
"blast-radius",
|
|
38
|
+
"deterministic-graph"
|
|
32
39
|
],
|
|
33
40
|
"author": "Atef Ataya (https://www.youtube.com/@atefataya)",
|
|
34
41
|
"license": "BUSL-1.1",
|