@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 +50 -26
- package/dist/chunk-PE6PHNN5.js +26 -0
- package/dist/index.js +192 -22
- package/dist/node-EEBHCXXR.js +12600 -0
- package/dist/tree-sitter-typescript-BG4W72SE.node +0 -0
- package/dist/tree-sitter-typescript-JEJYGECV.node +0 -0
- package/dist/tree-sitter-typescript-JM3MXYBJ.node +0 -0
- package/dist/tree-sitter-typescript-JPGF4LUJ.node +0 -0
- package/dist/tree-sitter-typescript-WAJPNOQ4.node +0 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,42 +1,66 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Repolens
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
**A cross-platform, multi-lingual repository intelligence CLI.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Repolens analyzes your codebase using tree-sitter ASTs to map dependency networks, identify architectural pillars, and calculate coupling metrics.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- **Dependency analysis**: Builds a graph of imports and references
|
|
10
|
-
- **Multiple outputs**: Terminal summaries, JSON export, and interactive HTML visualization
|
|
7
|
+
[](https://www.npmjs.com/package/@anirudw/repolens)
|
|
8
|
+
[](LICENSE)
|
|
11
9
|
|
|
12
|
-
##
|
|
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
|
-
##
|
|
16
|
+
## Quick Start
|
|
20
17
|
|
|
21
18
|
```bash
|
|
22
|
-
#
|
|
23
|
-
repolens
|
|
19
|
+
# Analyze a repository
|
|
20
|
+
repolens ./my-project
|
|
24
21
|
|
|
25
|
-
#
|
|
26
|
-
repolens ./
|
|
22
|
+
# Export dependency graph
|
|
23
|
+
repolens ./my-project --format json --output graph.json
|
|
27
24
|
|
|
28
|
-
#
|
|
29
|
-
repolens --
|
|
25
|
+
# Find implementations of an interface
|
|
26
|
+
repolens ./my-project --implements ILogger
|
|
30
27
|
|
|
31
|
-
#
|
|
32
|
-
repolens
|
|
33
|
-
repolens --format html --output graph.html
|
|
28
|
+
# View architectural health metrics
|
|
29
|
+
repolens ./my-project --health
|
|
34
30
|
```
|
|
35
31
|
|
|
36
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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.
|
|
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
|
|
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);
|