@anirudw/repolens 0.1.3 → 0.2.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/README.md CHANGED
@@ -1,42 +1,66 @@
1
- # repolens
1
+ # Repolens
2
2
 
3
- A CLI tool to visualize repository dependency graphs by parsing ASTs across multiple languages.
3
+ **A cross-platform, multi-lingual repository intelligence CLI.**
4
4
 
5
- ## Features
5
+ Repolens analyzes your codebase using tree-sitter ASTs to map dependency networks, identify architectural pillars, and calculate coupling metrics.
6
6
 
7
- - **Multi-language support**: JavaScript, TypeScript, Python, Java, and Markdown
8
- - **Smart scanning**: Respects `.gitignore` rules and common build directories
9
- - **Dependency analysis**: Builds a graph of imports and references
10
- - **Multiple outputs**: Terminal summaries, JSON export, and interactive HTML visualization
7
+ [![NPM Version](https://img.shields.io/npm/v/@anirudw/repolens)](https://www.npmjs.com/package/@anirudw/repolens)
8
+ [![License](https://img.shields.io/npm/l/@anirudw/repolens)](LICENSE)
11
9
 
12
- ## Installation
10
+ ## Install
13
11
 
14
12
  ```bash
15
- npm install
16
- npm run build
13
+ npm install -g @anirudw/repolens
17
14
  ```
18
15
 
19
- ## Usage
16
+ ## Quick Start
20
17
 
21
18
  ```bash
22
- # Scan current directory
23
- repolens
19
+ # Analyze a repository
20
+ repolens ./my-project
24
21
 
25
- # Scan specific directory
26
- repolens ./path/to/repo
22
+ # Export dependency graph
23
+ repolens ./my-project --format json --output graph.json
27
24
 
28
- # With verbose output
29
- repolens --verbose
25
+ # Find implementations of an interface
26
+ repolens ./my-project --implements ILogger
30
27
 
31
- # Output formats
32
- repolens --format json --output graph.json
33
- repolens --format html --output graph.html
28
+ # View architectural health metrics
29
+ repolens ./my-project --health
34
30
  ```
35
31
 
36
- ## Supported Languages
32
+ ## Options
33
+
34
+ | Flag | Description |
35
+ |------|-------------|
36
+ | `-v, --verbose` | Enable verbose output |
37
+ | `-f, --format` | Output format: `text` (default) or `json` |
38
+ | `-o, --output <file>` | Output file path |
39
+ | `-i, --implements <name>` | Find files implementing an interface |
40
+ | `--health` | Display architectural health metrics |
41
+
42
+ ## Features
43
+
44
+ - **Multi-lingual AST parsing** — JavaScript, TypeScript, Python, Java, Markdown
45
+ - **Path resolution** — Automatically resolves local imports
46
+ - **PageRank centrality** — Identifies the most relied-upon files
47
+ - **Entry-point detection** — Flags critical entry points
48
+ - **Interface registry** — Track class/interface relationships
49
+ - **Health metrics** — Coupling (Ca, Ce) and instability (I)
50
+
51
+ ## Health Metrics
52
+
53
+ The `--health` flag calculates coupling metrics:
54
+
55
+ - **Ca (Afferent Coupling)** — Files that depend on this file
56
+ - **Ce (Efferent Coupling)** — Files this file depends on
57
+ - **Instability** — `I = Ce / (Ca + Ce)`
58
+
59
+ | Value | Meaning |
60
+ |-------|---------|
61
+ | ~0 | Stable core dependency |
62
+ | ~1 | Highly unstable (fragile) |
63
+
64
+ ## License
37
65
 
38
- - JavaScript/JSX (.js, .jsx)
39
- - TypeScript/TSX (.ts, .tsx)
40
- - Python (.py)
41
- - Java (.java)
42
- - Markdown (.md)
66
+ MIT
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
4
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
5
+ }) : x)(function(x) {
6
+ if (typeof require !== "undefined") return require.apply(this, arguments);
7
+ throw Error('Dynamic require of "' + x + '" is not supported');
8
+ });
9
+ var __glob = (map) => (path) => {
10
+ var fn = map[path];
11
+ if (fn) return fn();
12
+ throw new Error("Module not found in bundle: " + path);
13
+ };
14
+ var __esm = (fn, res) => function __init() {
15
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
16
+ };
17
+ var __commonJS = (cb, mod) => function __require2() {
18
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
19
+ };
20
+
21
+ export {
22
+ __require,
23
+ __glob,
24
+ __esm,
25
+ __commonJS
26
+ };
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import "./chunk-PE6PHNN5.js";
2
3
 
3
4
  // src/cli/commands.ts
4
5
  import { Command } from "commander";
@@ -237,34 +238,52 @@ var MarkdownParser = class {
237
238
  // src/parser/strategies/javascript.ts
238
239
  var TreeSitterParser;
239
240
  var JavaScript;
241
+ var TypeScript;
240
242
  async function ensureLoaded() {
241
243
  if (!TreeSitterParser) {
242
- const [ts, js] = await Promise.all([
244
+ const [ts, js, tst] = await Promise.all([
243
245
  import("tree-sitter"),
244
- import("tree-sitter-javascript")
246
+ import("tree-sitter-javascript"),
247
+ import("./node-EEBHCXXR.js")
245
248
  ]);
246
249
  TreeSitterParser = ts.default;
247
250
  JavaScript = js.default;
251
+ TypeScript = tst.default.typescript;
248
252
  }
249
253
  }
250
254
  var JavaScriptParser = class {
251
- parser = null;
252
- async ensureParser() {
253
- if (!this.parser) {
254
- await ensureLoaded();
255
- this.parser = new TreeSitterParser();
256
- this.parser.setLanguage(JavaScript);
255
+ jsParser = null;
256
+ tsParser = null;
257
+ async ensureParser(filePath) {
258
+ await ensureLoaded();
259
+ const isTsFile = filePath.endsWith(".ts") || filePath.endsWith(".tsx");
260
+ if (isTsFile) {
261
+ if (!this.tsParser) {
262
+ this.tsParser = new TreeSitterParser();
263
+ this.tsParser.setLanguage(TypeScript);
264
+ }
265
+ return this.tsParser;
266
+ } else {
267
+ if (!this.jsParser) {
268
+ this.jsParser = new TreeSitterParser();
269
+ this.jsParser.setLanguage(JavaScript);
270
+ }
271
+ return this.jsParser;
257
272
  }
258
- return this.parser;
259
273
  }
260
274
  async parse(filePath, content) {
261
- const parser = await this.ensureParser();
275
+ const parser = await this.ensureParser(filePath);
262
276
  const tree = parser.parse(content);
263
277
  const rootNode = tree.rootNode;
264
278
  const dependencies = [];
265
279
  const imports = /* @__PURE__ */ new Set();
266
280
  const sourceModules = [];
281
+ const definedClasses = [];
282
+ const definedInterfaces = [];
283
+ const implementsInterfaces = [];
267
284
  this.extractImports(rootNode, dependencies, imports, sourceModules);
285
+ this.extractClassData(rootNode, definedClasses, implementsInterfaces);
286
+ this.extractInterfaceData(rootNode, definedInterfaces);
268
287
  const heuristics = {};
269
288
  const allImports = [...imports, ...sourceModules].map((s) => s.toLowerCase());
270
289
  heuristics.isReact = allImports.some((i) => i === "react" || i === "@types/react");
@@ -279,7 +298,10 @@ var JavaScriptParser = class {
279
298
  dependencies,
280
299
  metadata: {
281
300
  sizeBytes: Buffer.byteLength(content, "utf-8"),
282
- heuristics
301
+ heuristics,
302
+ definedClasses,
303
+ definedInterfaces,
304
+ implementsInterfaces
283
305
  }
284
306
  };
285
307
  }
@@ -293,6 +315,45 @@ var JavaScriptParser = class {
293
315
  this.extractImports(child, dependencies, imports, sourceModules);
294
316
  }
295
317
  }
318
+ extractClassData(node, definedClasses, implementsInterfaces) {
319
+ if (node.type === "class_declaration") {
320
+ const nameNode = node.childForFieldName("name") || this.getChildByType(node, "type_identifier");
321
+ if (nameNode) {
322
+ definedClasses.push(nameNode.text);
323
+ }
324
+ const classHeritage = node.childForFieldName("heritage") || this.getChildByType(node, "class_heritage");
325
+ if (classHeritage) {
326
+ const implementsClause = classHeritage.childForFieldName("interfaces") || this.getChildByType(classHeritage, "implements_clause");
327
+ if (implementsClause) {
328
+ for (const child of implementsClause.children) {
329
+ if (child.type === "type_identifier" || child.type === "identifier") {
330
+ implementsInterfaces.push(child.text);
331
+ }
332
+ }
333
+ }
334
+ }
335
+ }
336
+ for (const child of node.children) {
337
+ this.extractClassData(child, definedClasses, implementsInterfaces);
338
+ }
339
+ }
340
+ extractInterfaceData(node, definedInterfaces) {
341
+ if (node.type === "interface_declaration") {
342
+ const nameNode = node.childForFieldName("name") || this.getChildByType(node, "type_identifier");
343
+ if (nameNode) {
344
+ definedInterfaces.push(nameNode.text);
345
+ }
346
+ }
347
+ for (const child of node.children) {
348
+ this.extractInterfaceData(child, definedInterfaces);
349
+ }
350
+ }
351
+ getChildByType(node, type) {
352
+ for (const child of node.children) {
353
+ if (child.type === type) return child;
354
+ }
355
+ return null;
356
+ }
296
357
  extractImportStatement(node, dependencies, imports, sourceModules) {
297
358
  const sourceNode = node.childForFieldName("source");
298
359
  if (sourceNode && sourceNode.type === "string") {
@@ -569,7 +630,12 @@ var JavaParser = class {
569
630
  const rootNode = tree.rootNode;
570
631
  const dependencies = [];
571
632
  const heuristics = {};
633
+ const definedClasses = [];
634
+ const definedInterfaces = [];
635
+ const implementsInterfaces = [];
572
636
  this.extractImports(rootNode, dependencies);
637
+ this.extractClassData(rootNode, definedClasses, implementsInterfaces);
638
+ this.extractInterfaceData(rootNode, definedInterfaces);
573
639
  heuristics.hasMainMethod = this.detectMainMethod(rootNode);
574
640
  return {
575
641
  id: filePath,
@@ -578,7 +644,10 @@ var JavaParser = class {
578
644
  dependencies,
579
645
  metadata: {
580
646
  sizeBytes: Buffer.byteLength(content, "utf-8"),
581
- heuristics
647
+ heuristics,
648
+ definedClasses,
649
+ definedInterfaces,
650
+ implementsInterfaces
582
651
  }
583
652
  };
584
653
  }
@@ -601,6 +670,41 @@ var JavaParser = class {
601
670
  this.extractImports(child, dependencies);
602
671
  }
603
672
  }
673
+ extractClassData(node, definedClasses, implementsInterfaces) {
674
+ if (node.type === "class_declaration") {
675
+ const nameNode = this.getChildByType(node, "identifier");
676
+ if (nameNode) {
677
+ definedClasses.push(nameNode.text);
678
+ }
679
+ const interfaces = node.childForFieldName("interfaces");
680
+ if (interfaces) {
681
+ const typeList = this.getChildByType(interfaces, "type_list");
682
+ if (typeList) {
683
+ for (const child of typeList.children) {
684
+ if (child.type === "type_identifier" || child.type === "identifier") {
685
+ implementsInterfaces.push(child.text);
686
+ } else if (child.type === "scoped_identifier") {
687
+ implementsInterfaces.push(this.getScopedIdentifierText(child));
688
+ }
689
+ }
690
+ }
691
+ }
692
+ }
693
+ for (const child of node.children) {
694
+ this.extractClassData(child, definedClasses, implementsInterfaces);
695
+ }
696
+ }
697
+ extractInterfaceData(node, definedInterfaces) {
698
+ if (node.type === "interface_declaration") {
699
+ const nameNode = this.getChildByType(node, "identifier");
700
+ if (nameNode) {
701
+ definedInterfaces.push(nameNode.text);
702
+ }
703
+ }
704
+ for (const child of node.children) {
705
+ this.extractInterfaceData(child, definedInterfaces);
706
+ }
707
+ }
604
708
  detectMainMethod(node) {
605
709
  if (node.type === "method_declaration") {
606
710
  const nameNode = this.getChildByType(node, "identifier");
@@ -688,19 +792,45 @@ import { resolve as resolve2, dirname } from "path";
688
792
  var Graph = class {
689
793
  nodes = /* @__PURE__ */ new Map();
690
794
  edges = [];
795
+ implementationRegistry = /* @__PURE__ */ new Map();
691
796
  constructor(files) {
692
797
  for (const file of files) {
693
- this.nodes.set(file.id, {
694
- id: file.id,
695
- relativePath: file.relativePath,
696
- language: file.language,
697
- metadata: file.metadata,
698
- inboundEdges: 0,
699
- outboundEdges: 0
700
- });
798
+ this.addNode(file);
701
799
  }
702
800
  this.resolveEdges(files);
703
801
  }
802
+ addNode(file) {
803
+ this.nodes.set(file.id, {
804
+ id: file.id,
805
+ relativePath: file.relativePath,
806
+ language: file.language,
807
+ metadata: file.metadata,
808
+ inboundEdges: 0,
809
+ outboundEdges: 0,
810
+ ca: 0,
811
+ ce: 0,
812
+ instability: 0
813
+ });
814
+ if (file.metadata.implementsInterfaces) {
815
+ for (const interfaceName of file.metadata.implementsInterfaces) {
816
+ if (!this.implementationRegistry.has(interfaceName)) {
817
+ this.implementationRegistry.set(interfaceName, []);
818
+ }
819
+ this.implementationRegistry.get(interfaceName).push(file.id);
820
+ }
821
+ }
822
+ }
823
+ calculateHealthMetrics() {
824
+ for (const node of this.nodes.values()) {
825
+ node.ca = node.inboundEdges;
826
+ node.ce = node.outboundEdges;
827
+ const denominator = node.ca + node.ce;
828
+ node.instability = denominator === 0 ? 0 : node.ce / denominator;
829
+ }
830
+ }
831
+ getImplementationRegistry() {
832
+ return Object.fromEntries(this.implementationRegistry);
833
+ }
704
834
  resolveEdges(files) {
705
835
  for (const file of files) {
706
836
  const dir = dirname(file.id);
@@ -831,7 +961,8 @@ async function exportGraphToJson(graph, parsedFiles, outputPath) {
831
961
  target: edge.target,
832
962
  type: edge.type
833
963
  }));
834
- const output = { nodes, edges };
964
+ const implementations = graph.getImplementationRegistry();
965
+ const output = { nodes, edges, implementations };
835
966
  await writeFile(outputPath, JSON.stringify(output, null, 2));
836
967
  }
837
968
 
@@ -856,7 +987,7 @@ function createCommand() {
856
987
  "-f, --format <format>",
857
988
  "Output format (text, json)",
858
989
  "text"
859
- ).option("-o, --output <file>", "Output file path").action(async (path, options) => {
990
+ ).option("-o, --output <file>", "Output file path").option("-i, --implements <interfaceName>", "Find all files implementing a specific interface or base class").option("--health", "Display architectural health metrics and identify unstable files").action(async (path, options) => {
860
991
  const scanResult = scanDirectory({ rootDir: path, verbose: options.verbose });
861
992
  if (options.verbose) {
862
993
  console.log(pc.dim("\nParsing files..."));
@@ -878,6 +1009,45 @@ function createCommand() {
878
1009
  }
879
1010
  const graph = new Graph(parsedFiles);
880
1011
  const rankedNodes = analyzeGraph(graph);
1012
+ graph.calculateHealthMetrics();
1013
+ if (options.health) {
1014
+ const nodes = Array.from(graph.getNodes().values());
1015
+ const topCoreDeps = nodes.sort((a, b) => b.ca - a.ca).slice(0, 5).filter((n) => n.ca > 0);
1016
+ const topUnstable = nodes.sort((a, b) => b.instability - a.instability).slice(0, 5).filter((n) => n.instability > 0);
1017
+ console.log(pc.bold("\n=== Architectural Health Metrics ===\n"));
1018
+ if (topCoreDeps.length > 0) {
1019
+ console.log(pc.bold("Top 5 Core Dependencies (Highest Ca - will break most things if changed):"));
1020
+ for (const node of topCoreDeps) {
1021
+ console.log(` ${pc.red(node.relativePath)}: ${pc.bold(node.ca.toString())} dependents`);
1022
+ }
1023
+ console.log();
1024
+ }
1025
+ if (topUnstable.length > 0) {
1026
+ console.log(pc.bold("Top 5 Most Unstable Files (Highest I = Ce/(Ca+Ce)):"));
1027
+ for (const node of topUnstable) {
1028
+ console.log(` ${pc.yellow(node.relativePath)}: ${pc.bold(node.instability.toFixed(3))} instability`);
1029
+ }
1030
+ console.log();
1031
+ }
1032
+ process.exit(0);
1033
+ }
1034
+ if (options.implements) {
1035
+ const registry = graph.getImplementationRegistry();
1036
+ const implementations = registry[options.implements] || [];
1037
+ console.log(pc.bold(`
1038
+ Searching for implementations of: ${pc.cyan(options.implements)}`));
1039
+ if (implementations.length === 0) {
1040
+ console.log(pc.yellow(`
1041
+ No implementations found for ${options.implements} in this repository.`));
1042
+ } else {
1043
+ console.log(pc.green(`
1044
+ Found ${implementations.length} implementation(s):`));
1045
+ for (const file of implementations) {
1046
+ console.log(` ${pc.dim(file)}`);
1047
+ }
1048
+ }
1049
+ process.exit(0);
1050
+ }
881
1051
  if (options.format === "json") {
882
1052
  const outputPath = options.output ?? resolve3(process.cwd(), "repolens-graph.json");
883
1053
  await exportGraphToJson(graph, parsedFiles, outputPath);