depwire-cli 0.9.24 → 0.9.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -34,9 +34,9 @@ import {
34
34
 
35
35
  // src/index.ts
36
36
  import { Command } from "commander";
37
- import { resolve as resolve2, dirname as dirname2, join as join3 } from "path";
37
+ import { resolve as resolve2, dirname as dirname3, join as join4 } from "path";
38
38
  import { writeFileSync, readFileSync as readFileSync2, existsSync } from "fs";
39
- import { fileURLToPath as fileURLToPath2 } from "url";
39
+ import { fileURLToPath as fileURLToPath3 } from "url";
40
40
 
41
41
  // src/graph/serializer.ts
42
42
  import { DirectedGraph } from "graphology";
@@ -502,13 +502,269 @@ async function trackCommand(command, version = "unknown") {
502
502
  // src/commands/whatif.ts
503
503
  import { resolve } from "path";
504
504
  import chalk from "chalk";
505
+
506
+ // src/viz/whatif-server.ts
507
+ import express2 from "express";
508
+ import open2 from "open";
509
+ import { fileURLToPath as fileURLToPath2 } from "url";
510
+ import { dirname as dirname2, join as join3 } from "path";
511
+
512
+ // src/viz/generate-whatif-html.ts
513
+ function generateWhatIfHtml(currentVizData, simulatedVizData, simulationResult, operation, target) {
514
+ const { healthDelta, diff } = simulationResult;
515
+ const deltaSign = healthDelta.delta >= 0 ? "+" : "";
516
+ const deltaLabel = healthDelta.delta === 0 ? "unchanged" : healthDelta.improved ? `${deltaSign}${healthDelta.delta} \u2713 improved` : `${healthDelta.delta} \u2717 degraded`;
517
+ const deltaColor = healthDelta.delta === 0 ? "#fbbf24" : healthDelta.improved ? "#4ade80" : "#f87171";
518
+ const opBadge = operation !== "none" ? `<span style="background:${deltaColor};color:#000;padding:4px 12px;border-radius:4px;font-weight:700;font-size:13px;text-transform:uppercase;margin-left:12px;">${operation} ${target}</span>` : "";
519
+ const brokenImportsHtml = diff.brokenImports.length > 0 ? `<details style="margin-top:16px;background:#16213e;border:1px solid #2a2a4a;border-radius:8px;padding:12px 16px;">
520
+ <summary style="cursor:pointer;color:#f87171;font-weight:600;font-size:14px;">Broken Imports (${diff.brokenImports.length})</summary>
521
+ <ul style="margin:8px 0 0 16px;padding:0;list-style:none;">
522
+ ${diff.brokenImports.map((bi) => `<li style="color:#e0e0e0;font-size:13px;padding:4px 0;font-family:monospace;">${bi.file} \u2192 <span style="color:#f87171;">${bi.importedSymbol}</span></li>`).join("")}
523
+ </ul>
524
+ </details>` : "";
525
+ const currentDataJson = JSON.stringify(currentVizData);
526
+ const simulatedDataJson = JSON.stringify(simulatedVizData);
527
+ return `<!DOCTYPE html>
528
+ <html lang="en">
529
+ <head>
530
+ <meta charset="UTF-8">
531
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
532
+ <title>Depwire \u2014 What If Simulation</title>
533
+ <link rel="stylesheet" href="/style.css">
534
+ <style>
535
+ body { overflow: auto; height: auto; }
536
+ .whatif-header {
537
+ background: #16213e;
538
+ border-bottom: 1px solid #2a2a4a;
539
+ padding: 16px 24px;
540
+ display: flex;
541
+ align-items: center;
542
+ gap: 16px;
543
+ }
544
+ .whatif-header h1 {
545
+ margin: 0;
546
+ font-size: 20px;
547
+ font-weight: 600;
548
+ background: linear-gradient(135deg, #4a9eff, #7c3aed);
549
+ -webkit-background-clip: text;
550
+ -webkit-text-fill-color: transparent;
551
+ background-clip: text;
552
+ }
553
+ .health-banner {
554
+ background: #0f1729;
555
+ border: 1px solid #2a2a4a;
556
+ border-radius: 8px;
557
+ padding: 16px 24px;
558
+ margin: 16px 24px;
559
+ display: flex;
560
+ align-items: center;
561
+ gap: 32px;
562
+ flex-wrap: wrap;
563
+ }
564
+ .health-score {
565
+ font-size: 22px;
566
+ font-weight: 700;
567
+ }
568
+ .health-stat {
569
+ font-size: 14px;
570
+ color: #a0a0a0;
571
+ }
572
+ .health-stat strong {
573
+ color: #e0e0e0;
574
+ font-size: 18px;
575
+ }
576
+ .panels {
577
+ display: flex;
578
+ flex-direction: row;
579
+ gap: 0;
580
+ width: 100%;
581
+ height: calc(100vh - 180px);
582
+ min-height: 400px;
583
+ }
584
+ .panel {
585
+ flex: 1;
586
+ min-width: 0;
587
+ display: flex;
588
+ flex-direction: column;
589
+ border-right: 1px solid #2a2a4a;
590
+ overflow: hidden;
591
+ position: relative;
592
+ }
593
+ .panel:last-child { border-right: none; }
594
+ .panel-label {
595
+ background: #16213e;
596
+ padding: 8px 16px;
597
+ font-size: 13px;
598
+ font-weight: 600;
599
+ color: #a0a0a0;
600
+ border-bottom: 1px solid #2a2a4a;
601
+ display: flex;
602
+ justify-content: space-between;
603
+ flex-shrink: 0;
604
+ }
605
+ .panel-diagram {
606
+ flex: 1;
607
+ overflow: hidden;
608
+ position: relative;
609
+ }
610
+ .panel-diagram svg {
611
+ display: block;
612
+ width: 100%;
613
+ height: 100%;
614
+ }
615
+ .broken-section {
616
+ padding: 0 24px 24px;
617
+ }
618
+ </style>
619
+ </head>
620
+ <body>
621
+ <div class="whatif-header">
622
+ <h1>depwire \u2014 What If Simulation</h1>
623
+ ${opBadge}
624
+ </div>
625
+
626
+ <div class="health-banner">
627
+ <div class="health-score" style="color:${deltaColor}">
628
+ Health Score: ${healthDelta.before} \u2192 ${healthDelta.after}
629
+ <span style="font-size:16px;margin-left:8px;">(${deltaLabel})</span>
630
+ </div>
631
+ <div class="health-stat"><strong>${diff.affectedNodes.length}</strong> Affected Nodes</div>
632
+ <div class="health-stat"><strong>${diff.brokenImports.length}</strong> Broken Imports</div>
633
+ <div class="health-stat"><strong>${diff.removedEdges.length}</strong> Removed Edges</div>
634
+ </div>
635
+
636
+ <div class="panels">
637
+ <div class="panel">
638
+ <div class="panel-label">
639
+ <span>Current</span>
640
+ <span>${currentVizData.stats.totalFiles} files</span>
641
+ </div>
642
+ <div class="panel-diagram" id="arc-diagram-current">
643
+ <svg id="svg-current"></svg>
644
+ </div>
645
+ <div class="tooltip" id="tooltip-current"></div>
646
+ </div>
647
+ <div class="panel">
648
+ <div class="panel-label">
649
+ <span>After ${operation !== "none" ? operation.toUpperCase() : "\u2014"}</span>
650
+ <span>${simulatedVizData.stats.totalFiles} files</span>
651
+ </div>
652
+ <div class="panel-diagram" id="arc-diagram-simulated">
653
+ <svg id="svg-simulated"></svg>
654
+ </div>
655
+ <div class="tooltip" id="tooltip-simulated"></div>
656
+ </div>
657
+ </div>
658
+
659
+ <div class="broken-section">
660
+ ${brokenImportsHtml}
661
+ </div>
662
+
663
+ <script>window.__depwireWhatIf = true;</script>
664
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"></script>
665
+ <script src="/arc.js"></script>
666
+ <script>
667
+ const currentData = ${currentDataJson};
668
+ const simulatedData = ${simulatedDataJson};
669
+
670
+ const left = window.createArcDiagram('arc-diagram-current', 'svg-current', 'tooltip-current', currentData);
671
+ const right = window.createArcDiagram('arc-diagram-simulated', 'svg-simulated', 'tooltip-simulated', simulatedData);
672
+
673
+ left.render();
674
+ right.render();
675
+
676
+ window.addEventListener('resize', () => {
677
+ left.render();
678
+ right.render();
679
+ });
680
+ </script>
681
+ </body>
682
+ </html>`;
683
+ }
684
+
685
+ // src/viz/whatif-server.ts
686
+ var __filename2 = fileURLToPath2(import.meta.url);
687
+ var __dirname2 = dirname2(__filename2);
688
+ async function findAvailablePort2(startPort, maxAttempts = 10) {
689
+ const net = await import("net");
690
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
691
+ const testPort = startPort + attempt;
692
+ const isAvailable = await new Promise((resolve3) => {
693
+ const server = net.createServer();
694
+ server.once("error", () => {
695
+ resolve3(false);
696
+ });
697
+ server.once("listening", () => {
698
+ server.close();
699
+ resolve3(true);
700
+ });
701
+ server.listen(testPort, "127.0.0.1");
702
+ });
703
+ if (isAvailable) {
704
+ if (attempt > 0) {
705
+ console.error(`Port ${startPort} in use, using port ${testPort} instead`);
706
+ }
707
+ return testPort;
708
+ }
709
+ }
710
+ throw new Error(`No available ports found between ${startPort} and ${startPort + maxAttempts - 1}`);
711
+ }
712
+ async function serveWhatIfViz(currentVizData, simulatedVizData, simulationResult, operation, target) {
713
+ const availablePort = await findAvailablePort2(3335);
714
+ const app = express2();
715
+ app.get("/", (_req, res) => {
716
+ const html = generateWhatIfHtml(currentVizData, simulatedVizData, simulationResult, operation, target);
717
+ res.type("html").send(html);
718
+ });
719
+ app.get("/favicon.ico", (_req, res) => {
720
+ res.sendFile(join3(__dirname2, "..", "..", "icon.png"));
721
+ });
722
+ const publicDir = join3(__dirname2, "viz", "public");
723
+ app.use(express2.static(publicDir));
724
+ app.get("/api/graph", (_req, res) => {
725
+ res.json(currentVizData);
726
+ });
727
+ app.get("/api/current", (_req, res) => {
728
+ res.json(currentVizData);
729
+ });
730
+ app.get("/api/simulated", (_req, res) => {
731
+ res.json(simulatedVizData);
732
+ });
733
+ app.get("/api/result", (_req, res) => {
734
+ res.json(simulationResult);
735
+ });
736
+ const server = app.listen(availablePort, "127.0.0.1", () => {
737
+ const url = `http://127.0.0.1:${availablePort}`;
738
+ console.error(`
739
+ Opening What If UI at ${url}`);
740
+ console.error("Press Ctrl+C to stop\n");
741
+ open2(url);
742
+ });
743
+ process.on("SIGINT", () => {
744
+ console.error("\nShutting down What If server...");
745
+ server.close(() => {
746
+ process.exit(0);
747
+ });
748
+ });
749
+ }
750
+
751
+ // src/commands/whatif.ts
505
752
  async function whatif(dir, options) {
506
753
  if (!options.simulate) {
507
- console.log("Usage: depwire whatif [dir] --simulate <action> --target <file> [options]");
508
- console.log("");
509
- console.log("Actions: move, delete, rename, split, merge");
510
- console.log("");
511
- console.log("Run without --simulate to open interactive browser UI (Phase B)");
754
+ const projectRoot2 = dir === "." ? findProjectRoot() : resolve(dir);
755
+ console.error(`Parsing project: ${projectRoot2}`);
756
+ const parsedFiles2 = await parseProject(projectRoot2);
757
+ const graph2 = buildGraph(parsedFiles2);
758
+ console.error(`Built graph: ${graph2.order} symbols, ${graph2.size} edges`);
759
+ const vizData = prepareVizData(graph2, projectRoot2);
760
+ const emptyResult = {
761
+ action: { type: "delete", target: "" },
762
+ originalGraph: { nodeCount: graph2.order, edgeCount: graph2.size, healthScore: 0 },
763
+ simulatedGraph: { nodeCount: graph2.order, edgeCount: graph2.size, healthScore: 0 },
764
+ diff: { addedEdges: [], removedEdges: [], affectedNodes: [], brokenImports: [], circularDepsIntroduced: [], circularDepsResolved: [] },
765
+ healthDelta: { before: 0, after: 0, delta: 0, improved: false, dimensionChanges: [] }
766
+ };
767
+ await serveWhatIfViz(vizData, vizData, emptyResult, "none", "");
512
768
  return;
513
769
  }
514
770
  const validActions = ["move", "delete", "rename", "split", "merge"];
@@ -522,11 +778,11 @@ async function whatif(dir, options) {
522
778
  }
523
779
  const action = buildAction(options);
524
780
  const projectRoot = dir === "." ? findProjectRoot() : resolve(dir);
525
- console.log(`Parsing project: ${projectRoot}`);
781
+ console.error(`Parsing project: ${projectRoot}`);
526
782
  const parsedFiles = await parseProject(projectRoot);
527
783
  const graph = buildGraph(parsedFiles);
528
- console.log(`Built graph: ${graph.order} symbols, ${graph.size} edges`);
529
- console.log("");
784
+ console.error(`Built graph: ${graph.order} symbols, ${graph.size} edges`);
785
+ console.error("");
530
786
  const engine = new SimulationEngine(graph);
531
787
  try {
532
788
  const result = engine.simulate(action);
@@ -632,9 +888,9 @@ function formatAction(action) {
632
888
  }
633
889
 
634
890
  // src/index.ts
635
- var __filename2 = fileURLToPath2(import.meta.url);
636
- var __dirname2 = dirname2(__filename2);
637
- var packageJsonPath = join3(__dirname2, "../package.json");
891
+ var __filename3 = fileURLToPath3(import.meta.url);
892
+ var __dirname3 = dirname3(__filename3);
893
+ var packageJsonPath = join4(__dirname3, "../package.json");
638
894
  var packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
639
895
  var program = new Command();
640
896
  program.name("depwire").description("Code cross-reference graph builder for TypeScript projects").version(packageJson.version);
@@ -777,7 +1033,7 @@ program.command("mcp").description("Start MCP server for AI coding tools").argum
777
1033
  } else {
778
1034
  const detectedRoot = findProjectRoot();
779
1035
  const cwd = process.cwd();
780
- if (detectedRoot !== cwd || existsSync(join3(cwd, "package.json")) || existsSync(join3(cwd, "tsconfig.json")) || existsSync(join3(cwd, "go.mod")) || existsSync(join3(cwd, "pyproject.toml")) || existsSync(join3(cwd, "setup.py")) || existsSync(join3(cwd, ".git"))) {
1036
+ 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"))) {
781
1037
  projectRootToConnect = detectedRoot;
782
1038
  }
783
1039
  }
@@ -835,7 +1091,7 @@ program.command("docs").description("Generate comprehensive codebase documentati
835
1091
  const startTime = Date.now();
836
1092
  try {
837
1093
  const projectRoot = directory ? resolve2(directory) : findProjectRoot();
838
- const outputDir = options.output ? resolve2(options.output) : join3(projectRoot, ".depwire");
1094
+ const outputDir = options.output ? resolve2(options.output) : join4(projectRoot, ".depwire");
839
1095
  const includeList = options.include.split(",").map((s) => s.trim());
840
1096
  const onlyList = options.only ? options.only.split(",").map((s) => s.trim()) : void 0;
841
1097
  if (options.gitignore === void 0 && !existsSyncNode(outputDir)) {
@@ -906,7 +1162,7 @@ async function promptGitignore() {
906
1162
  });
907
1163
  }
908
1164
  function addToGitignore(projectRoot, pattern) {
909
- const gitignorePath = join3(projectRoot, ".gitignore");
1165
+ const gitignorePath = join4(projectRoot, ".gitignore");
910
1166
  try {
911
1167
  let content = "";
912
1168
  if (existsSyncNode(gitignorePath)) {
@@ -1,4 +1,176 @@
1
1
  // Arc Diagram Renderer using D3.js
2
+
3
+ // ── Factory function for creating isolated arc diagram instances ──
4
+ window.createArcDiagram = function(containerId, svgId, tooltipId, data) {
5
+ let _data = data;
6
+ let _svg = null;
7
+ let _g = null;
8
+ let _filePositions = new Map();
9
+ let _selectedFile = null;
10
+ let _selectedArc = null;
11
+
12
+ function _showTooltip(event, content) {
13
+ const el = document.getElementById(tooltipId);
14
+ if (!el) return;
15
+ el.innerHTML = content;
16
+ el.style.left = (event.pageX + 10) + 'px';
17
+ el.style.top = (event.pageY + 10) + 'px';
18
+ el.classList.add('show');
19
+ }
20
+
21
+ function _hideTooltip() {
22
+ const el = document.getElementById(tooltipId);
23
+ if (!el) return;
24
+ el.classList.remove('show');
25
+ }
26
+
27
+ function _clearSelection() {
28
+ _selectedFile = null;
29
+ _selectedArc = null;
30
+ const ctr = d3.select('#' + containerId);
31
+ ctr.selectAll('.arc').classed('highlighted', false).classed('dimmed', false);
32
+ ctr.selectAll('.file-bar').classed('highlighted', false).classed('dimmed', false);
33
+ }
34
+
35
+ function render() {
36
+ const container = document.getElementById(containerId);
37
+ if (!container || !_data) return;
38
+
39
+ const width = container.clientWidth;
40
+ const height = container.clientHeight;
41
+ _filePositions.clear();
42
+ _selectedFile = null;
43
+ _selectedArc = null;
44
+
45
+ const svgEl = d3.select('#' + svgId);
46
+ svgEl.selectAll('*').remove();
47
+ _svg = svgEl.attr('width', width).attr('height', height);
48
+ _g = _svg.append('g');
49
+
50
+ const zoom = d3.zoom().scaleExtent([0.5, 4]).on('zoom', (event) => {
51
+ _g.attr('transform', event.transform);
52
+ });
53
+ _svg.call(zoom);
54
+
55
+ const margin = { top: 60, right: 40, bottom: 120, left: 40 };
56
+ const plotWidth = width - margin.left - margin.right;
57
+ const plotHeight = height - margin.top - margin.bottom;
58
+ const baseline = margin.top + plotHeight;
59
+
60
+ const totalSymbols = d3.sum(_data.files, d => d.symbolCount);
61
+ const minBarWidth = 4;
62
+ const gap = 2;
63
+ let x = margin.left;
64
+
65
+ _data.files.forEach(file => {
66
+ const barWidth = Math.max(minBarWidth, (file.symbolCount / totalSymbols) * plotWidth * 0.8);
67
+ _filePositions.set(file.path, { x: x + barWidth / 2, width: barWidth, file: file });
68
+ x += barWidth + gap;
69
+ });
70
+
71
+ const directories = [...new Set(_data.files.map(f => f.directory))];
72
+ const colorScale = d3.scaleOrdinal().domain(directories)
73
+ .range(['#4a9eff', '#7c3aed', '#ec4899', '#f59e0b', '#10b981', '#06b6d4']);
74
+
75
+ const maxDistance = d3.max(_data.arcs, arc => {
76
+ const s = _filePositions.get(arc.sourceFile);
77
+ const t = _filePositions.get(arc.targetFile);
78
+ return (s && t) ? Math.abs(t.x - s.x) : 0;
79
+ }) || 1;
80
+
81
+ const ctr = d3.select('#' + containerId);
82
+
83
+ _g.selectAll('.arc').data(_data.arcs).enter().append('path')
84
+ .attr('class', 'arc')
85
+ .attr('d', d => {
86
+ const s = _filePositions.get(d.sourceFile);
87
+ const t = _filePositions.get(d.targetFile);
88
+ if (!s || !t) return null;
89
+ const x1 = s.x, x2 = t.x, dist = Math.abs(x2 - x1), midX = (x1 + x2) / 2;
90
+ return `M ${x1} ${baseline} Q ${midX} ${baseline - dist * 0.4} ${x2} ${baseline}`;
91
+ })
92
+ .attr('stroke', d => {
93
+ const s = _filePositions.get(d.sourceFile);
94
+ const t = _filePositions.get(d.targetFile);
95
+ if (!s || !t) return '#4a9eff';
96
+ return d3.interpolateRainbow(Math.abs(t.x - s.x) / maxDistance);
97
+ })
98
+ .attr('stroke-width', d => Math.min(4, 1 + Math.log(d.edgeCount)))
99
+ .on('mouseover', function(event, d) {
100
+ if (_selectedArc) return;
101
+ d3.select(this).classed('highlighted', true);
102
+ ctr.selectAll('.arc').filter(a => a !== d).classed('dimmed', true);
103
+ ctr.selectAll('.file-bar').each(function(f) {
104
+ const match = f.path === d.sourceFile || f.path === d.targetFile;
105
+ d3.select(this).classed('highlighted', match).classed('dimmed', !match);
106
+ });
107
+ _showTooltip(event, `<div class="tooltip-line"><strong>${d.sourceFile}</strong> → <strong>${d.targetFile}</strong></div><div class="tooltip-line"><span class="tooltip-label">Edges:</span> ${d.edgeCount}</div>`);
108
+ })
109
+ .on('mouseout', function() {
110
+ if (_selectedArc) return;
111
+ d3.select(this).classed('highlighted', false);
112
+ ctr.selectAll('.arc').classed('dimmed', false);
113
+ ctr.selectAll('.file-bar').classed('highlighted', false).classed('dimmed', false);
114
+ _hideTooltip();
115
+ })
116
+ .on('click', function(event, d) {
117
+ event.stopPropagation();
118
+ if (_selectedArc === d) { _selectedArc = null; ctr.selectAll('.arc,.file-bar').classed('highlighted', false).classed('dimmed', false); _hideTooltip(); return; }
119
+ _selectedArc = d; _selectedFile = null;
120
+ ctr.selectAll('.arc').classed('highlighted', false).classed('dimmed', true);
121
+ d3.select(this).classed('highlighted', true).classed('dimmed', false);
122
+ ctr.selectAll('.file-bar').each(function(f) {
123
+ const match = f.path === d.sourceFile || f.path === d.targetFile;
124
+ d3.select(this).classed('highlighted', match).classed('dimmed', !match);
125
+ });
126
+ });
127
+
128
+ _g.selectAll('.file-bar').data(_data.files).enter().append('rect')
129
+ .attr('class', 'file-bar')
130
+ .attr('x', d => { const p = _filePositions.get(d.path); return p.x - p.width / 2; })
131
+ .attr('y', baseline).attr('width', d => _filePositions.get(d.path).width).attr('height', 8)
132
+ .attr('fill', d => colorScale(d.directory))
133
+ .on('mouseover', function(event, d) {
134
+ if (_selectedFile) return;
135
+ d3.select(this).classed('highlighted', true);
136
+ const connected = _data.arcs.filter(a => a.sourceFile === d.path || a.targetFile === d.path);
137
+ ctr.selectAll('.arc').classed('highlighted', a => connected.includes(a)).classed('dimmed', a => !connected.includes(a));
138
+ ctr.selectAll('.file-bar').filter(f => f !== d).classed('dimmed', true);
139
+ _showTooltip(event, `<div class="tooltip-line"><strong>${d.path}</strong></div><div class="tooltip-line"><span class="tooltip-label">Symbols:</span> ${d.symbolCount} | In: ${d.incomingCount} | Out: ${d.outgoingCount}</div>`);
140
+ })
141
+ .on('mouseout', function() {
142
+ if (_selectedFile) return;
143
+ d3.select(this).classed('highlighted', false);
144
+ ctr.selectAll('.arc').classed('highlighted', false).classed('dimmed', false);
145
+ ctr.selectAll('.file-bar').classed('dimmed', false);
146
+ _hideTooltip();
147
+ })
148
+ .on('click', function(event, d) {
149
+ event.stopPropagation();
150
+ if (_selectedFile === d) { _selectedFile = null; ctr.selectAll('.arc,.file-bar').classed('highlighted', false).classed('dimmed', false); return; }
151
+ _selectedFile = d; _selectedArc = null;
152
+ const connected = _data.arcs.filter(a => a.sourceFile === d.path || a.targetFile === d.path);
153
+ ctr.selectAll('.arc').classed('highlighted', a => connected.includes(a)).classed('dimmed', a => !connected.includes(a));
154
+ ctr.selectAll('.file-bar').classed('highlighted', f => f === d).classed('dimmed', f => f !== d);
155
+ });
156
+
157
+ _g.selectAll('.file-label').data(_data.files).enter().append('text')
158
+ .attr('class', 'file-label')
159
+ .attr('x', d => _filePositions.get(d.path).x)
160
+ .attr('y', baseline + 20)
161
+ .attr('transform', d => `rotate(-45, ${_filePositions.get(d.path).x}, ${baseline + 20})`)
162
+ .attr('text-anchor', 'end')
163
+ .text(d => d.path.split('/').pop());
164
+
165
+ _svg.append('text').attr('x', 10).attr('y', 20).attr('fill', '#4a9eff')
166
+ .attr('font-size', '12px').attr('cursor', 'pointer').text('↺ Reset View')
167
+ .on('click', () => { _svg.transition().duration(750).call(zoom.transform, d3.zoomIdentity); _clearSelection(); });
168
+ }
169
+
170
+ return { render };
171
+ };
172
+
173
+ // ── Legacy global state and functions (used by depwire viz) ──
2
174
  let graphData = null;
3
175
  let svg = null;
4
176
  let g = null;
@@ -23,12 +195,16 @@ async function init() {
23
195
  // Render diagram
24
196
  renderArcDiagram();
25
197
 
26
- // Setup interactions
27
- setupSearch();
28
- setupExport();
198
+ // Setup interactions (not in whatif mode)
199
+ if (!window.__depwireWhatIf) {
200
+ setupSearch();
201
+ setupExport();
202
+ }
29
203
 
30
- // Setup WebSocket for live updates
31
- setupWebSocket();
204
+ // Setup WebSocket for live updates (not in whatif mode)
205
+ if (!window.__depwireWhatIf) {
206
+ setupWebSocket();
207
+ }
32
208
 
33
209
  // Handle window resize
34
210
  window.addEventListener('resize', () => {
@@ -575,5 +751,7 @@ d3.select('body').on('click', () => {
575
751
  }
576
752
  });
577
753
 
578
- // Initialize on page load
579
- init();
754
+ // Initialize on page load — only if NOT in What If context
755
+ if (!window.__depwireWhatIf) {
756
+ init();
757
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "depwire-cli",
3
- "version": "0.9.24",
3
+ "version": "0.9.25",
4
4
  "description": "Dependency graph + 16 MCP tools for AI coding assistants. Impact analysis, health scoring, visualization.",
5
5
  "type": "module",
6
6
  "bin": {