@anirudw/repolens 0.1.6 → 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,28 +1,66 @@
1
1
  # Repolens
2
2
 
3
- ![NPM Version](https://img.shields.io/npm/v/@anirudw/repolens?color=blue&style=for-the-badge)
4
- ![NPM Downloads](https://img.shields.io/npm/dt/@anirudw/repolens?style=for-the-badge)
5
- ![License](https://img.shields.io/npm/l/@anirudw/repolens?style=for-the-badge)
6
-
7
3
  **A cross-platform, multi-lingual repository intelligence CLI.**
8
4
 
9
- Repolens analyzes your codebase using native C++ Abstract Syntax Trees (ASTs) to map out dependency networks, resolve local imports, and run PageRank algorithms to identify the architectural pillars of your project.
5
+ Repolens analyzes your codebase using tree-sitter ASTs to map dependency networks, identify architectural pillars, and calculate coupling metrics.
6
+
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)
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install -g @anirudw/repolens
14
+ ```
15
+
16
+ ## Quick Start
17
+
18
+ ```bash
19
+ # Analyze a repository
20
+ repolens ./my-project
21
+
22
+ # Export dependency graph
23
+ repolens ./my-project --format json --output graph.json
24
+
25
+ # Find implementations of an interface
26
+ repolens ./my-project --implements ILogger
27
+
28
+ # View architectural health metrics
29
+ repolens ./my-project --health
30
+ ```
31
+
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 |
10
41
 
11
42
  ## Features
12
43
 
13
- * **Multi-Lingual AST Parsing:** Uses `tree-sitter` to deeply understand JavaScript, TypeScript, Python, Java, and Markdown.
14
- * **Intelligent Path Resolution:** Automatically resolves complex, extensionless local imports into absolute file paths.
15
- * **PageRank Centrality:** Calculates inbound and outbound connection graphs to identify the most heavily relied-upon "Hub" files in your repository.
16
- * **Entry-Point Detection:** Uses language-specific heuristics (e.g., `__name__ == "__main__"`, `public static void main`, React imports) to flag critical entry points.
17
- * **JSON Export:** Dump your entire repository graph to disk for external visualization or CI/CD integrations.
18
- * **Safe Scanning:** Automatically respects `.gitignore` and safely skips `node_modules` and `.git` directories.
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)
19
50
 
20
- ---
51
+ ## Health Metrics
21
52
 
22
- ## Installation
53
+ The `--health` flag calculates coupling metrics:
23
54
 
24
- You can run Repolens on-demand using `npx`, or install it globally to use it as a daily driver.
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)`
25
58
 
26
- **Global Installation (Recommended):**
27
- ```bash
28
- npm install -g @anirudw/repolens
59
+ | Value | Meaning |
60
+ |-------|---------|
61
+ | ~0 | Stable core dependency |
62
+ | ~1 | Highly unstable (fragile) |
63
+
64
+ ## License
65
+
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);