@adhisang/minecraft-modding-mcp 1.2.1 → 2.1.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +184 -64
  3. package/dist/cli.js +31 -4
  4. package/dist/compat-stdio-transport.d.ts +2 -7
  5. package/dist/compat-stdio-transport.js +12 -154
  6. package/dist/index.js +537 -202
  7. package/dist/json-rpc-framing.d.ts +22 -0
  8. package/dist/json-rpc-framing.js +168 -0
  9. package/dist/mapping-pipeline-service.d.ts +1 -1
  10. package/dist/mapping-pipeline-service.js +13 -5
  11. package/dist/mapping-service.d.ts +12 -4
  12. package/dist/mapping-service.js +222 -105
  13. package/dist/mcp-helpers.d.ts +10 -2
  14. package/dist/mcp-helpers.js +59 -5
  15. package/dist/minecraft-explorer-service.d.ts +1 -2
  16. package/dist/minecraft-explorer-service.js +120 -24
  17. package/dist/mixin-validator.d.ts +24 -2
  18. package/dist/mixin-validator.js +228 -103
  19. package/dist/mod-decompile-service.d.ts +5 -0
  20. package/dist/mod-decompile-service.js +40 -5
  21. package/dist/mod-remap-service.js +142 -30
  22. package/dist/mojang-tiny-mapping-service.js +26 -26
  23. package/dist/path-resolver.js +41 -4
  24. package/dist/registry-service.d.ts +10 -1
  25. package/dist/registry-service.js +154 -22
  26. package/dist/resources.js +7 -7
  27. package/dist/search-hit-accumulator.d.ts +0 -3
  28. package/dist/search-hit-accumulator.js +27 -6
  29. package/dist/source-jar-reader.js +16 -2
  30. package/dist/source-resolver.d.ts +1 -0
  31. package/dist/source-resolver.js +93 -2
  32. package/dist/source-service.d.ts +76 -47
  33. package/dist/source-service.js +1344 -763
  34. package/dist/stdio-supervisor.d.ts +46 -0
  35. package/dist/stdio-supervisor.js +349 -0
  36. package/dist/storage/files-repo.d.ts +3 -0
  37. package/dist/storage/files-repo.js +66 -1
  38. package/dist/storage/migrations.d.ts +1 -1
  39. package/dist/storage/migrations.js +6 -2
  40. package/dist/storage/schema.d.ts +1 -0
  41. package/dist/storage/schema.js +7 -0
  42. package/dist/symbols/symbol-extractor.js +6 -4
  43. package/dist/tool-execution-gate.d.ts +15 -0
  44. package/dist/tool-execution-gate.js +58 -0
  45. package/dist/tool-input.d.ts +6 -0
  46. package/dist/tool-input.js +64 -0
  47. package/dist/types.d.ts +1 -1
  48. package/dist/version-diff-service.js +10 -5
  49. package/dist/version-service.js +7 -2
  50. package/dist/workspace-mapping-service.js +12 -0
  51. package/package.json +4 -1
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { existsSync, readFileSync, rmSync } from "node:fs";
2
2
  import { mkdir } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { spawn } from "node:child_process";
@@ -6,6 +6,40 @@ import { createError, ERROR_CODES } from "./errors.js";
6
6
  import { log } from "./logger.js";
7
7
  const DATAGEN_TIMEOUT_MS = 5 * 60 * 1000;
8
8
  const MAX_STDIO_SNAPSHOT = 6_240;
9
+ function clampPositiveInt(value) {
10
+ if (!Number.isFinite(value) || value == null) {
11
+ return undefined;
12
+ }
13
+ return Math.max(1, Math.trunc(value));
14
+ }
15
+ function sortedRegistryEntryNames(entries) {
16
+ return Object.keys(entries).sort((left, right) => left.localeCompare(right));
17
+ }
18
+ function limitRegistryEntries(data, maxEntries) {
19
+ const entryNames = sortedRegistryEntryNames(data.entries);
20
+ const total = entryNames.length;
21
+ if (maxEntries == null || total <= maxEntries) {
22
+ return {
23
+ data,
24
+ total,
25
+ returned: total,
26
+ truncated: false
27
+ };
28
+ }
29
+ const limitedEntries = {};
30
+ for (const entryName of entryNames.slice(0, maxEntries)) {
31
+ limitedEntries[entryName] = data.entries[entryName];
32
+ }
33
+ return {
34
+ data: {
35
+ default: data.default,
36
+ entries: limitedEntries
37
+ },
38
+ total,
39
+ returned: maxEntries,
40
+ truncated: true
41
+ };
42
+ }
9
43
  function limitOutput(text) {
10
44
  if (text.length <= MAX_STDIO_SNAPSHOT)
11
45
  return text;
@@ -25,6 +59,31 @@ function findRegistryFile(registryDir) {
25
59
  }
26
60
  return undefined;
27
61
  }
62
+ function parseRegistrySnapshot(raw, version, registryFile) {
63
+ let parsed;
64
+ try {
65
+ parsed = JSON.parse(raw);
66
+ }
67
+ catch (error) {
68
+ throw createError({
69
+ code: ERROR_CODES.REGISTRY_GENERATION_FAILED,
70
+ message: `Failed to parse registries.json for version "${version}".`,
71
+ details: {
72
+ version,
73
+ registryFile,
74
+ reason: error instanceof Error ? error.message : String(error)
75
+ }
76
+ });
77
+ }
78
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
79
+ throw createError({
80
+ code: ERROR_CODES.REGISTRY_GENERATION_FAILED,
81
+ message: `registries.json for version "${version}" has invalid structure.`,
82
+ details: { version, registryFile }
83
+ });
84
+ }
85
+ return parsed;
86
+ }
28
87
  function runDataGen(serverJarPath, outputDir, version) {
29
88
  return new Promise((resolve, reject) => {
30
89
  // MC 1.18+ uses bundler format, older versions use -cp with main class directly.
@@ -131,6 +190,12 @@ export class RegistryService {
131
190
  const warnings = [];
132
191
  const allRegistries = await this.loadRegistries(version, warnings);
133
192
  const registryNames = Object.keys(allRegistries).sort();
193
+ const includeData = input.includeData ?? true;
194
+ const maxEntriesPerRegistry = clampPositiveInt(input.maxEntriesPerRegistry);
195
+ const registryEntryCounts = Object.fromEntries(registryNames.map((registryName) => [
196
+ registryName,
197
+ Object.keys(allRegistries[registryName].entries).length
198
+ ]));
134
199
  if (input.registry) {
135
200
  const registryName = normalizeRegistryName(input.registry);
136
201
  const data = allRegistries[registryName];
@@ -145,11 +210,17 @@ export class RegistryService {
145
210
  }
146
211
  });
147
212
  }
213
+ const limited = limitRegistryEntries(data, maxEntriesPerRegistry);
148
214
  return {
149
215
  version,
150
216
  registry: registryName,
151
- data,
152
- entryCount: Object.keys(data.entries).length,
217
+ ...(includeData ? { data: limited.data } : {}),
218
+ entryCount: limited.total,
219
+ returnedEntryCount: includeData ? limited.returned : 0,
220
+ registryEntryCounts: {
221
+ [registryName]: limited.total
222
+ },
223
+ ...(limited.truncated ? { dataTruncated: true } : {}),
153
224
  warnings
154
225
  };
155
226
  }
@@ -157,11 +228,25 @@ export class RegistryService {
157
228
  for (const registry of Object.values(allRegistries)) {
158
229
  totalEntries += Object.keys(registry.entries).length;
159
230
  }
231
+ let returnedEntryCount = 0;
232
+ let dataTruncated = false;
233
+ const limitedData = {};
234
+ if (includeData) {
235
+ for (const registryName of registryNames) {
236
+ const limited = limitRegistryEntries(allRegistries[registryName], maxEntriesPerRegistry);
237
+ limitedData[registryName] = limited.data;
238
+ returnedEntryCount += limited.returned;
239
+ dataTruncated ||= limited.truncated;
240
+ }
241
+ }
160
242
  return {
161
243
  version,
162
244
  registries: registryNames,
163
- data: allRegistries,
245
+ ...(includeData ? { data: limitedData } : {}),
164
246
  entryCount: totalEntries,
247
+ returnedEntryCount: includeData ? returnedEntryCount : 0,
248
+ registryEntryCounts,
249
+ ...(dataTruncated ? { dataTruncated: true } : {}),
165
250
  warnings
166
251
  };
167
252
  }
@@ -170,31 +255,79 @@ export class RegistryService {
170
255
  if (cached)
171
256
  return cached;
172
257
  const registryDir = join(this.config.cacheDir, "registries", version);
173
- // Check if we already have generated data
174
- let registryFile = findRegistryFile(registryDir);
258
+ const cachedRegistries = this.loadExistingRegistries(registryDir, version, warnings);
259
+ if (cachedRegistries) {
260
+ this.cacheRegistries(version, cachedRegistries);
261
+ return cachedRegistries;
262
+ }
263
+ await mkdir(registryDir, { recursive: true });
264
+ const serverJar = await this.versionService.resolveServerJar(version);
265
+ await runDataGen(serverJar.jarPath, registryDir, version);
266
+ const registryFile = findRegistryFile(registryDir);
175
267
  if (!registryFile) {
176
- await mkdir(registryDir, { recursive: true });
177
- const serverJar = await this.versionService.resolveServerJar(version);
178
- await runDataGen(serverJar.jarPath, registryDir, version);
179
- registryFile = findRegistryFile(registryDir);
180
- if (!registryFile) {
181
- throw createError({
182
- code: ERROR_CODES.REGISTRY_GENERATION_FAILED,
183
- message: `Registry data generation did not produce registries.json for version "${version}".`,
184
- details: { version, registryDir }
268
+ throw createError({
269
+ code: ERROR_CODES.REGISTRY_GENERATION_FAILED,
270
+ message: `Registry data generation did not produce registries.json for version "${version}".`,
271
+ details: { version, registryDir }
272
+ });
273
+ }
274
+ const parsed = this.readRegistryFileOrThrow(registryFile, version);
275
+ this.cacheRegistries(version, parsed);
276
+ return parsed;
277
+ }
278
+ loadExistingRegistries(registryDir, version, warnings) {
279
+ for (const candidate of resolveRegistryPaths(registryDir)) {
280
+ if (!existsSync(candidate)) {
281
+ continue;
282
+ }
283
+ try {
284
+ return this.readRegistryFileOrThrow(candidate, version);
285
+ }
286
+ catch (error) {
287
+ if (!this.isInvalidRegistrySnapshot(error)) {
288
+ throw error;
289
+ }
290
+ warnings.push(`Ignored corrupt cached registry snapshot and regenerated it: ${candidate}`);
291
+ log("warn", "registry.cache.invalidated", {
292
+ version,
293
+ registryFile: candidate,
294
+ reason: error.message
185
295
  });
296
+ try {
297
+ rmSync(candidate, { force: true });
298
+ }
299
+ catch {
300
+ // best-effort cleanup
301
+ }
186
302
  }
187
303
  }
188
- const raw = readFileSync(registryFile, "utf8");
189
- const parsed = JSON.parse(raw);
190
- // Validate structure
191
- if (typeof parsed !== "object" || parsed === null) {
304
+ return undefined;
305
+ }
306
+ readRegistryFileOrThrow(registryFile, version) {
307
+ let raw;
308
+ try {
309
+ raw = readFileSync(registryFile, "utf8");
310
+ }
311
+ catch (error) {
192
312
  throw createError({
193
313
  code: ERROR_CODES.REGISTRY_GENERATION_FAILED,
194
- message: `registries.json for version "${version}" has invalid structure.`,
195
- details: { version }
314
+ message: `Failed to read registries.json for version "${version}".`,
315
+ details: {
316
+ version,
317
+ registryFile,
318
+ reason: error instanceof Error ? error.message : String(error)
319
+ }
196
320
  });
197
321
  }
322
+ return parseRegistrySnapshot(raw, version, registryFile);
323
+ }
324
+ isInvalidRegistrySnapshot(error) {
325
+ return (typeof error === "object" &&
326
+ error !== null &&
327
+ "code" in error &&
328
+ error.code === ERROR_CODES.REGISTRY_GENERATION_FAILED);
329
+ }
330
+ cacheRegistries(version, parsed) {
198
331
  this.registryCache.set(version, parsed);
199
332
  // Trim cache to avoid unbounded growth
200
333
  if (this.registryCache.size > 8) {
@@ -202,7 +335,6 @@ export class RegistryService {
202
335
  if (oldest !== undefined)
203
336
  this.registryCache.delete(oldest);
204
337
  }
205
- return parsed;
206
338
  }
207
339
  }
208
340
  function normalizeRegistryName(name) {
package/dist/resources.js CHANGED
@@ -30,7 +30,7 @@ export function registerResources(server, sourceService) {
30
30
  }
31
31
  catch (e) {
32
32
  if (isAppError(e))
33
- return errorResource(uri.href, e.message);
33
+ return errorResource(uri.href, { message: e.message, code: e.code });
34
34
  throw e;
35
35
  }
36
36
  });
@@ -41,7 +41,7 @@ export function registerResources(server, sourceService) {
41
41
  }
42
42
  catch (e) {
43
43
  if (isAppError(e))
44
- return errorResource(uri.href, e.message);
44
+ return errorResource(uri.href, { message: e.message, code: e.code });
45
45
  throw e;
46
46
  }
47
47
  });
@@ -56,7 +56,7 @@ export function registerResources(server, sourceService) {
56
56
  }
57
57
  catch (e) {
58
58
  if (isAppError(e))
59
- return errorResource(uri.href, e.message);
59
+ return errorResource(uri.href, { message: e.message, code: e.code });
60
60
  throw e;
61
61
  }
62
62
  });
@@ -70,7 +70,7 @@ export function registerResources(server, sourceService) {
70
70
  }
71
71
  catch (e) {
72
72
  if (isAppError(e))
73
- return errorResource(uri.href, e.message);
73
+ return errorResource(uri.href, { message: e.message, code: e.code });
74
74
  throw e;
75
75
  }
76
76
  });
@@ -87,7 +87,7 @@ export function registerResources(server, sourceService) {
87
87
  }
88
88
  catch (e) {
89
89
  if (isAppError(e))
90
- return errorResource(uri.href, e.message);
90
+ return errorResource(uri.href, { message: e.message, code: e.code });
91
91
  throw e;
92
92
  }
93
93
  });
@@ -101,7 +101,7 @@ export function registerResources(server, sourceService) {
101
101
  }
102
102
  catch (e) {
103
103
  if (isAppError(e))
104
- return errorResource(uri.href, e.message);
104
+ return errorResource(uri.href, { message: e.message, code: e.code });
105
105
  throw e;
106
106
  }
107
107
  });
@@ -112,7 +112,7 @@ export function registerResources(server, sourceService) {
112
112
  }
113
113
  catch (e) {
114
114
  if (isAppError(e))
115
- return errorResource(uri.href, e.message);
115
+ return errorResource(uri.href, { message: e.message, code: e.code });
116
116
  throw e;
117
117
  }
118
118
  });
@@ -9,9 +9,6 @@ export type SearchSourceHit = {
9
9
  filePath: string;
10
10
  score: number;
11
11
  matchedIn: "symbol" | "path" | "content";
12
- startLine: number;
13
- endLine: number;
14
- snippet: string;
15
12
  reasonCodes: string[];
16
13
  symbol?: SearchResultSymbol;
17
14
  };
@@ -12,8 +12,8 @@ export function scoreHitOrder(left, right) {
12
12
  if (symbolCompare !== 0) {
13
13
  return symbolCompare;
14
14
  }
15
- const leftLine = left.symbol?.line ?? left.startLine;
16
- const rightLine = right.symbol?.line ?? right.startLine;
15
+ const leftLine = left.symbol?.line ?? 0;
16
+ const rightLine = right.symbol?.line ?? 0;
17
17
  return leftLine - rightLine;
18
18
  }
19
19
  export function encodeSearchCursor(hit, contextKey) {
@@ -21,7 +21,7 @@ export function encodeSearchCursor(hit, contextKey) {
21
21
  score: hit.score,
22
22
  filePath: hit.filePath,
23
23
  symbolName: hit.symbol?.symbolName ?? "",
24
- line: hit.symbol?.line ?? hit.startLine,
24
+ line: hit.symbol?.line ?? 0,
25
25
  contextKey
26
26
  }), "utf8").toString("base64");
27
27
  }
@@ -66,7 +66,7 @@ export function isAfterSearchCursor(hit, cursor) {
66
66
  if (symbolCompare < 0) {
67
67
  return false;
68
68
  }
69
- const hitLine = hit.symbol?.line ?? hit.startLine;
69
+ const hitLine = hit.symbol?.line ?? 0;
70
70
  return hitLine > cursor.line;
71
71
  }
72
72
  /**
@@ -107,6 +107,21 @@ function heapSiftUp(heap, index) {
107
107
  index = parent;
108
108
  }
109
109
  }
110
+ function heapPopWorst(heap) {
111
+ if (heap.length === 0) {
112
+ return undefined;
113
+ }
114
+ const root = heap[0];
115
+ const tail = heap.pop();
116
+ if (heap.length === 0) {
117
+ return root;
118
+ }
119
+ if (tail) {
120
+ heap[0] = tail;
121
+ heapSiftDown(heap, 0, heap.length);
122
+ }
123
+ return root;
124
+ }
110
125
  export function createSearchHitAccumulator(limit, cursor) {
111
126
  const pageLimit = Math.max(1, limit);
112
127
  const keepLimit = pageLimit + 1;
@@ -141,8 +156,14 @@ export function createSearchHitAccumulator(limit, cursor) {
141
156
  return heap.length;
142
157
  },
143
158
  finalize() {
144
- // Sort heap contents by scoreHitOrder (best first)
145
- const sorted = heap.slice().sort(scoreHitOrder);
159
+ const sorted = new Array(heap.length);
160
+ const heapCopy = heap.slice();
161
+ for (let index = heapCopy.length - 1; index >= 0; index -= 1) {
162
+ const next = heapPopWorst(heapCopy);
163
+ if (next) {
164
+ sorted[index] = next;
165
+ }
166
+ }
146
167
  const page = sorted.slice(0, pageLimit);
147
168
  const hasMore = totalAfterCursor > page.length;
148
169
  return {
@@ -9,6 +9,20 @@ function toErrorMessage(value) {
9
9
  }
10
10
  return String(value);
11
11
  }
12
+ function hasJavaSourceExtension(entryPath) {
13
+ const suffix = ".java";
14
+ if (entryPath.length < suffix.length) {
15
+ return false;
16
+ }
17
+ for (let index = 0; index < suffix.length; index += 1) {
18
+ const charCode = entryPath.charCodeAt(entryPath.length - suffix.length + index);
19
+ const normalizedCharCode = charCode >= 65 && charCode <= 90 ? charCode + 32 : charCode;
20
+ if (normalizedCharCode !== suffix.charCodeAt(index)) {
21
+ return false;
22
+ }
23
+ }
24
+ return true;
25
+ }
12
26
  function openZipFile(jarPath) {
13
27
  return new Promise((resolve, reject) => {
14
28
  yauzl.open(jarPath, {
@@ -128,7 +142,7 @@ export async function listJarEntries(jarPath) {
128
142
  }
129
143
  export async function listJavaEntries(jarPath) {
130
144
  const entries = await listJarEntries(jarPath);
131
- return entries.filter((entry) => entry.toLowerCase().endsWith(".java") && isSecureJarEntryPath(entry));
145
+ return entries.filter((entry) => hasJavaSourceExtension(entry) && isSecureJarEntryPath(entry));
132
146
  }
133
147
  export async function readJarEntryAsUtf8(jarPath, entryPath) {
134
148
  const contentBuffer = await readJarEntryAsBuffer(jarPath, entryPath);
@@ -171,7 +185,7 @@ export async function* iterateJavaEntriesAsUtf8(jarPath, maxBytes) {
171
185
  if (!entry) {
172
186
  break;
173
187
  }
174
- if (!entry.fileName.toLowerCase().endsWith(".java")) {
188
+ if (!hasJavaSourceExtension(entry.fileName)) {
175
189
  continue;
176
190
  }
177
191
  if (!isSecureJarEntryPath(entry.fileName)) {
@@ -1,6 +1,7 @@
1
1
  import type { Config, ResolvedSourceArtifact, SourceTargetInput } from "./types.js";
2
2
  export interface ResolveSourceTargetOptions {
3
3
  allowDecompile: boolean;
4
+ preferBinaryOnly?: boolean;
4
5
  preferredRepos?: string[];
5
6
  onRepoFailover?: (event: {
6
7
  stage: "source" | "binary";
@@ -1,5 +1,7 @@
1
1
  import { readdirSync } from "node:fs";
2
2
  import { basename, dirname, join, resolve as resolvePath } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import fastGlob from "fast-glob";
3
5
  import { createError, ERROR_CODES } from "./errors.js";
4
6
  import { buildRemoteBinaryUrls, buildRemoteSourceUrls, hasExistingJar, parseCoordinate, normalizedCoordinateValue } from "./maven-resolver.js";
5
7
  import { defaultDownloadPath, downloadToCache } from "./repo-downloader.js";
@@ -23,6 +25,16 @@ function resolveExactJarSourceCandidate(inputJarPath) {
23
25
  const base = jarName.endsWith(".jar") ? jarName.slice(0, -4) : jarName;
24
26
  return join(directory, `${base}-sources.jar`);
25
27
  }
28
+ function resolveSiblingBinaryJarCandidate(inputJarPath) {
29
+ const directory = dirname(inputJarPath);
30
+ const jarName = basename(inputJarPath);
31
+ if (!jarName.endsWith("-sources.jar")) {
32
+ return undefined;
33
+ }
34
+ const binaryName = `${jarName.slice(0, -"-sources.jar".length)}.jar`;
35
+ const candidate = join(directory, binaryName);
36
+ return hasExistingJar(candidate) ? candidate : undefined;
37
+ }
26
38
  function listAdjacentJarSourceCandidates(inputJarPath) {
27
39
  const directory = dirname(inputJarPath);
28
40
  const exact = resolveExactJarSourceCandidate(inputJarPath);
@@ -59,6 +71,66 @@ function resolveLocalCoordinateCandidates(localM2Path, coordinate) {
59
71
  }
60
72
  return [...existing];
61
73
  }
74
+ function resolveLocalCoordinateBinaryCandidate(localM2Path, coordinate) {
75
+ const parsed = parseCoordinate(coordinate);
76
+ const groupPath = parsed.groupId.replace(/\./g, "/");
77
+ const baseDir = resolvePath(localM2Path, groupPath, parsed.artifactId, parsed.version);
78
+ const base = `${parsed.artifactId}-${parsed.version}`;
79
+ const classifierSuffix = parsed.classifier ? `-${parsed.classifier}` : "";
80
+ const candidates = [
81
+ resolvePath(baseDir, `${base}${classifierSuffix}.jar`),
82
+ ...(classifierSuffix ? [resolvePath(baseDir, `${base}.jar`)] : [])
83
+ ];
84
+ return candidates.find((candidate) => hasExistingJar(candidate));
85
+ }
86
+ function resolveGradleUserHome() {
87
+ const configured = process.env.GRADLE_USER_HOME?.trim();
88
+ if (configured) {
89
+ return configured;
90
+ }
91
+ return resolvePath(homedir(), ".gradle");
92
+ }
93
+ function resolveGradleCacheCoordinateCandidate(coordinate) {
94
+ const parsed = parseCoordinate(coordinate);
95
+ const baseDir = resolvePath(resolveGradleUserHome(), "caches", "modules-2", "files-2.1", parsed.groupId, parsed.artifactId, parsed.version);
96
+ const base = `${parsed.artifactId}-${parsed.version}`;
97
+ const classifierSuffix = parsed.classifier ? `-${parsed.classifier}` : "";
98
+ const preferredSourceNames = [
99
+ `${base}${classifierSuffix}-sources.jar`,
100
+ ...(classifierSuffix ? [`${base}-sources.jar`] : [])
101
+ ];
102
+ const preferredBinaryNames = [
103
+ `${base}${classifierSuffix}.jar`,
104
+ ...(classifierSuffix ? [`${base}.jar`] : [])
105
+ ];
106
+ let discoveredFiles = [];
107
+ try {
108
+ discoveredFiles = fastGlob.sync("*/*", {
109
+ cwd: baseDir,
110
+ absolute: true,
111
+ onlyFiles: true
112
+ });
113
+ }
114
+ catch {
115
+ return undefined;
116
+ }
117
+ discoveredFiles = discoveredFiles.filter((entry) => hasExistingJar(entry)).sort((left, right) => left.localeCompare(right));
118
+ const pickFirst = (candidates) => {
119
+ for (const fileName of candidates) {
120
+ const match = discoveredFiles.find((entry) => basename(entry) === fileName);
121
+ if (match) {
122
+ return match;
123
+ }
124
+ }
125
+ return undefined;
126
+ };
127
+ const sourceJarPath = pickFirst(preferredSourceNames);
128
+ const binaryJarPath = pickFirst(preferredBinaryNames);
129
+ if (!sourceJarPath && !binaryJarPath) {
130
+ return undefined;
131
+ }
132
+ return { sourceJarPath, binaryJarPath };
133
+ }
62
134
  function resolveRemoteBinaryCandidate(coordinate, repos) {
63
135
  return buildRemoteBinaryUrls(repos, coordinate);
64
136
  }
@@ -80,19 +152,23 @@ export async function resolveSourceTarget(input, options, explicitConfig) {
80
152
  const exactSourceJarPath = resolveExactJarSourceCandidate(resolvedJarPath);
81
153
  const adjacentSourceCandidates = listAdjacentJarSourceCandidates(resolvedJarPath);
82
154
  const maybeAdjacentSourceCandidates = adjacentSourceCandidates.length > 0 ? adjacentSourceCandidates : undefined;
155
+ const preferBinaryOnly = options.preferBinaryOnly ?? false;
83
156
  if (await hasJavaSources(resolvedJarPath)) {
157
+ const siblingBinaryJarPath = resolveSiblingBinaryJarCandidate(resolvedJarPath);
158
+ const binaryJarPath = siblingBinaryJarPath ??
159
+ (basename(resolvedJarPath).endsWith("-sources.jar") ? undefined : resolvedJarPath);
84
160
  return {
85
161
  artifactId: artifactIdForJar("jar", resolvedJarPath, binarySignature),
86
162
  artifactSignature: binarySignature,
87
163
  origin: "local-jar",
88
- binaryJarPath: resolvedJarPath,
164
+ binaryJarPath,
89
165
  sourceJarPath: resolvedJarPath,
90
166
  adjacentSourceCandidates: maybeAdjacentSourceCandidates,
91
167
  isDecompiled: false,
92
168
  resolvedAt: resolvedAtNow()
93
169
  };
94
170
  }
95
- if (await hasJavaSources(exactSourceJarPath)) {
171
+ if (!preferBinaryOnly && await hasJavaSources(exactSourceJarPath)) {
96
172
  const sourceSignature = readStatsSignature(exactSourceJarPath);
97
173
  return {
98
174
  artifactId: artifactIdForJar("jar", exactSourceJarPath, sourceSignature),
@@ -142,12 +218,27 @@ export async function resolveSourceTarget(input, options, explicitConfig) {
142
218
  artifactSignature: signature,
143
219
  origin: "local-m2",
144
220
  sourceJarPath: candidate,
221
+ binaryJarPath: resolveLocalCoordinateBinaryCandidate(explicitConfig.localM2Path, coordinate),
145
222
  coordinate,
146
223
  isDecompiled: false,
147
224
  resolvedAt: resolvedAtNow()
148
225
  };
149
226
  }
150
227
  }
228
+ const gradleCacheCandidate = resolveGradleCacheCoordinateCandidate(coordinate);
229
+ if (gradleCacheCandidate?.sourceJarPath && (await hasJavaSources(gradleCacheCandidate.sourceJarPath))) {
230
+ const signature = readStatsSignature(gradleCacheCandidate.sourceJarPath);
231
+ return {
232
+ artifactId: artifactIdForCoordinate(coordinate, "local-m2", signature),
233
+ artifactSignature: signature,
234
+ origin: "local-m2",
235
+ sourceJarPath: gradleCacheCandidate.sourceJarPath,
236
+ binaryJarPath: gradleCacheCandidate.binaryJarPath,
237
+ coordinate,
238
+ isDecompiled: false,
239
+ resolvedAt: resolvedAtNow()
240
+ };
241
+ }
151
242
  const remoteSourceUrls = buildRemoteSourceUrls(repos, coordinate);
152
243
  for (let index = 0; index < remoteSourceUrls.length; index++) {
153
244
  const sourceUrl = remoteSourceUrls[index];