maven-indexer-mcp 1.0.0 → 1.0.2
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 +3 -1
- package/build/config.js +15 -0
- package/build/db/index.js +9 -4
- package/build/index.js +105 -136
- package/build/indexer.js +56 -28
- package/build/source_parser.js +41 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,6 +33,7 @@ If the auto-detection fails, or if you want to filter which packages are indexed
|
|
|
33
33
|
|
|
34
34
|
* **`MAVEN_REPO`**: Absolute path to your local Maven repository (e.g., `/Users/yourname/.m2/repository`). Use this if your repository is in a non-standard location.
|
|
35
35
|
* **`INCLUDED_PACKAGES`**: Comma-separated list of package patterns to index (e.g., `com.mycompany.*,org.example.*`). Default is `*` (index everything).
|
|
36
|
+
* **`MAVEN_INDEXER_CFR_PATH`**: (Optional) Absolute path to a specific CFR decompiler JAR. If not provided, the server will attempt to use its bundled CFR version.
|
|
36
37
|
|
|
37
38
|
Example with optional configuration:
|
|
38
39
|
|
|
@@ -44,7 +45,8 @@ Example with optional configuration:
|
|
|
44
45
|
"args": ["-y", "maven-indexer-mcp@latest"],
|
|
45
46
|
"env": {
|
|
46
47
|
"MAVEN_REPO": "/Users/yourname/.m2/repository",
|
|
47
|
-
"INCLUDED_PACKAGES": "com.mycompany.*"
|
|
48
|
+
"INCLUDED_PACKAGES": "com.mycompany.*",
|
|
49
|
+
"MAVEN_INDEXER_CFR_PATH": "/path/to/cfr-0.152.jar"
|
|
48
50
|
}
|
|
49
51
|
}
|
|
50
52
|
}
|
package/build/config.js
CHANGED
|
@@ -7,6 +7,7 @@ export class Config {
|
|
|
7
7
|
localRepository = "";
|
|
8
8
|
javaBinary = "java";
|
|
9
9
|
includedPackages = ["*"];
|
|
10
|
+
cfrPath = null;
|
|
10
11
|
constructor() { }
|
|
11
12
|
static async getInstance() {
|
|
12
13
|
if (!Config.instance) {
|
|
@@ -62,10 +63,24 @@ export class Config {
|
|
|
62
63
|
.map(p => p.trim())
|
|
63
64
|
.filter(p => p.length > 0);
|
|
64
65
|
}
|
|
66
|
+
// Load CFR Path
|
|
67
|
+
if (process.env.MAVEN_INDEXER_CFR_PATH) {
|
|
68
|
+
this.cfrPath = process.env.MAVEN_INDEXER_CFR_PATH;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// Fallback to default bundled lib
|
|
72
|
+
this.cfrPath = path.resolve(process.cwd(), 'lib/cfr-0.152.jar');
|
|
73
|
+
}
|
|
65
74
|
// Log to stderr so it doesn't interfere with MCP protocol on stdout
|
|
66
75
|
console.error(`Using local repository: ${this.localRepository}`);
|
|
67
76
|
console.error(`Using Java binary: ${this.javaBinary}`);
|
|
68
77
|
console.error(`Included packages: ${JSON.stringify(this.includedPackages)}`);
|
|
78
|
+
if (this.cfrPath) {
|
|
79
|
+
console.error(`Using CFR jar: ${this.cfrPath}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
getCfrJarPath() {
|
|
83
|
+
return this.cfrPath;
|
|
69
84
|
}
|
|
70
85
|
getJavapPath() {
|
|
71
86
|
if (this.javaBinary === 'java')
|
package/build/db/index.js
CHANGED
|
@@ -25,6 +25,7 @@ export class DB {
|
|
|
25
25
|
version TEXT NOT NULL,
|
|
26
26
|
abspath TEXT NOT NULL,
|
|
27
27
|
has_source INTEGER DEFAULT 0,
|
|
28
|
+
is_indexed INTEGER DEFAULT 0,
|
|
28
29
|
UNIQUE(group_id, artifact_id, version)
|
|
29
30
|
);
|
|
30
31
|
|
|
@@ -35,11 +36,15 @@ export class DB {
|
|
|
35
36
|
tokenize="trigram"
|
|
36
37
|
);
|
|
37
38
|
|
|
38
|
-
--
|
|
39
|
-
|
|
40
|
-
artifact_id INTEGER PRIMARY KEY
|
|
41
|
-
);
|
|
39
|
+
-- Cleanup old table if exists
|
|
40
|
+
DROP TABLE IF EXISTS indexed_artifacts;
|
|
42
41
|
`);
|
|
42
|
+
try {
|
|
43
|
+
this.db.exec('ALTER TABLE artifacts ADD COLUMN is_indexed INTEGER DEFAULT 0');
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
// Column likely already exists
|
|
47
|
+
}
|
|
43
48
|
}
|
|
44
49
|
getDb() {
|
|
45
50
|
return this.db;
|
package/build/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
4
|
import path from 'path';
|
|
5
|
+
import { z } from "zod";
|
|
6
6
|
import { Indexer } from "./indexer.js";
|
|
7
7
|
import { SourceParser } from "./source_parser.js";
|
|
8
|
-
const server = new
|
|
8
|
+
const server = new McpServer({
|
|
9
9
|
name: "maven-indexer",
|
|
10
10
|
version: "1.0.0",
|
|
11
11
|
}, {
|
|
@@ -20,155 +20,124 @@ indexer.index().then(() => {
|
|
|
20
20
|
// Start watching for changes after initial index
|
|
21
21
|
return indexer.startWatch();
|
|
22
22
|
}).catch(err => console.error("Initial indexing failed:", err));
|
|
23
|
-
server.
|
|
23
|
+
server.registerTool("search_artifacts", {
|
|
24
|
+
description: "Search for artifacts in the local Maven repository",
|
|
25
|
+
inputSchema: z.object({
|
|
26
|
+
query: z.string().describe("Search query (groupId, artifactId, or keyword)"),
|
|
27
|
+
}),
|
|
28
|
+
}, async ({ query }) => {
|
|
29
|
+
const matches = indexer.search(query);
|
|
30
|
+
// Limit results to avoid overflow
|
|
31
|
+
const limitedMatches = matches.slice(0, 20);
|
|
32
|
+
const text = limitedMatches.length > 0
|
|
33
|
+
? limitedMatches.map(a => `[ID: ${a.id}] ${a.groupId}:${a.artifactId}:${a.version} (Has Source: ${a.hasSource})`).join("\n")
|
|
34
|
+
: "No artifacts found matching the query.";
|
|
24
35
|
return {
|
|
25
|
-
|
|
36
|
+
content: [
|
|
26
37
|
{
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
inputSchema: {
|
|
30
|
-
type: "object",
|
|
31
|
-
properties: {
|
|
32
|
-
query: {
|
|
33
|
-
type: "string",
|
|
34
|
-
description: "Search query (groupId, artifactId, or keyword)",
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
required: ["query"],
|
|
38
|
-
},
|
|
38
|
+
type: "text",
|
|
39
|
+
text: `Found ${matches.length} matches${matches.length > 20 ? ' (showing first 20)' : ''}:\n${text}`,
|
|
39
40
|
},
|
|
40
|
-
{
|
|
41
|
-
name: "search_classes",
|
|
42
|
-
description: "Search for Java classes. Can be used to find classes by name or to find classes for a specific purpose (by searching keywords in class names).",
|
|
43
|
-
inputSchema: {
|
|
44
|
-
type: "object",
|
|
45
|
-
properties: {
|
|
46
|
-
className: {
|
|
47
|
-
type: "string",
|
|
48
|
-
description: "Fully qualified class name, partial name, or keywords describing the class purpose (e.g. 'JsonToXml').",
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
required: ["className"],
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
name: "get_class_details",
|
|
56
|
-
description: "Get details about a specific class from an artifact, including method signatures and javadocs (if source is available).",
|
|
57
|
-
inputSchema: {
|
|
58
|
-
type: "object",
|
|
59
|
-
properties: {
|
|
60
|
-
className: {
|
|
61
|
-
type: "string",
|
|
62
|
-
description: "Fully qualified class name",
|
|
63
|
-
},
|
|
64
|
-
artifactId: {
|
|
65
|
-
type: "number",
|
|
66
|
-
description: "The internal ID of the artifact (returned by search_classes)",
|
|
67
|
-
},
|
|
68
|
-
type: {
|
|
69
|
-
type: "string",
|
|
70
|
-
enum: ["signatures", "docs", "source"],
|
|
71
|
-
description: "Type of detail to retrieve: 'signatures' (methods), 'docs' (javadocs + methods), 'source' (full source code).",
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
required: ["className", "artifactId", "type"],
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
name: "refresh_index",
|
|
79
|
-
description: "Trigger a re-scan of the Maven repository",
|
|
80
|
-
inputSchema: {
|
|
81
|
-
type: "object",
|
|
82
|
-
properties: {},
|
|
83
|
-
},
|
|
84
|
-
}
|
|
85
41
|
],
|
|
86
42
|
};
|
|
87
43
|
});
|
|
88
|
-
server.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
44
|
+
server.registerTool("search_classes", {
|
|
45
|
+
description: "Search for Java classes. Can be used to find classes by name or to find classes for a specific purpose (by searching keywords in class names).",
|
|
46
|
+
inputSchema: z.object({
|
|
47
|
+
className: z.string().describe("Fully qualified class name, partial name, or keywords describing the class purpose (e.g. 'JsonToXml')."),
|
|
48
|
+
}),
|
|
49
|
+
}, async ({ className }) => {
|
|
50
|
+
const matches = indexer.searchClass(className);
|
|
51
|
+
const text = matches.length > 0
|
|
52
|
+
? matches.map(m => {
|
|
53
|
+
// Group by artifact ID to allow easy selection
|
|
54
|
+
const artifacts = m.artifacts.slice(0, 5).map(a => `[ID: ${a.id}] ${a.groupId}:${a.artifactId}:${a.version}${a.hasSource ? ' (Has Source)' : ''}`).join("\n ");
|
|
55
|
+
const more = m.artifacts.length > 5 ? `\n ... (${m.artifacts.length - 5} more versions)` : '';
|
|
56
|
+
return `Class: ${m.className}\n ${artifacts}${more}`;
|
|
57
|
+
}).join("\n\n")
|
|
58
|
+
: "No classes found matching the query. Try different keywords.";
|
|
59
|
+
return {
|
|
60
|
+
content: [{ type: "text", text }]
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
server.registerTool("get_class_details", {
|
|
64
|
+
description: "Get details about a specific class from an artifact, including method signatures and javadocs (if source is available).",
|
|
65
|
+
inputSchema: z.object({
|
|
66
|
+
className: z.string().describe("Fully qualified class name"),
|
|
67
|
+
artifactId: z.number().describe("The internal ID of the artifact (returned by search_classes)"),
|
|
68
|
+
type: z.enum(["signatures", "docs", "source"]).describe("Type of detail to retrieve: 'signatures' (methods), 'docs' (javadocs + methods), 'source' (full source code)."),
|
|
69
|
+
}),
|
|
70
|
+
}, async ({ className, artifactId, type }) => {
|
|
71
|
+
const artifact = indexer.getArtifactById(artifactId);
|
|
72
|
+
if (!artifact) {
|
|
73
|
+
return { content: [{ type: "text", text: "Artifact not found." }] };
|
|
105
74
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
75
|
+
let detail = null;
|
|
76
|
+
let usedDecompilation = false;
|
|
77
|
+
// 1. If requesting source/docs, try Source JAR first
|
|
78
|
+
if (type === 'source' || type === 'docs') {
|
|
79
|
+
if (artifact.hasSource) {
|
|
80
|
+
const sourceJarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}-sources.jar`);
|
|
81
|
+
try {
|
|
82
|
+
detail = await SourceParser.getClassDetail(sourceJarPath, className, type);
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
// Ignore error and fallthrough to main jar (decompilation)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// If not found in source jar (or no source jar), try main jar (decompilation)
|
|
89
|
+
if (!detail) {
|
|
90
|
+
const mainJarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}.jar`);
|
|
91
|
+
try {
|
|
92
|
+
// SourceParser will try to decompile if source file not found in jar
|
|
93
|
+
detail = await SourceParser.getClassDetail(mainJarPath, className, type);
|
|
94
|
+
if (detail && detail.source) {
|
|
95
|
+
usedDecompilation = true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
// Ignore
|
|
100
|
+
}
|
|
101
|
+
}
|
|
120
102
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
103
|
+
else {
|
|
104
|
+
// Signatures -> Use Main JAR
|
|
105
|
+
const mainJarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}.jar`);
|
|
106
|
+
detail = await SourceParser.getClassDetail(mainJarPath, className, type);
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
if (!detail) {
|
|
110
|
+
return { content: [{ type: "text", text: `Class ${className} not found in artifact ${artifact.artifactId}.` }] };
|
|
128
111
|
}
|
|
129
|
-
let
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
jarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}.jar`);
|
|
112
|
+
let resultText = `Class: ${detail.className}\n\n`;
|
|
113
|
+
if (usedDecompilation) {
|
|
114
|
+
resultText += "*Source code decompiled from binary class file.*\n\n";
|
|
133
115
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (!artifact.hasSource) {
|
|
137
|
-
return { content: [{ type: "text", text: `Artifact ${artifact.groupId}:${artifact.artifactId}:${artifact.version} does not have a sources jar available locally.` }] };
|
|
138
|
-
}
|
|
139
|
-
jarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}-sources.jar`);
|
|
116
|
+
if (type === 'source') {
|
|
117
|
+
resultText += "```java\n" + detail.source + "\n```";
|
|
140
118
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
return { content: [{ type: "text", text: `Class ${className} not found in ${type === 'signatures' ? 'artifact' : 'sources'} of ${artifact.artifactId}.` }] };
|
|
119
|
+
else {
|
|
120
|
+
if (detail.doc) {
|
|
121
|
+
resultText += "Documentation:\n" + detail.doc + "\n\n";
|
|
145
122
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
resultText += "```java\n" + detail.source + "\n```";
|
|
123
|
+
if (detail.signatures) {
|
|
124
|
+
resultText += "Methods:\n" + detail.signatures.join("\n") + "\n";
|
|
149
125
|
}
|
|
150
|
-
else {
|
|
151
|
-
if (detail.doc) {
|
|
152
|
-
resultText += "Documentation:\n" + detail.doc + "\n\n";
|
|
153
|
-
}
|
|
154
|
-
if (detail.signatures) {
|
|
155
|
-
resultText += "Methods:\n" + detail.signatures.join("\n") + "\n";
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
return { content: [{ type: "text", text: resultText }] };
|
|
159
|
-
}
|
|
160
|
-
catch (e) {
|
|
161
|
-
return { content: [{ type: "text", text: `Error reading source: ${e.message}` }] };
|
|
162
126
|
}
|
|
127
|
+
return { content: [{ type: "text", text: resultText }] };
|
|
163
128
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
indexer.index().catch(console.error);
|
|
167
|
-
return {
|
|
168
|
-
content: [{ type: "text", text: "Index refresh started." }]
|
|
169
|
-
};
|
|
129
|
+
catch (e) {
|
|
130
|
+
return { content: [{ type: "text", text: `Error reading source: ${e.message}` }] };
|
|
170
131
|
}
|
|
171
|
-
|
|
132
|
+
});
|
|
133
|
+
server.registerTool("refresh_index", {
|
|
134
|
+
description: "Trigger a re-scan of the Maven repository",
|
|
135
|
+
}, async () => {
|
|
136
|
+
// Re-run index
|
|
137
|
+
indexer.index().catch(console.error);
|
|
138
|
+
return {
|
|
139
|
+
content: [{ type: "text", text: "Index refresh started." }]
|
|
140
|
+
};
|
|
172
141
|
});
|
|
173
142
|
const transport = new StdioServerTransport();
|
|
174
143
|
await server.connect(transport);
|
package/build/indexer.js
CHANGED
|
@@ -5,6 +5,10 @@ import yauzl from 'yauzl';
|
|
|
5
5
|
import chokidar from 'chokidar';
|
|
6
6
|
import { Config } from './config.js';
|
|
7
7
|
import { DB } from './db/index.js';
|
|
8
|
+
/**
|
|
9
|
+
* Singleton class responsible for indexing Maven artifacts.
|
|
10
|
+
* It scans the local repository, watches for changes, and indexes Java classes.
|
|
11
|
+
*/
|
|
8
12
|
export class Indexer {
|
|
9
13
|
static instance;
|
|
10
14
|
isIndexing = false;
|
|
@@ -17,6 +21,10 @@ export class Indexer {
|
|
|
17
21
|
}
|
|
18
22
|
return Indexer.instance;
|
|
19
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Starts watching the local repository for changes.
|
|
26
|
+
* Debounces changes to trigger re-indexing.
|
|
27
|
+
*/
|
|
20
28
|
async startWatch() {
|
|
21
29
|
const config = await Config.getInstance();
|
|
22
30
|
const repoPath = config.localRepository;
|
|
@@ -52,6 +60,12 @@ export class Indexer {
|
|
|
52
60
|
onChange();
|
|
53
61
|
});
|
|
54
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Main indexing process.
|
|
65
|
+
* 1. Scans the file system for Maven artifacts.
|
|
66
|
+
* 2. Synchronizes the database with found artifacts.
|
|
67
|
+
* 3. Indexes classes for artifacts that haven't been indexed yet.
|
|
68
|
+
*/
|
|
55
69
|
async index() {
|
|
56
70
|
if (this.isIndexing)
|
|
57
71
|
return;
|
|
@@ -71,40 +85,28 @@ export class Indexer {
|
|
|
71
85
|
const artifacts = await this.scanRepository(repoPath);
|
|
72
86
|
console.error(`Found ${artifacts.length} artifacts on disk.`);
|
|
73
87
|
// 2. Persist artifacts and determine what needs indexing
|
|
74
|
-
|
|
88
|
+
// We use is_indexed = 0 for new artifacts.
|
|
89
|
+
const insertArtifact = db.prepare(`
|
|
90
|
+
INSERT OR IGNORE INTO artifacts (group_id, artifact_id, version, abspath, has_source, is_indexed)
|
|
91
|
+
VALUES (@groupId, @artifactId, @version, @abspath, @hasSource, 0)
|
|
92
|
+
`);
|
|
93
|
+
// Use a transaction only for the batch insert of artifacts
|
|
75
94
|
db.transaction(() => {
|
|
76
|
-
const insertArtifact = db.prepare(`
|
|
77
|
-
INSERT OR IGNORE INTO artifacts (group_id, artifact_id, version, abspath, has_source)
|
|
78
|
-
VALUES (@groupId, @artifactId, @version, @abspath, @hasSource)
|
|
79
|
-
`);
|
|
80
|
-
const selectId = db.prepare(`
|
|
81
|
-
SELECT id FROM artifacts
|
|
82
|
-
WHERE group_id = @groupId AND artifact_id = @artifactId AND version = @version
|
|
83
|
-
`);
|
|
84
|
-
const checkIndexed = db.prepare(`
|
|
85
|
-
SELECT 1 FROM indexed_artifacts WHERE artifact_id = ?
|
|
86
|
-
`);
|
|
87
95
|
for (const art of artifacts) {
|
|
88
96
|
insertArtifact.run({
|
|
89
97
|
...art,
|
|
90
98
|
hasSource: art.hasSource ? 1 : 0
|
|
91
99
|
});
|
|
92
|
-
const row = selectId.get({
|
|
93
|
-
groupId: art.groupId,
|
|
94
|
-
artifactId: art.artifactId,
|
|
95
|
-
version: art.version
|
|
96
|
-
});
|
|
97
|
-
if (row) {
|
|
98
|
-
art.id = row.id;
|
|
99
|
-
const isIndexed = checkIndexed.get(art.id);
|
|
100
|
-
if (!isIndexed) {
|
|
101
|
-
artifactsToIndex.push(art);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
100
|
}
|
|
105
101
|
});
|
|
102
|
+
// 3. Find artifacts that need indexing (is_indexed = 0)
|
|
103
|
+
const artifactsToIndex = db.prepare(`
|
|
104
|
+
SELECT id, group_id as groupId, artifact_id as artifactId, version, abspath, has_source as hasSource
|
|
105
|
+
FROM artifacts
|
|
106
|
+
WHERE is_indexed = 0
|
|
107
|
+
`).all();
|
|
106
108
|
console.error(`${artifactsToIndex.length} artifacts need indexing.`);
|
|
107
|
-
//
|
|
109
|
+
// 4. Scan JARs for classes and update DB
|
|
108
110
|
const CHUNK_SIZE = 50;
|
|
109
111
|
let processedCount = 0;
|
|
110
112
|
for (let i = 0; i < artifactsToIndex.length; i += CHUNK_SIZE) {
|
|
@@ -124,6 +126,9 @@ export class Indexer {
|
|
|
124
126
|
this.isIndexing = false;
|
|
125
127
|
}
|
|
126
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Recursively scans a directory for Maven artifacts (POM files).
|
|
131
|
+
*/
|
|
127
132
|
async scanRepository(rootDir) {
|
|
128
133
|
const results = [];
|
|
129
134
|
const scanDir = async (dir) => {
|
|
@@ -168,6 +173,10 @@ export class Indexer {
|
|
|
168
173
|
await scanDir(rootDir);
|
|
169
174
|
return results;
|
|
170
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Extracts classes from the artifact's JAR and indexes them.
|
|
178
|
+
* Updates the 'is_indexed' flag upon completion.
|
|
179
|
+
*/
|
|
171
180
|
async indexArtifactClasses(artifact) {
|
|
172
181
|
const jarPath = path.join(artifact.abspath, `${artifact.artifactId}-${artifact.version}.jar`);
|
|
173
182
|
const db = DB.getInstance();
|
|
@@ -178,12 +187,13 @@ export class Indexer {
|
|
|
178
187
|
catch {
|
|
179
188
|
// If jar missing, mark as indexed so we don't retry endlessly?
|
|
180
189
|
// Or maybe it's a pom-only artifact.
|
|
181
|
-
db.prepare('
|
|
190
|
+
db.prepare('UPDATE artifacts SET is_indexed = 1 WHERE id = ?').run(artifact.id);
|
|
182
191
|
return;
|
|
183
192
|
}
|
|
184
193
|
return new Promise((resolve) => {
|
|
185
194
|
yauzl.open(jarPath, { lazyEntries: true, autoClose: true }, (err, zipfile) => {
|
|
186
195
|
if (err || !zipfile) {
|
|
196
|
+
// If we can't open it, maybe it's corrupt. Skip for now.
|
|
187
197
|
resolve();
|
|
188
198
|
return;
|
|
189
199
|
}
|
|
@@ -215,7 +225,8 @@ export class Indexer {
|
|
|
215
225
|
const simpleName = cls.split('.').pop() || cls;
|
|
216
226
|
insertClass.run(artifact.id, cls, simpleName);
|
|
217
227
|
}
|
|
218
|
-
|
|
228
|
+
// Mark as indexed
|
|
229
|
+
db.prepare('UPDATE artifacts SET is_indexed = 1 WHERE id = ?').run(artifact.id);
|
|
219
230
|
});
|
|
220
231
|
}
|
|
221
232
|
catch (e) {
|
|
@@ -229,6 +240,9 @@ export class Indexer {
|
|
|
229
240
|
});
|
|
230
241
|
});
|
|
231
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* Checks if a class package is included in the configuration patterns.
|
|
245
|
+
*/
|
|
232
246
|
isPackageIncluded(className, patterns) {
|
|
233
247
|
if (patterns.length === 0 || (patterns.length === 1 && patterns[0] === '*'))
|
|
234
248
|
return true;
|
|
@@ -249,6 +263,9 @@ export class Indexer {
|
|
|
249
263
|
}
|
|
250
264
|
return false;
|
|
251
265
|
}
|
|
266
|
+
/**
|
|
267
|
+
* Searches for artifacts by group ID or artifact ID.
|
|
268
|
+
*/
|
|
252
269
|
search(query) {
|
|
253
270
|
// Artifact coordinates search
|
|
254
271
|
const db = DB.getInstance();
|
|
@@ -260,12 +277,20 @@ export class Indexer {
|
|
|
260
277
|
`).all(`%${query}%`, `%${query}%`);
|
|
261
278
|
return rows;
|
|
262
279
|
}
|
|
280
|
+
/**
|
|
281
|
+
* Searches for classes matching the pattern.
|
|
282
|
+
* Uses Full-Text Search (FTS) for efficient matching.
|
|
283
|
+
*/
|
|
263
284
|
searchClass(classNamePattern) {
|
|
264
285
|
const db = DB.getInstance();
|
|
265
286
|
// Use FTS for smart matching
|
|
266
287
|
// If pattern has no spaces, we assume it's a prefix or exact match query
|
|
267
288
|
// "String" -> match "String" in simple_name or class_name
|
|
268
|
-
const
|
|
289
|
+
const escapedPattern = classNamePattern.replace(/"/g, '""');
|
|
290
|
+
const safeQuery = classNamePattern.replace(/[^a-zA-Z0-9]/g, ' ').trim();
|
|
291
|
+
const query = safeQuery.length > 0
|
|
292
|
+
? `"${escapedPattern}"* OR ${safeQuery}`
|
|
293
|
+
: `"${escapedPattern}"*`;
|
|
269
294
|
try {
|
|
270
295
|
const rows = db.prepare(`
|
|
271
296
|
SELECT c.class_name, c.simple_name, a.id, a.group_id, a.artifact_id, a.version, a.abspath, a.has_source
|
|
@@ -301,6 +326,9 @@ export class Indexer {
|
|
|
301
326
|
return [];
|
|
302
327
|
}
|
|
303
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* Retrieves an artifact by its database ID.
|
|
331
|
+
*/
|
|
304
332
|
getArtifactById(id) {
|
|
305
333
|
const db = DB.getInstance();
|
|
306
334
|
const row = db.prepare(`
|
package/build/source_parser.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
1
3
|
import yauzl from 'yauzl';
|
|
2
4
|
import { exec } from 'child_process';
|
|
3
5
|
import { promisify } from 'util';
|
|
@@ -10,8 +12,12 @@ export class SourceParser {
|
|
|
10
12
|
}
|
|
11
13
|
// className: com.example.MyClass
|
|
12
14
|
// internalPath: com/example/MyClass.java
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
+
const basePath = className.replace(/\./g, '/');
|
|
16
|
+
const candidates = [
|
|
17
|
+
basePath + '.java',
|
|
18
|
+
basePath + '.kt'
|
|
19
|
+
];
|
|
20
|
+
const result = await new Promise((resolve, reject) => {
|
|
15
21
|
yauzl.open(jarPath, { lazyEntries: true, autoClose: true }, (err, zipfile) => {
|
|
16
22
|
if (err || !zipfile) {
|
|
17
23
|
resolve(null);
|
|
@@ -20,7 +26,7 @@ export class SourceParser {
|
|
|
20
26
|
let found = false;
|
|
21
27
|
zipfile.readEntry();
|
|
22
28
|
zipfile.on('entry', (entry) => {
|
|
23
|
-
if (entry.fileName
|
|
29
|
+
if (candidates.includes(entry.fileName)) {
|
|
24
30
|
found = true;
|
|
25
31
|
zipfile.openReadStream(entry, (err, readStream) => {
|
|
26
32
|
if (err || !readStream) {
|
|
@@ -45,6 +51,38 @@ export class SourceParser {
|
|
|
45
51
|
});
|
|
46
52
|
});
|
|
47
53
|
});
|
|
54
|
+
if (result) {
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
// Fallback to decompilation if source not found in this JAR
|
|
58
|
+
// Note: This works best if the provided jarPath is the MAIN jar.
|
|
59
|
+
// If it is the sources jar, decompilation will fail (as it doesn't contain .class files),
|
|
60
|
+
// returning null.
|
|
61
|
+
if (type === 'source' || type === 'docs') {
|
|
62
|
+
return this.decompileClass(jarPath, className);
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
static async decompileClass(jarPath, className) {
|
|
67
|
+
try {
|
|
68
|
+
const config = await Config.getInstance();
|
|
69
|
+
const cfrPath = config.getCfrJarPath();
|
|
70
|
+
if (!cfrPath || !fs.existsSync(cfrPath)) {
|
|
71
|
+
// If we can't find CFR, we can't decompile.
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
// Use java -cp cfr.jar:target.jar org.benf.cfr.reader.Main className
|
|
75
|
+
const classpath = `${cfrPath}${path.delimiter}${jarPath}`;
|
|
76
|
+
const { stdout } = await execAsync(`java -cp "${classpath}" org.benf.cfr.reader.Main "${className}"`);
|
|
77
|
+
return {
|
|
78
|
+
className,
|
|
79
|
+
source: stdout // Return as source
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
// console.error(`CFR failed for ${className} in ${jarPath}:`, e);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
48
86
|
}
|
|
49
87
|
static async getSignaturesWithJavap(jarPath, className) {
|
|
50
88
|
try {
|