depwire-cli 0.6.2 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,26 +2,37 @@
2
2
  import {
3
3
  buildGraph,
4
4
  calculateHealthScore,
5
+ checkoutCommit,
5
6
  createEmptyState,
7
+ createSnapshot,
6
8
  findProjectRoot,
7
9
  generateDocs,
8
10
  getArchitectureSummary,
11
+ getCommitLog,
12
+ getCurrentBranch,
9
13
  getHealthTrend,
10
14
  getImpact,
15
+ isGitRepo,
16
+ loadSnapshot,
11
17
  parseProject,
18
+ popStash,
12
19
  prepareVizData,
20
+ restoreOriginal,
21
+ sampleCommits,
22
+ saveSnapshot,
13
23
  searchSymbols,
14
24
  startMcpServer,
15
25
  startVizServer,
26
+ stashChanges,
16
27
  updateFileInGraph,
17
28
  watchProject
18
- } from "./chunk-VNUOE5VC.js";
29
+ } from "./chunk-65H7HCM4.js";
19
30
 
20
31
  // src/index.ts
21
32
  import { Command } from "commander";
22
- import { resolve, dirname, join } from "path";
23
- import { writeFileSync, readFileSync, existsSync } from "fs";
24
- import { fileURLToPath } from "url";
33
+ import { resolve, dirname as dirname2, join as join3 } from "path";
34
+ import { writeFileSync, readFileSync as readFileSync2, existsSync } from "fs";
35
+ import { fileURLToPath as fileURLToPath2 } from "url";
25
36
 
26
37
  // src/graph/serializer.ts
27
38
  import { DirectedGraph } from "graphology";
@@ -207,10 +218,266 @@ function gray(text) {
207
218
  // src/index.ts
208
219
  import { readFileSync as readFileSyncNode, appendFileSync, existsSync as existsSyncNode } from "fs";
209
220
  import { createInterface } from "readline";
221
+
222
+ // src/temporal/index.ts
223
+ import { join as join2 } from "path";
224
+
225
+ // src/viz/temporal-server.ts
226
+ import express from "express";
227
+ import { readFileSync } from "fs";
228
+ import { fileURLToPath } from "url";
229
+ import { dirname, join } from "path";
230
+ import open from "open";
231
+
232
+ // src/viz/temporal-data.ts
233
+ import { basename } from "path";
234
+
235
+ // src/temporal/diff.ts
236
+ function diffSnapshots(previous, current) {
237
+ const prevFiles = new Set(previous.files.map((f) => f.path));
238
+ const currFiles = new Set(current.files.map((f) => f.path));
239
+ const addedFiles = Array.from(currFiles).filter((f) => !prevFiles.has(f));
240
+ const removedFiles = Array.from(prevFiles).filter((f) => !currFiles.has(f));
241
+ const prevEdges = new Set(
242
+ previous.edges.map((e) => `${e.source}|${e.target}`)
243
+ );
244
+ const currEdges = new Set(current.edges.map((e) => `${e.source}|${e.target}`));
245
+ const addedEdgeKeys = Array.from(currEdges).filter((e) => !prevEdges.has(e));
246
+ const removedEdgeKeys = Array.from(prevEdges).filter((e) => !currEdges.has(e));
247
+ const addedEdges = addedEdgeKeys.map((key) => {
248
+ const [source, target] = key.split("|");
249
+ return { source, target };
250
+ });
251
+ const removedEdges = removedEdgeKeys.map((key) => {
252
+ const [source, target] = key.split("|");
253
+ return { source, target };
254
+ });
255
+ return {
256
+ addedFiles,
257
+ removedFiles,
258
+ addedEdges,
259
+ removedEdges,
260
+ statsChange: {
261
+ files: current.stats.totalFiles - previous.stats.totalFiles,
262
+ symbols: current.stats.totalSymbols - previous.stats.totalSymbols,
263
+ edges: current.stats.totalEdges - previous.stats.totalEdges
264
+ }
265
+ };
266
+ }
267
+
268
+ // src/viz/temporal-data.ts
269
+ function prepareTemporalVizData(snapshots, projectRoot) {
270
+ const projectName = basename(projectRoot);
271
+ const snapshotsWithDiff = snapshots.map((snapshot, index) => {
272
+ const diff = index > 0 ? diffSnapshots(snapshots[index - 1], snapshot) : void 0;
273
+ return {
274
+ commitHash: snapshot.commitHash,
275
+ commitDate: snapshot.commitDate,
276
+ commitMessage: snapshot.commitMessage,
277
+ commitAuthor: snapshot.commitAuthor,
278
+ stats: snapshot.stats,
279
+ files: snapshot.files,
280
+ arcs: snapshot.edges,
281
+ diff
282
+ };
283
+ });
284
+ const timeline = snapshots.map((snapshot, index) => ({
285
+ index,
286
+ date: snapshot.commitDate,
287
+ shortHash: snapshot.commitHash.substring(0, 8),
288
+ message: snapshot.commitMessage
289
+ }));
290
+ return {
291
+ projectName,
292
+ snapshots: snapshotsWithDiff,
293
+ timeline
294
+ };
295
+ }
296
+
297
+ // src/viz/temporal-server.ts
210
298
  var __filename = fileURLToPath(import.meta.url);
211
299
  var __dirname = dirname(__filename);
212
- var packageJsonPath = join(__dirname, "../package.json");
213
- var packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
300
+ async function findAvailablePort(startPort) {
301
+ const net = await import("net");
302
+ for (let attempt = 0; attempt < 10; attempt++) {
303
+ const testPort = startPort + attempt;
304
+ const isAvailable = await new Promise((resolve2) => {
305
+ const server = net.createServer().once("error", () => resolve2(false)).once("listening", () => {
306
+ server.close();
307
+ resolve2(true);
308
+ }).listen(testPort, "127.0.0.1");
309
+ });
310
+ if (isAvailable) {
311
+ return testPort;
312
+ }
313
+ }
314
+ throw new Error(`No available port found starting from ${startPort}`);
315
+ }
316
+ async function startTemporalServer(snapshots, projectRoot, preferredPort = 3334) {
317
+ const availablePort = await findAvailablePort(preferredPort);
318
+ const app = express();
319
+ const vizData = prepareTemporalVizData(snapshots, projectRoot);
320
+ app.get("/api/data", (_req, res) => {
321
+ res.json(vizData);
322
+ });
323
+ const publicDir = join(__dirname, "viz", "public");
324
+ app.get("/", (_req, res) => {
325
+ const htmlPath = join(publicDir, "temporal.html");
326
+ const html = readFileSync(htmlPath, "utf-8");
327
+ res.send(html);
328
+ });
329
+ app.get("/temporal.js", (_req, res) => {
330
+ const jsPath = join(publicDir, "temporal.js");
331
+ const js = readFileSync(jsPath, "utf-8");
332
+ res.type("application/javascript").send(js);
333
+ });
334
+ app.get("/temporal.css", (_req, res) => {
335
+ const cssPath = join(publicDir, "temporal.css");
336
+ const css = readFileSync(cssPath, "utf-8");
337
+ res.type("text/css").send(css);
338
+ });
339
+ const server = app.listen(availablePort, "127.0.0.1", () => {
340
+ const url = `http://127.0.0.1:${availablePort}`;
341
+ console.log(`
342
+ \u2713 Temporal visualization server running at ${url}`);
343
+ console.log(" Press Ctrl+C to stop\n");
344
+ open(url).catch(() => {
345
+ console.log(" (Could not open browser automatically)");
346
+ });
347
+ });
348
+ await new Promise((resolve2, reject) => {
349
+ server.on("error", reject);
350
+ process.on("SIGINT", () => {
351
+ console.log("\n\nShutting down temporal server...");
352
+ server.close(() => {
353
+ console.log("Server stopped");
354
+ resolve2();
355
+ process.exit(0);
356
+ });
357
+ });
358
+ });
359
+ }
360
+
361
+ // src/temporal/index.ts
362
+ async function runTemporalAnalysis(projectDir, options) {
363
+ if (!isGitRepo(projectDir)) {
364
+ throw new Error("Not a git repository. Temporal analysis requires git history.");
365
+ }
366
+ console.log("\u{1F50D} Analyzing git history...");
367
+ const originalBranch = await getCurrentBranch(projectDir);
368
+ const hadStash = await stashChanges(projectDir);
369
+ try {
370
+ const outputDir = options.output || join2(projectDir, ".depwire", "temporal");
371
+ const commits = await getCommitLog(projectDir);
372
+ if (commits.length === 0) {
373
+ throw new Error("No commits found in repository");
374
+ }
375
+ console.log(`Found ${commits.length} commits`);
376
+ const sampledCommits = sampleCommits(
377
+ commits,
378
+ options.commits,
379
+ options.strategy
380
+ );
381
+ console.log(
382
+ `Sampled ${sampledCommits.length} commits using ${options.strategy} strategy`
383
+ );
384
+ const snapshots = [];
385
+ for (let i = 0; i < sampledCommits.length; i++) {
386
+ const commit = sampledCommits[i];
387
+ const progress = `[${i + 1}/${sampledCommits.length}]`;
388
+ const existingSnapshot = loadSnapshot(commit.hash, outputDir);
389
+ if (existingSnapshot) {
390
+ if (options.verbose) {
391
+ console.log(
392
+ `${progress} Using cached snapshot for ${commit.hash.substring(0, 8)} - ${commit.message}`
393
+ );
394
+ }
395
+ snapshots.push(existingSnapshot);
396
+ continue;
397
+ }
398
+ if (options.verbose) {
399
+ console.log(
400
+ `${progress} Parsing commit ${commit.hash.substring(0, 8)} - ${commit.message}`
401
+ );
402
+ }
403
+ await checkoutCommit(projectDir, commit.hash);
404
+ const parsedFiles = await parseProject(projectDir);
405
+ const graph = buildGraph(parsedFiles);
406
+ const projectGraph = exportToJSON(graph, projectDir);
407
+ const snapshot = createSnapshot(
408
+ projectGraph,
409
+ commit.hash,
410
+ commit.date,
411
+ commit.message,
412
+ commit.author
413
+ );
414
+ saveSnapshot(snapshot, outputDir);
415
+ snapshots.push(snapshot);
416
+ }
417
+ await restoreOriginal(projectDir, originalBranch);
418
+ if (hadStash) {
419
+ await popStash(projectDir);
420
+ }
421
+ snapshots.reverse();
422
+ console.log(`\u2713 Created ${snapshots.length} snapshots`);
423
+ if (options.stats) {
424
+ printStats(snapshots);
425
+ }
426
+ console.log("\n\u{1F680} Starting temporal visualization server...");
427
+ await startTemporalServer(snapshots, projectDir, options.port);
428
+ } catch (error) {
429
+ await restoreOriginal(projectDir, originalBranch);
430
+ if (hadStash) {
431
+ await popStash(projectDir);
432
+ }
433
+ throw error;
434
+ }
435
+ }
436
+ function printStats(snapshots) {
437
+ console.log("\n\u{1F4CA} Temporal Analysis Statistics:");
438
+ const first = snapshots[0];
439
+ const last = snapshots[snapshots.length - 1];
440
+ console.log(
441
+ `
442
+ Time Range: ${new Date(first.commitDate).toLocaleDateString()} \u2192 ${new Date(last.commitDate).toLocaleDateString()}`
443
+ );
444
+ console.log(`
445
+ Growth:`);
446
+ console.log(
447
+ ` Files: ${first.stats.totalFiles} \u2192 ${last.stats.totalFiles} (${last.stats.totalFiles >= first.stats.totalFiles ? "+" : ""}${last.stats.totalFiles - first.stats.totalFiles})`
448
+ );
449
+ console.log(
450
+ ` Symbols: ${first.stats.totalSymbols} \u2192 ${last.stats.totalSymbols} (${last.stats.totalSymbols >= first.stats.totalSymbols ? "+" : ""}${last.stats.totalSymbols - first.stats.totalSymbols})`
451
+ );
452
+ console.log(
453
+ ` Edges: ${first.stats.totalEdges} \u2192 ${last.stats.totalEdges} (${last.stats.totalEdges >= first.stats.totalEdges ? "+" : ""}${last.stats.totalEdges - first.stats.totalEdges})`
454
+ );
455
+ let maxGrowth = { index: 0, files: 0 };
456
+ for (let i = 1; i < snapshots.length; i++) {
457
+ const growth = snapshots[i].stats.totalFiles - snapshots[i - 1].stats.totalFiles;
458
+ if (growth > maxGrowth.files) {
459
+ maxGrowth = { index: i, files: growth };
460
+ }
461
+ }
462
+ if (maxGrowth.files > 0) {
463
+ const growthCommit = snapshots[maxGrowth.index];
464
+ console.log(`
465
+ Biggest Growth Period:`);
466
+ console.log(
467
+ ` +${maxGrowth.files} files at ${new Date(growthCommit.commitDate).toLocaleDateString()}`
468
+ );
469
+ console.log(` ${growthCommit.commitMessage}`);
470
+ }
471
+ const trend = last.stats.totalFiles > first.stats.totalFiles ? "Growing" : last.stats.totalFiles < first.stats.totalFiles ? "Shrinking" : "Stable";
472
+ console.log(`
473
+ Overall Trend: ${trend}`);
474
+ }
475
+
476
+ // src/index.ts
477
+ var __filename2 = fileURLToPath2(import.meta.url);
478
+ var __dirname2 = dirname2(__filename2);
479
+ var packageJsonPath = join3(__dirname2, "../package.json");
480
+ var packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
214
481
  var program = new Command();
215
482
  program.name("depwire").description("Code cross-reference graph builder for TypeScript projects").version(packageJson.version);
216
483
  program.command("parse").description("Parse a TypeScript project and build dependency graph").argument("[directory]", "Project directory to parse (defaults to current directory or auto-detected project root)").option("-o, --output <path>", "Output JSON file path", "depwire-output.json").option("--pretty", "Pretty-print JSON output").option("--stats", "Print summary statistics").option("--exclude <patterns...>", 'Glob patterns to exclude (e.g., "**/*.test.*" "dist/**")').option("--verbose", "Show detailed parsing progress").action(async (directory, options) => {
@@ -259,7 +526,7 @@ program.command("query").description("Query impact analysis for a symbol").argum
259
526
  let graph;
260
527
  if (existsSync(cacheFile)) {
261
528
  console.log("Loading from cache...");
262
- const json = JSON.parse(readFileSync(cacheFile, "utf-8"));
529
+ const json = JSON.parse(readFileSync2(cacheFile, "utf-8"));
263
530
  graph = importFromJSON(json);
264
531
  } else {
265
532
  console.log("Parsing project...");
@@ -322,6 +589,22 @@ program.command("viz").description("Launch interactive arc diagram visualization
322
589
  process.exit(1);
323
590
  }
324
591
  });
592
+ 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) => {
593
+ try {
594
+ const projectRoot = directory ? resolve(directory) : findProjectRoot();
595
+ await runTemporalAnalysis(projectRoot, {
596
+ commits: parseInt(options.commits, 10),
597
+ strategy: options.strategy,
598
+ port: parseInt(options.port, 10),
599
+ output: options.output,
600
+ verbose: options.verbose,
601
+ stats: options.stats
602
+ });
603
+ } catch (err) {
604
+ console.error("Error running temporal analysis:", err);
605
+ process.exit(1);
606
+ }
607
+ });
325
608
  program.command("mcp").description("Start MCP server for AI coding tools").argument("[directory]", "Project directory to analyze (optional - auto-detects project root or use connect_repo tool to connect later)").action(async (directory) => {
326
609
  try {
327
610
  const state = createEmptyState();
@@ -331,7 +614,7 @@ program.command("mcp").description("Start MCP server for AI coding tools").argum
331
614
  } else {
332
615
  const detectedRoot = findProjectRoot();
333
616
  const cwd = process.cwd();
334
- if (detectedRoot !== cwd || existsSync(join(cwd, "package.json")) || existsSync(join(cwd, "tsconfig.json")) || existsSync(join(cwd, "go.mod")) || existsSync(join(cwd, "pyproject.toml")) || existsSync(join(cwd, "setup.py")) || existsSync(join(cwd, ".git"))) {
617
+ 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"))) {
335
618
  projectRootToConnect = detectedRoot;
336
619
  }
337
620
  }
@@ -388,7 +671,7 @@ program.command("docs").description("Generate comprehensive codebase documentati
388
671
  const startTime = Date.now();
389
672
  try {
390
673
  const projectRoot = directory ? resolve(directory) : findProjectRoot();
391
- const outputDir = options.output ? resolve(options.output) : join(projectRoot, ".depwire");
674
+ const outputDir = options.output ? resolve(options.output) : join3(projectRoot, ".depwire");
392
675
  const includeList = options.include.split(",").map((s) => s.trim());
393
676
  const onlyList = options.only ? options.only.split(",").map((s) => s.trim()) : void 0;
394
677
  if (options.gitignore === void 0 && !existsSyncNode(outputDir)) {
@@ -459,7 +742,7 @@ async function promptGitignore() {
459
742
  });
460
743
  }
461
744
  function addToGitignore(projectRoot, pattern) {
462
- const gitignorePath = join(projectRoot, ".gitignore");
745
+ const gitignorePath = join3(projectRoot, ".gitignore");
463
746
  try {
464
747
  let content = "";
465
748
  if (existsSyncNode(gitignorePath)) {
@@ -6,7 +6,7 @@ import {
6
6
  startMcpServer,
7
7
  updateFileInGraph,
8
8
  watchProject
9
- } from "./chunk-VNUOE5VC.js";
9
+ } from "./chunk-65H7HCM4.js";
10
10
 
11
11
  // src/mcpb-entry.ts
12
12
  import { resolve } from "path";