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/{chunk-VNUOE5VC.js → chunk-65H7HCM4.js} +391 -11
- package/dist/index.js +292 -10
- package/dist/mcpb-entry.js +1 -1
- package/dist/viz/public/temporal.css +397 -0
- package/dist/viz/public/temporal.html +118 -0
- package/dist/viz/public/temporal.js +475 -0
- package/package.json +1 -1
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-
|
|
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
|
-
|
|
213
|
-
|
|
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(
|
|
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(
|
|
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) :
|
|
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 =
|
|
744
|
+
const gitignorePath = join3(projectRoot, ".gitignore");
|
|
463
745
|
try {
|
|
464
746
|
let content = "";
|
|
465
747
|
if (existsSyncNode(gitignorePath)) {
|