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/README.md +81 -2
- package/dist/{chunk-VNUOE5VC.js → chunk-65H7HCM4.js} +391 -11
- package/dist/index.js +293 -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 +508 -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,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
|
-
|
|
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
|
+
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(
|
|
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(
|
|
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) :
|
|
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 =
|
|
745
|
+
const gitignorePath = join3(projectRoot, ".gitignore");
|
|
463
746
|
try {
|
|
464
747
|
let content = "";
|
|
465
748
|
if (existsSyncNode(gitignorePath)) {
|