depwire-cli 0.6.2 → 0.7.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/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,265 @@ 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
+ console.log(`\u2713 Created ${snapshots.length} snapshots`);
422
+ if (options.stats) {
423
+ printStats(snapshots);
424
+ }
425
+ console.log("\n\u{1F680} Starting temporal visualization server...");
426
+ await startTemporalServer(snapshots, projectDir, options.port);
427
+ } catch (error) {
428
+ await restoreOriginal(projectDir, originalBranch);
429
+ if (hadStash) {
430
+ await popStash(projectDir);
431
+ }
432
+ throw error;
433
+ }
434
+ }
435
+ function printStats(snapshots) {
436
+ console.log("\n\u{1F4CA} Temporal Analysis Statistics:");
437
+ const first = snapshots[0];
438
+ const last = snapshots[snapshots.length - 1];
439
+ console.log(
440
+ `
441
+ Time Range: ${new Date(first.commitDate).toLocaleDateString()} \u2192 ${new Date(last.commitDate).toLocaleDateString()}`
442
+ );
443
+ console.log(`
444
+ Growth:`);
445
+ console.log(
446
+ ` Files: ${first.stats.totalFiles} \u2192 ${last.stats.totalFiles} (${last.stats.totalFiles >= first.stats.totalFiles ? "+" : ""}${last.stats.totalFiles - first.stats.totalFiles})`
447
+ );
448
+ console.log(
449
+ ` Symbols: ${first.stats.totalSymbols} \u2192 ${last.stats.totalSymbols} (${last.stats.totalSymbols >= first.stats.totalSymbols ? "+" : ""}${last.stats.totalSymbols - first.stats.totalSymbols})`
450
+ );
451
+ console.log(
452
+ ` Edges: ${first.stats.totalEdges} \u2192 ${last.stats.totalEdges} (${last.stats.totalEdges >= first.stats.totalEdges ? "+" : ""}${last.stats.totalEdges - first.stats.totalEdges})`
453
+ );
454
+ let maxGrowth = { index: 0, files: 0 };
455
+ for (let i = 1; i < snapshots.length; i++) {
456
+ const growth = snapshots[i].stats.totalFiles - snapshots[i - 1].stats.totalFiles;
457
+ if (growth > maxGrowth.files) {
458
+ maxGrowth = { index: i, files: growth };
459
+ }
460
+ }
461
+ if (maxGrowth.files > 0) {
462
+ const growthCommit = snapshots[maxGrowth.index];
463
+ console.log(`
464
+ Biggest Growth Period:`);
465
+ console.log(
466
+ ` +${maxGrowth.files} files at ${new Date(growthCommit.commitDate).toLocaleDateString()}`
467
+ );
468
+ console.log(` ${growthCommit.commitMessage}`);
469
+ }
470
+ const trend = last.stats.totalFiles > first.stats.totalFiles ? "Growing" : last.stats.totalFiles < first.stats.totalFiles ? "Shrinking" : "Stable";
471
+ console.log(`
472
+ Overall Trend: ${trend}`);
473
+ }
474
+
475
+ // src/index.ts
476
+ var __filename2 = fileURLToPath2(import.meta.url);
477
+ var __dirname2 = dirname2(__filename2);
478
+ var packageJsonPath = join3(__dirname2, "../package.json");
479
+ var packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
214
480
  var program = new Command();
215
481
  program.name("depwire").description("Code cross-reference graph builder for TypeScript projects").version(packageJson.version);
216
482
  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 +525,7 @@ program.command("query").description("Query impact analysis for a symbol").argum
259
525
  let graph;
260
526
  if (existsSync(cacheFile)) {
261
527
  console.log("Loading from cache...");
262
- const json = JSON.parse(readFileSync(cacheFile, "utf-8"));
528
+ const json = JSON.parse(readFileSync2(cacheFile, "utf-8"));
263
529
  graph = importFromJSON(json);
264
530
  } else {
265
531
  console.log("Parsing project...");
@@ -322,6 +588,22 @@ program.command("viz").description("Launch interactive arc diagram visualization
322
588
  process.exit(1);
323
589
  }
324
590
  });
591
+ 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) => {
592
+ try {
593
+ const projectRoot = directory ? resolve(directory) : findProjectRoot();
594
+ await runTemporalAnalysis(projectRoot, {
595
+ commits: parseInt(options.commits, 10),
596
+ strategy: options.strategy,
597
+ port: parseInt(options.port, 10),
598
+ output: options.output,
599
+ verbose: options.verbose,
600
+ stats: options.stats
601
+ });
602
+ } catch (err) {
603
+ console.error("Error running temporal analysis:", err);
604
+ process.exit(1);
605
+ }
606
+ });
325
607
  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
608
  try {
327
609
  const state = createEmptyState();
@@ -331,7 +613,7 @@ program.command("mcp").description("Start MCP server for AI coding tools").argum
331
613
  } else {
332
614
  const detectedRoot = findProjectRoot();
333
615
  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"))) {
616
+ 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
617
  projectRootToConnect = detectedRoot;
336
618
  }
337
619
  }
@@ -388,7 +670,7 @@ program.command("docs").description("Generate comprehensive codebase documentati
388
670
  const startTime = Date.now();
389
671
  try {
390
672
  const projectRoot = directory ? resolve(directory) : findProjectRoot();
391
- const outputDir = options.output ? resolve(options.output) : join(projectRoot, ".depwire");
673
+ const outputDir = options.output ? resolve(options.output) : join3(projectRoot, ".depwire");
392
674
  const includeList = options.include.split(",").map((s) => s.trim());
393
675
  const onlyList = options.only ? options.only.split(",").map((s) => s.trim()) : void 0;
394
676
  if (options.gitignore === void 0 && !existsSyncNode(outputDir)) {
@@ -459,7 +741,7 @@ async function promptGitignore() {
459
741
  });
460
742
  }
461
743
  function addToGitignore(projectRoot, pattern) {
462
- const gitignorePath = join(projectRoot, ".gitignore");
744
+ const gitignorePath = join3(projectRoot, ".gitignore");
463
745
  try {
464
746
  let content = "";
465
747
  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";