@adhisang/minecraft-modding-mcp 2.1.0 → 3.0.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/CHANGELOG.md +18 -0
- package/README.md +33 -4
- package/dist/cache-registry.d.ts +95 -0
- package/dist/cache-registry.js +541 -0
- package/dist/entry-tools/analyze-mod-service.d.ts +207 -0
- package/dist/entry-tools/analyze-mod-service.js +253 -0
- package/dist/entry-tools/analyze-symbol-service.d.ts +209 -0
- package/dist/entry-tools/analyze-symbol-service.js +304 -0
- package/dist/entry-tools/compare-minecraft-service.d.ts +210 -0
- package/dist/entry-tools/compare-minecraft-service.js +397 -0
- package/dist/entry-tools/entry-tool-schema.d.ts +6 -0
- package/dist/entry-tools/entry-tool-schema.js +10 -0
- package/dist/entry-tools/inspect-minecraft-service.d.ts +1953 -0
- package/dist/entry-tools/inspect-minecraft-service.js +876 -0
- package/dist/entry-tools/manage-cache-service.d.ts +130 -0
- package/dist/entry-tools/manage-cache-service.js +229 -0
- package/dist/entry-tools/request-normalizers.d.ts +10 -0
- package/dist/entry-tools/request-normalizers.js +36 -0
- package/dist/entry-tools/response-contract.d.ts +44 -0
- package/dist/entry-tools/response-contract.js +96 -0
- package/dist/entry-tools/validate-project-service.d.ts +543 -0
- package/dist/entry-tools/validate-project-service.js +381 -0
- package/dist/index.js +103 -9
- package/package.json +1 -1
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { readdir, rm, stat } from "node:fs/promises";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { createError, ERROR_CODES } from "./errors.js";
|
|
5
|
+
import { normalizeOptionalPathForHost } from "./path-converter.js";
|
|
6
|
+
import Database from "./storage/sqlite.js";
|
|
7
|
+
export const PUBLIC_CACHE_KINDS = [
|
|
8
|
+
"artifact-index",
|
|
9
|
+
"downloads",
|
|
10
|
+
"mapping",
|
|
11
|
+
"registry",
|
|
12
|
+
"decompiled-source",
|
|
13
|
+
"mod-remap"
|
|
14
|
+
];
|
|
15
|
+
export const CACHE_HEALTH_STATES = [
|
|
16
|
+
"healthy",
|
|
17
|
+
"partial",
|
|
18
|
+
"stale",
|
|
19
|
+
"orphaned",
|
|
20
|
+
"corrupt",
|
|
21
|
+
"in_use"
|
|
22
|
+
];
|
|
23
|
+
const STALE_ENTRY_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
24
|
+
const CURSOR_VERSION = 1;
|
|
25
|
+
const STATUS_PRIORITY = ["in_use", "corrupt", "orphaned", "stale", "partial", "healthy"];
|
|
26
|
+
function kindRoot(config, cacheKind) {
|
|
27
|
+
switch (cacheKind) {
|
|
28
|
+
case "artifact-index":
|
|
29
|
+
return resolve(config.sqlitePath);
|
|
30
|
+
case "downloads":
|
|
31
|
+
return join(config.cacheDir, "downloads");
|
|
32
|
+
case "mapping":
|
|
33
|
+
return join(config.cacheDir, "mappings");
|
|
34
|
+
case "registry":
|
|
35
|
+
return join(config.cacheDir, "registries");
|
|
36
|
+
case "decompiled-source":
|
|
37
|
+
return join(config.cacheDir, "decompiled");
|
|
38
|
+
case "mod-remap":
|
|
39
|
+
return join(config.cacheDir, "remapped-mods");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function listFilesRecursive(root) {
|
|
43
|
+
if (!existsSync(root)) {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
const output = [];
|
|
47
|
+
const pending = [root];
|
|
48
|
+
while (pending.length > 0) {
|
|
49
|
+
const current = pending.pop();
|
|
50
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
const fullPath = join(current, entry.name);
|
|
53
|
+
if (entry.isDirectory()) {
|
|
54
|
+
pending.push(fullPath);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (entry.isFile()) {
|
|
58
|
+
output.push(fullPath);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return output.sort((left, right) => left.localeCompare(right));
|
|
63
|
+
}
|
|
64
|
+
function normalizePathKey(pathValue, runtimeInfo) {
|
|
65
|
+
const normalized = normalizeOptionalPathForHost(pathValue, runtimeInfo, "jarPath");
|
|
66
|
+
if (!normalized) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
return normalized.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
70
|
+
}
|
|
71
|
+
function parseStringArray(value) {
|
|
72
|
+
if (!value) {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const parsed = JSON.parse(value);
|
|
77
|
+
return Array.isArray(parsed) ? parsed.filter((entry) => typeof entry === "string") : [];
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function inferVersion(...candidates) {
|
|
84
|
+
for (const candidate of candidates) {
|
|
85
|
+
if (!candidate) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const match = candidate.match(/\b(\d+\.\d+(?:\.\d+)?(?:-[A-Za-z0-9.]+)?)\b/);
|
|
89
|
+
if (match?.[1]) {
|
|
90
|
+
return match[1];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
function inferMapping(...candidates) {
|
|
96
|
+
for (const candidate of candidates) {
|
|
97
|
+
const normalized = candidate?.toLowerCase();
|
|
98
|
+
if (!normalized) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (normalized.includes("intermediary")) {
|
|
102
|
+
return "intermediary";
|
|
103
|
+
}
|
|
104
|
+
if (normalized.includes("mojang")) {
|
|
105
|
+
return "mojang";
|
|
106
|
+
}
|
|
107
|
+
if (normalized.includes("yarn")) {
|
|
108
|
+
return "yarn";
|
|
109
|
+
}
|
|
110
|
+
if (normalized.includes("obfuscated")) {
|
|
111
|
+
return "obfuscated";
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
function inferScope(...candidates) {
|
|
117
|
+
for (const candidate of candidates) {
|
|
118
|
+
const normalized = candidate?.toLowerCase();
|
|
119
|
+
if (!normalized) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (normalized.includes("loader")) {
|
|
123
|
+
return "loader";
|
|
124
|
+
}
|
|
125
|
+
if (normalized.includes("merged")) {
|
|
126
|
+
return "merged";
|
|
127
|
+
}
|
|
128
|
+
if (normalized.includes("vanilla")) {
|
|
129
|
+
return "vanilla";
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
function inferProjectPath(pathValue, runtimeInfo) {
|
|
135
|
+
const normalized = normalizePathKey(pathValue, runtimeInfo);
|
|
136
|
+
if (!normalized) {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
for (const marker of ["/.gradle/", "/build/", "/src/"]) {
|
|
140
|
+
const index = normalized.indexOf(marker);
|
|
141
|
+
if (index > 0) {
|
|
142
|
+
return normalized.slice(0, index);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
function isCorruptRegistryJson(filePath) {
|
|
148
|
+
if (!filePath.endsWith(".json")) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
JSON.parse(readFileSync(filePath, "utf8"));
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function parseOlderThan(value) {
|
|
160
|
+
if (!value) {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
const trimmed = value.trim();
|
|
164
|
+
if (!trimmed) {
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
const match = trimmed.match(/^P(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/);
|
|
168
|
+
if (!match) {
|
|
169
|
+
throw createError({
|
|
170
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
171
|
+
message: `olderThan must be an ISO-8601 duration like "P30D" or "PT12H".`
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
const weeks = Number(match[1] ?? 0);
|
|
175
|
+
const days = Number(match[2] ?? 0);
|
|
176
|
+
const hours = Number(match[3] ?? 0);
|
|
177
|
+
const minutes = Number(match[4] ?? 0);
|
|
178
|
+
const seconds = Number(match[5] ?? 0);
|
|
179
|
+
const totalMs = (((weeks * 7 + days) * 24 + hours) * 60 * 60 + minutes * 60 + seconds) * 1000;
|
|
180
|
+
if (totalMs <= 0) {
|
|
181
|
+
throw createError({
|
|
182
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
183
|
+
message: "olderThan must be greater than zero."
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return totalMs;
|
|
187
|
+
}
|
|
188
|
+
function prepareSelector(selector, runtimeInfo) {
|
|
189
|
+
if (!selector) {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
...selector,
|
|
194
|
+
olderThanMs: parseOlderThan(selector.olderThan),
|
|
195
|
+
normalizedJarPath: normalizePathKey(selector.jarPath, runtimeInfo),
|
|
196
|
+
normalizedProjectPath: normalizePathKey(selector.projectPath, runtimeInfo)
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function openDb(sqlitePath) {
|
|
200
|
+
if (!existsSync(sqlitePath)) {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
return new Database(sqlitePath);
|
|
204
|
+
}
|
|
205
|
+
function candidatePathsForEntry(entry) {
|
|
206
|
+
const paths = new Set();
|
|
207
|
+
const maybeMeta = entry.meta ?? {};
|
|
208
|
+
for (const candidate of [
|
|
209
|
+
entry.path,
|
|
210
|
+
typeof maybeMeta.jarPath === "string" ? maybeMeta.jarPath : undefined,
|
|
211
|
+
typeof maybeMeta.binaryJarPath === "string" ? maybeMeta.binaryJarPath : undefined,
|
|
212
|
+
typeof maybeMeta.sourceJarPath === "string" ? maybeMeta.sourceJarPath : undefined,
|
|
213
|
+
typeof maybeMeta.projectPath === "string" ? maybeMeta.projectPath : undefined
|
|
214
|
+
]) {
|
|
215
|
+
if (candidate) {
|
|
216
|
+
paths.add(candidate);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return [...paths];
|
|
220
|
+
}
|
|
221
|
+
function entryUpdatedAt(entry) {
|
|
222
|
+
return typeof entry.meta?.updatedAt === "string" ? entry.meta.updatedAt : undefined;
|
|
223
|
+
}
|
|
224
|
+
function deriveEntryStatus(entry, config, now) {
|
|
225
|
+
const maybeMeta = entry.meta ?? {};
|
|
226
|
+
if (maybeMeta.inUse === true) {
|
|
227
|
+
return "in_use";
|
|
228
|
+
}
|
|
229
|
+
if (maybeMeta.corrupt === true) {
|
|
230
|
+
return "corrupt";
|
|
231
|
+
}
|
|
232
|
+
const candidatePaths = candidatePathsForEntry(entry);
|
|
233
|
+
const existingPaths = candidatePaths.filter((candidate) => existsSync(candidate));
|
|
234
|
+
if (entry.cacheKind === "artifact-index" && !existsSync(config.sqlitePath)) {
|
|
235
|
+
return "orphaned";
|
|
236
|
+
}
|
|
237
|
+
if (candidatePaths.length > 0 && existingPaths.length === 0) {
|
|
238
|
+
return "orphaned";
|
|
239
|
+
}
|
|
240
|
+
if (candidatePaths.length > 1 && existingPaths.length > 0 && existingPaths.length < candidatePaths.length) {
|
|
241
|
+
return "partial";
|
|
242
|
+
}
|
|
243
|
+
if (maybeMeta.partial === true) {
|
|
244
|
+
return "partial";
|
|
245
|
+
}
|
|
246
|
+
const updatedAt = entryUpdatedAt(entry);
|
|
247
|
+
if (updatedAt) {
|
|
248
|
+
const updatedAtMs = Date.parse(updatedAt);
|
|
249
|
+
if (Number.isFinite(updatedAtMs) && now - updatedAtMs >= STALE_ENTRY_AGE_MS) {
|
|
250
|
+
return "stale";
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return "healthy";
|
|
254
|
+
}
|
|
255
|
+
function sortEntries(entries) {
|
|
256
|
+
return [...entries].sort((left, right) => {
|
|
257
|
+
if (left.cacheKind !== right.cacheKind) {
|
|
258
|
+
return left.cacheKind.localeCompare(right.cacheKind);
|
|
259
|
+
}
|
|
260
|
+
return left.entryId.localeCompare(right.entryId);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
function entrySortKey(entry) {
|
|
264
|
+
return `${entry.cacheKind}\u0000${entry.entryId}`;
|
|
265
|
+
}
|
|
266
|
+
function encodeCursor(entry) {
|
|
267
|
+
return Buffer.from(JSON.stringify({ version: CURSOR_VERSION, key: entrySortKey(entry) }), "utf8").toString("base64");
|
|
268
|
+
}
|
|
269
|
+
function decodeCursor(cursor) {
|
|
270
|
+
if (!cursor) {
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
const decoded = JSON.parse(Buffer.from(cursor, "base64").toString("utf8"));
|
|
275
|
+
if (decoded.version !== CURSOR_VERSION || typeof decoded.key !== "string" || !decoded.key) {
|
|
276
|
+
throw new Error("invalid");
|
|
277
|
+
}
|
|
278
|
+
return decoded.key;
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
throw createError({
|
|
282
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
283
|
+
message: "Invalid pagination cursor."
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function paginateEntries(entries, limit, cursor) {
|
|
288
|
+
const cursorKey = decodeCursor(cursor);
|
|
289
|
+
const pageSource = cursorKey
|
|
290
|
+
? entries.filter((entry) => entrySortKey(entry) > cursorKey)
|
|
291
|
+
: entries;
|
|
292
|
+
const pageEntries = pageSource.slice(0, limit);
|
|
293
|
+
return {
|
|
294
|
+
entries: pageEntries,
|
|
295
|
+
nextCursor: pageSource.length > limit && pageEntries.length > 0 ? encodeCursor(pageEntries[pageEntries.length - 1]) : undefined
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function rollupStatus(entries, rootExists) {
|
|
299
|
+
if (!rootExists || entries.length === 0) {
|
|
300
|
+
return "partial";
|
|
301
|
+
}
|
|
302
|
+
for (const status of STATUS_PRIORITY) {
|
|
303
|
+
if (entries.some((entry) => entry.status === status)) {
|
|
304
|
+
return status;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return "healthy";
|
|
308
|
+
}
|
|
309
|
+
function matchesSelector(entry, selector, runtimeInfo) {
|
|
310
|
+
if (!selector) {
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
const maybeMeta = entry.meta ?? {};
|
|
314
|
+
if (selector.entryId && selector.entryId !== entry.entryId) {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
if (selector.artifactId && maybeMeta.artifactId !== selector.artifactId) {
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
if (selector.status && selector.status !== entry.status) {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
if (selector.version) {
|
|
324
|
+
const version = typeof maybeMeta.version === "string" ? maybeMeta.version : undefined;
|
|
325
|
+
if (version !== selector.version && !entry.path.includes(selector.version)) {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (selector.mapping) {
|
|
330
|
+
const mappings = new Set([maybeMeta.mapping, maybeMeta.requestedMapping, maybeMeta.mappingApplied]
|
|
331
|
+
.filter((value) => typeof value === "string"));
|
|
332
|
+
if (!mappings.has(selector.mapping)) {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (selector.scope) {
|
|
337
|
+
const scope = typeof maybeMeta.scope === "string" ? maybeMeta.scope : undefined;
|
|
338
|
+
if (scope !== selector.scope) {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (selector.olderThanMs != null) {
|
|
343
|
+
const updatedAt = entryUpdatedAt(entry);
|
|
344
|
+
const updatedAtMs = updatedAt ? Date.parse(updatedAt) : Number.NaN;
|
|
345
|
+
if (!Number.isFinite(updatedAtMs) || Date.now() - updatedAtMs < selector.olderThanMs) {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const normalizedPaths = candidatePathsForEntry(entry)
|
|
350
|
+
.map((candidate) => normalizePathKey(candidate, runtimeInfo))
|
|
351
|
+
.filter((candidate) => Boolean(candidate));
|
|
352
|
+
if (selector.normalizedJarPath && !normalizedPaths.includes(selector.normalizedJarPath)) {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
if (selector.normalizedProjectPath) {
|
|
356
|
+
const projectMatch = normalizedPaths.some((candidate) => candidate === selector.normalizedProjectPath || candidate.startsWith(`${selector.normalizedProjectPath}/`));
|
|
357
|
+
if (!projectMatch) {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
async function artifactIndexEntries(config) {
|
|
364
|
+
const db = openDb(config.sqlitePath);
|
|
365
|
+
if (!db) {
|
|
366
|
+
return [];
|
|
367
|
+
}
|
|
368
|
+
try {
|
|
369
|
+
const rows = db.prepare(`
|
|
370
|
+
SELECT
|
|
371
|
+
artifacts.artifact_id,
|
|
372
|
+
artifacts.updated_at,
|
|
373
|
+
COALESCE(artifact_content_bytes.total_content_bytes, 0) AS total_content_bytes,
|
|
374
|
+
artifacts.version,
|
|
375
|
+
artifacts.binary_jar_path,
|
|
376
|
+
artifacts.source_jar_path,
|
|
377
|
+
artifacts.requested_mapping,
|
|
378
|
+
artifacts.mapping_applied,
|
|
379
|
+
artifacts.quality_flags_json
|
|
380
|
+
FROM artifacts
|
|
381
|
+
LEFT JOIN artifact_content_bytes
|
|
382
|
+
ON artifact_content_bytes.artifact_id = artifacts.artifact_id
|
|
383
|
+
ORDER BY artifacts.updated_at DESC
|
|
384
|
+
`).all();
|
|
385
|
+
const dbInUse = existsSync(`${config.sqlitePath}-wal`) || existsSync(`${config.sqlitePath}-journal`);
|
|
386
|
+
return rows.map((row) => {
|
|
387
|
+
const qualityFlags = parseStringArray(row.quality_flags_json);
|
|
388
|
+
const binaryJarPath = row.binary_jar_path ?? undefined;
|
|
389
|
+
const sourceJarPath = row.source_jar_path ?? undefined;
|
|
390
|
+
return {
|
|
391
|
+
cacheKind: "artifact-index",
|
|
392
|
+
entryId: row.artifact_id,
|
|
393
|
+
path: binaryJarPath ?? sourceJarPath ?? config.sqlitePath,
|
|
394
|
+
sizeBytes: Math.max(0, row.total_content_bytes),
|
|
395
|
+
status: "healthy",
|
|
396
|
+
meta: {
|
|
397
|
+
artifactId: row.artifact_id,
|
|
398
|
+
updatedAt: row.updated_at,
|
|
399
|
+
version: row.version ?? inferVersion(binaryJarPath, sourceJarPath),
|
|
400
|
+
requestedMapping: row.requested_mapping ?? undefined,
|
|
401
|
+
mappingApplied: row.mapping_applied ?? undefined,
|
|
402
|
+
mapping: row.mapping_applied ?? row.requested_mapping ?? inferMapping(binaryJarPath, sourceJarPath, ...qualityFlags),
|
|
403
|
+
binaryJarPath,
|
|
404
|
+
sourceJarPath,
|
|
405
|
+
projectPath: inferProjectPath(binaryJarPath ?? sourceJarPath, config.pathRuntimeInfo),
|
|
406
|
+
scope: inferScope(binaryJarPath, sourceJarPath, ...qualityFlags) ?? "vanilla",
|
|
407
|
+
partial: qualityFlags.some((flag) => flag.includes("partial")),
|
|
408
|
+
inUse: dbInUse
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
finally {
|
|
414
|
+
db.close();
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
async function fileBackedEntries(config, cacheKind) {
|
|
418
|
+
const root = kindRoot(config, cacheKind);
|
|
419
|
+
const files = await listFilesRecursive(root);
|
|
420
|
+
const entries = [];
|
|
421
|
+
for (const filePath of files) {
|
|
422
|
+
const fileStat = await stat(filePath);
|
|
423
|
+
const normalizedEntryId = filePath.slice(root.length + 1);
|
|
424
|
+
const inferredScope = inferScope(filePath, normalizedEntryId) ?? (cacheKind === "decompiled-source" ? "vanilla" : undefined);
|
|
425
|
+
entries.push({
|
|
426
|
+
cacheKind,
|
|
427
|
+
entryId: normalizedEntryId,
|
|
428
|
+
path: filePath,
|
|
429
|
+
sizeBytes: fileStat.size,
|
|
430
|
+
status: "healthy",
|
|
431
|
+
meta: {
|
|
432
|
+
updatedAt: fileStat.mtime.toISOString(),
|
|
433
|
+
version: inferVersion(filePath, normalizedEntryId),
|
|
434
|
+
mapping: inferMapping(filePath, normalizedEntryId),
|
|
435
|
+
scope: inferredScope,
|
|
436
|
+
projectPath: inferProjectPath(filePath, config.pathRuntimeInfo),
|
|
437
|
+
partial: fileStat.size === 0,
|
|
438
|
+
corrupt: cacheKind === "registry" ? isCorruptRegistryJson(filePath) : false,
|
|
439
|
+
inUse: filePath.endsWith(".lock") ||
|
|
440
|
+
filePath.endsWith(".wal") ||
|
|
441
|
+
filePath.endsWith(".journal"),
|
|
442
|
+
...(cacheKind === "downloads" || cacheKind === "mod-remap" ? { jarPath: filePath } : {})
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
return entries;
|
|
447
|
+
}
|
|
448
|
+
export function createCacheRegistry(config) {
|
|
449
|
+
async function collectEntries(cacheKinds, selector) {
|
|
450
|
+
const selectedKinds = cacheKinds?.length ? cacheKinds : [...PUBLIC_CACHE_KINDS];
|
|
451
|
+
const preparedSelector = prepareSelector(selector, config.pathRuntimeInfo);
|
|
452
|
+
const now = Date.now();
|
|
453
|
+
const entries = await Promise.all(selectedKinds.map((cacheKind) => cacheKind === "artifact-index"
|
|
454
|
+
? artifactIndexEntries(config)
|
|
455
|
+
: fileBackedEntries(config, cacheKind)));
|
|
456
|
+
const enriched = entries
|
|
457
|
+
.flat()
|
|
458
|
+
.map((entry) => ({
|
|
459
|
+
...entry,
|
|
460
|
+
status: deriveEntryStatus(entry, config, now)
|
|
461
|
+
}));
|
|
462
|
+
return sortEntries(enriched.filter((entry) => matchesSelector(entry, preparedSelector, config.pathRuntimeInfo)));
|
|
463
|
+
}
|
|
464
|
+
return {
|
|
465
|
+
async summarize(input) {
|
|
466
|
+
const selectedKinds = input.cacheKinds?.length ? input.cacheKinds : [...PUBLIC_CACHE_KINDS];
|
|
467
|
+
const entries = await collectEntries(selectedKinds, input.selector);
|
|
468
|
+
const kinds = {};
|
|
469
|
+
for (const cacheKind of selectedKinds) {
|
|
470
|
+
const root = kindRoot(config, cacheKind);
|
|
471
|
+
const rows = entries.filter((entry) => entry.cacheKind === cacheKind);
|
|
472
|
+
kinds[cacheKind] = {
|
|
473
|
+
cacheKind,
|
|
474
|
+
entryCount: rows.length,
|
|
475
|
+
totalBytes: rows.reduce((total, entry) => total + entry.sizeBytes, 0),
|
|
476
|
+
status: rollupStatus(rows, existsSync(root))
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
return { kinds };
|
|
480
|
+
},
|
|
481
|
+
async listEntries(input) {
|
|
482
|
+
const entries = await collectEntries(input.cacheKinds, input.selector);
|
|
483
|
+
const limit = Math.max(1, input.limit ?? 50);
|
|
484
|
+
return paginateEntries(entries, limit, input.cursor);
|
|
485
|
+
},
|
|
486
|
+
async inspectEntries(input) {
|
|
487
|
+
const entries = await collectEntries(input.cacheKinds, input.selector);
|
|
488
|
+
const limit = Math.max(1, input.limit ?? 50);
|
|
489
|
+
return entries.slice(0, limit);
|
|
490
|
+
},
|
|
491
|
+
async verifyEntries(input) {
|
|
492
|
+
const entries = await collectEntries(input.cacheKinds, input.selector);
|
|
493
|
+
const unhealthy = entries.filter((entry) => entry.status !== "healthy");
|
|
494
|
+
const warningStatuses = [...new Set(unhealthy.map((entry) => entry.status))];
|
|
495
|
+
return {
|
|
496
|
+
checkedEntries: entries.length,
|
|
497
|
+
unhealthyEntries: unhealthy.length,
|
|
498
|
+
warnings: warningStatuses.length > 0
|
|
499
|
+
? [`Detected cache entries with health states: ${warningStatuses.join(", ")}.`]
|
|
500
|
+
: []
|
|
501
|
+
};
|
|
502
|
+
},
|
|
503
|
+
async deleteEntries(input) {
|
|
504
|
+
const entries = await collectEntries(input.cacheKinds, input.selector);
|
|
505
|
+
const selectedBytes = entries.reduce((total, entry) => total + entry.sizeBytes, 0);
|
|
506
|
+
if (input.executionMode === "apply") {
|
|
507
|
+
const db = openDb(config.sqlitePath);
|
|
508
|
+
try {
|
|
509
|
+
for (const entry of entries) {
|
|
510
|
+
if (entry.cacheKind === "artifact-index") {
|
|
511
|
+
db?.prepare("DELETE FROM artifacts WHERE artifact_id = ?").run([entry.entryId]);
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
if (existsSync(entry.path)) {
|
|
515
|
+
await rm(entry.path, { force: true });
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
finally {
|
|
520
|
+
db?.close();
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return {
|
|
524
|
+
deletedEntries: entries.length,
|
|
525
|
+
deletedBytes: selectedBytes,
|
|
526
|
+
warnings: []
|
|
527
|
+
};
|
|
528
|
+
},
|
|
529
|
+
async pruneEntries(input) {
|
|
530
|
+
return this.deleteEntries(input);
|
|
531
|
+
},
|
|
532
|
+
async rebuildEntries(input) {
|
|
533
|
+
const entries = await collectEntries(input.cacheKinds, input.selector);
|
|
534
|
+
return {
|
|
535
|
+
rebuiltEntries: entries.length,
|
|
536
|
+
warnings: []
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
//# sourceMappingURL=cache-registry.js.map
|