@creeperhost/modlens-mcp 1.6.13 → 1.6.14
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 +32 -2
- package/dist/feed-the-beast.d.ts +46 -0
- package/dist/feed-the-beast.d.ts.map +1 -0
- package/dist/feed-the-beast.js +29 -0
- package/dist/feed-the-beast.js.map +1 -0
- package/dist/generated/sqlite/browser.d.ts +10 -0
- package/dist/generated/sqlite/browser.d.ts.map +1 -1
- package/dist/generated/sqlite/client.d.ts +10 -0
- package/dist/generated/sqlite/client.d.ts.map +1 -1
- package/dist/generated/sqlite/internal/class.d.ts +22 -0
- package/dist/generated/sqlite/internal/class.d.ts.map +1 -1
- package/dist/generated/sqlite/internal/class.js +5 -5
- package/dist/generated/sqlite/internal/class.js.map +1 -1
- package/dist/generated/sqlite/internal/prismaNamespace.d.ts +180 -1
- package/dist/generated/sqlite/internal/prismaNamespace.d.ts.map +1 -1
- package/dist/generated/sqlite/internal/prismaNamespace.js +27 -0
- package/dist/generated/sqlite/internal/prismaNamespace.js.map +1 -1
- package/dist/generated/sqlite/internal/prismaNamespaceBrowser.d.ts +29 -0
- package/dist/generated/sqlite/internal/prismaNamespaceBrowser.d.ts.map +1 -1
- package/dist/generated/sqlite/internal/prismaNamespaceBrowser.js +27 -0
- package/dist/generated/sqlite/internal/prismaNamespaceBrowser.js.map +1 -1
- package/dist/generated/sqlite/models/Mod.d.ts +219 -0
- package/dist/generated/sqlite/models/Mod.d.ts.map +1 -1
- package/dist/generated/sqlite/models/PackFile.d.ts +1603 -0
- package/dist/generated/sqlite/models/PackFile.d.ts.map +1 -0
- package/dist/generated/sqlite/models/PackFile.js +2 -0
- package/dist/generated/sqlite/models/PackFile.js.map +1 -0
- package/dist/generated/sqlite/models/PackVersion.d.ts +1365 -0
- package/dist/generated/sqlite/models/PackVersion.d.ts.map +1 -0
- package/dist/generated/sqlite/models/PackVersion.js +2 -0
- package/dist/generated/sqlite/models/PackVersion.js.map +1 -0
- package/dist/generated/sqlite/models.d.ts +2 -0
- package/dist/generated/sqlite/models.d.ts.map +1 -1
- package/dist/modrinth.d.ts +10 -0
- package/dist/modrinth.d.ts.map +1 -1
- package/dist/modrinth.js +20 -0
- package/dist/modrinth.js.map +1 -1
- package/dist/server.js +25 -10
- package/dist/server.js.map +1 -1
- package/dist/tools/modpacks-ch.d.ts +269 -3
- package/dist/tools/modpacks-ch.d.ts.map +1 -1
- package/dist/tools/modpacks-ch.js +810 -11
- package/dist/tools/modpacks-ch.js.map +1 -1
- package/package.json +1 -1
- package/prisma/backends/schema.sqlite.prisma +42 -0
- package/prisma/backends/template.db +0 -0
|
@@ -19,20 +19,56 @@
|
|
|
19
19
|
* search_ftb_mods — search the FTB mod index (returns mixed CF int / MR
|
|
20
20
|
* string IDs)
|
|
21
21
|
*/
|
|
22
|
-
import { join, resolve, sep } from "path";
|
|
22
|
+
import { basename, extname, join, resolve, sep } from "path";
|
|
23
23
|
import { createHash } from "crypto";
|
|
24
|
-
import { rename, mkdir } from "fs/promises";
|
|
24
|
+
import { rename, mkdir, unlink } from "fs/promises";
|
|
25
25
|
import AdmZip from "adm-zip";
|
|
26
26
|
import { ensureDir, exists, CACHE_ROOT } from "../cache.js";
|
|
27
27
|
import { searchPacks, getFeaturedPacks, getPack, getPackManifest, getCfPack, getCfPackManifest, searchMods, getMod, getModsBatch, resolveModVersionUrl, USER_AGENT, downloadManifestFile, resolveFileUrl, } from "../modpacks-ch.js";
|
|
28
28
|
import { fetchWithRetry, DOWNLOAD_OPTS } from "../fetch-utils.js";
|
|
29
29
|
import { createWriteStream } from "fs";
|
|
30
30
|
import { pipeline } from "stream/promises";
|
|
31
|
+
import { searchProjects as searchModrinthProjects, getProject as getModrinthProject, getProjectVersions as getModrinthProjectVersions, getProjectVersion as getModrinthProjectVersion, getVersion as getModrinthVersion, getPrimaryFile as getModrinthPrimaryFile, } from "../modrinth.js";
|
|
32
|
+
import { searchOfficialFtbPacks, getOfficialFtbPack, getOfficialFtbPackManifest, } from "../feed-the-beast.js";
|
|
31
33
|
const MOD_HEADERS = { "User-Agent": USER_AGENT };
|
|
32
34
|
import { ingestMod } from "./ingest.js";
|
|
33
35
|
import { upsertPackVersion, upsertPackFile, listPackVersions, listPackFiles, findPacksForMod, findPacksForCfProject, findPackVersion, } from "../repositories/packs.js";
|
|
34
36
|
// ── Pack search / browse ──────────────────────────────────────────────────────
|
|
35
37
|
export async function searchPacksAction(term, namespace = "ftb", limit = 20) {
|
|
38
|
+
if (namespace === "modrinth") {
|
|
39
|
+
const r = await searchModrinthProjects(term, { projectType: "modpack", limit });
|
|
40
|
+
return {
|
|
41
|
+
modrinthPacks: (r?.hits ?? []).map((h) => ({
|
|
42
|
+
id: h.project_id,
|
|
43
|
+
slug: h.slug,
|
|
44
|
+
name: h.title,
|
|
45
|
+
description: h.description,
|
|
46
|
+
downloads: h.downloads,
|
|
47
|
+
versions: h.versions,
|
|
48
|
+
loaders: h.loaders,
|
|
49
|
+
url: `https://modrinth.com/modpack/${h.slug}`,
|
|
50
|
+
})),
|
|
51
|
+
total: r?.total_hits ?? 0,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (namespace === "feedthebeast") {
|
|
55
|
+
const r = await searchOfficialFtbPacks(term, limit, true);
|
|
56
|
+
const packs = (r?.packs ?? []).filter((p) => typeof p === "object");
|
|
57
|
+
return {
|
|
58
|
+
feedTheBeastPacks: packs.map((p) => ({
|
|
59
|
+
id: p.id,
|
|
60
|
+
slug: p.slug,
|
|
61
|
+
name: p.name,
|
|
62
|
+
synopsis: p.synopsis,
|
|
63
|
+
provider: p.provider,
|
|
64
|
+
platform: p.platform,
|
|
65
|
+
updated: p.updated ? new Date(p.updated * 1000).toISOString() : null,
|
|
66
|
+
url: `https://www.feed-the-beast.com/modpacks/${p.id}`,
|
|
67
|
+
})),
|
|
68
|
+
total: r?.count ?? r?.total ?? packs.length,
|
|
69
|
+
note: "Official Feed The Beast API results. Use namespace=feedthebeast for info/list_versions/ingest_pack.",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
36
72
|
// Always call the unified FTB search endpoint — it returns both FTB pack IDs
|
|
37
73
|
// (in `packs`) and CurseForge pack IDs (in `curseforge`) in a single response.
|
|
38
74
|
// The /curseforge/search/ endpoint does not exist.
|
|
@@ -56,7 +92,503 @@ export async function featuredPacksAction(limit = 20) {
|
|
|
56
92
|
return { packs: [], total: 0 };
|
|
57
93
|
return r;
|
|
58
94
|
}
|
|
95
|
+
function normalized(value) {
|
|
96
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
97
|
+
}
|
|
98
|
+
function hashToInt(value) {
|
|
99
|
+
return parseInt(createHash("sha1").update(value).digest("hex").slice(0, 7), 16);
|
|
100
|
+
}
|
|
101
|
+
function maybeNumber(value) {
|
|
102
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
103
|
+
return value;
|
|
104
|
+
if (typeof value === "string" && /^\d+$/.test(value))
|
|
105
|
+
return Number(value);
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
function parseModrinthWebRef(packRef, versionRef) {
|
|
109
|
+
if (typeof packRef !== "string")
|
|
110
|
+
return { packRef, versionRef };
|
|
111
|
+
let url;
|
|
112
|
+
try {
|
|
113
|
+
url = new URL(packRef);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return { packRef, versionRef };
|
|
117
|
+
}
|
|
118
|
+
if (url.protocol !== "https:") {
|
|
119
|
+
throw new Error(`Modrinth pack URL must use HTTPS: ${packRef}`);
|
|
120
|
+
}
|
|
121
|
+
const host = url.hostname.toLowerCase().replace(/^www\./, "");
|
|
122
|
+
const parts = url.pathname.split("/").filter(Boolean).map(decodeURIComponent);
|
|
123
|
+
if (url.pathname.toLowerCase().endsWith(".mrpack")) {
|
|
124
|
+
const cdnData = host === "cdn.modrinth.com" && parts[0] === "data" && parts[2] === "versions"
|
|
125
|
+
? { packRef: parts[1], versionRef: versionRef ?? parts[3] }
|
|
126
|
+
: {};
|
|
127
|
+
return { ...cdnData, mrpackUrl: url.toString() };
|
|
128
|
+
}
|
|
129
|
+
if (host === "modrinth.com") {
|
|
130
|
+
const packIdx = parts.indexOf("modpack");
|
|
131
|
+
if (packIdx >= 0 && parts[packIdx + 1]) {
|
|
132
|
+
const parsedVersion = parts[packIdx + 2] === "version" ? parts[packIdx + 3] : undefined;
|
|
133
|
+
return { packRef: parts[packIdx + 1], versionRef: versionRef ?? parsedVersion };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (host === "api.modrinth.com") {
|
|
137
|
+
const projectIdx = parts.indexOf("project");
|
|
138
|
+
if (projectIdx >= 0 && parts[projectIdx + 1]) {
|
|
139
|
+
const parsedVersion = parts[projectIdx + 2] === "version" ? parts[projectIdx + 3] : undefined;
|
|
140
|
+
return { packRef: parts[projectIdx + 1], versionRef: versionRef ?? parsedVersion };
|
|
141
|
+
}
|
|
142
|
+
const versionIdx = parts.indexOf("version");
|
|
143
|
+
if (versionIdx >= 0 && parts[versionIdx + 1]) {
|
|
144
|
+
return { versionRef: versionRef ?? parts[versionIdx + 1], versionIdOnly: parts[versionIdx + 1] };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return { packRef, versionRef };
|
|
148
|
+
}
|
|
149
|
+
function modrinthInternalPackId(projectId) {
|
|
150
|
+
return hashToInt(`modrinth:project:${projectId}`);
|
|
151
|
+
}
|
|
152
|
+
function modrinthInternalVersionId(versionId) {
|
|
153
|
+
return hashToInt(`modrinth:version:${versionId}`);
|
|
154
|
+
}
|
|
155
|
+
function pickModrinthLoader(deps, loaders) {
|
|
156
|
+
if (deps.neoforge)
|
|
157
|
+
return "neoforge";
|
|
158
|
+
if (deps.forge)
|
|
159
|
+
return "forge";
|
|
160
|
+
if (deps["fabric-loader"])
|
|
161
|
+
return "fabric";
|
|
162
|
+
if (deps["quilt-loader"])
|
|
163
|
+
return "quilt";
|
|
164
|
+
return loaders?.find((l) => l !== "minecraft") ?? null;
|
|
165
|
+
}
|
|
166
|
+
function classifyModrinthPath(path) {
|
|
167
|
+
const p = path.replace(/\\/g, "/").toLowerCase();
|
|
168
|
+
if (p.startsWith("mods/") && p.endsWith(".jar"))
|
|
169
|
+
return "mod";
|
|
170
|
+
if (p.startsWith("resourcepacks/"))
|
|
171
|
+
return "resource";
|
|
172
|
+
if (p.startsWith("datapacks/"))
|
|
173
|
+
return "resource";
|
|
174
|
+
if (p.startsWith("config/") || p.startsWith("defaultconfigs/"))
|
|
175
|
+
return "config";
|
|
176
|
+
if (p.startsWith("kubejs/") || p.startsWith("scripts/"))
|
|
177
|
+
return "script";
|
|
178
|
+
return "override";
|
|
179
|
+
}
|
|
180
|
+
function resolveVersionByRef(versions, ref) {
|
|
181
|
+
if (versions.length === 0)
|
|
182
|
+
throw new Error("Pack has no versions");
|
|
183
|
+
if (ref === undefined || ref === "")
|
|
184
|
+
return versions[0];
|
|
185
|
+
const numeric = maybeNumber(ref);
|
|
186
|
+
if (numeric !== undefined) {
|
|
187
|
+
const hit = versions.find((v) => typeof v.id === "number" && v.id === numeric);
|
|
188
|
+
if (hit)
|
|
189
|
+
return hit;
|
|
190
|
+
}
|
|
191
|
+
const wanted = normalized(String(ref));
|
|
192
|
+
const hit = versions.find((v) => normalized(String(v.id)) === wanted ||
|
|
193
|
+
normalized(v.name) === wanted ||
|
|
194
|
+
(v.version !== undefined && normalized(v.version) === wanted) ||
|
|
195
|
+
(v.version_number !== undefined && normalized(v.version_number) === wanted));
|
|
196
|
+
if (hit)
|
|
197
|
+
return hit;
|
|
198
|
+
const partialMatches = versions.filter((v) => normalized(v.name).includes(wanted) ||
|
|
199
|
+
(v.version !== undefined && normalized(v.version).includes(wanted)) ||
|
|
200
|
+
(v.version_number !== undefined && normalized(v.version_number).includes(wanted)));
|
|
201
|
+
if (partialMatches.length === 1)
|
|
202
|
+
return partialMatches[0];
|
|
203
|
+
if (partialMatches.length > 1) {
|
|
204
|
+
const examples = partialMatches.slice(0, 8).map((v) => `${v.name} (${String(v.id)})`).join(", ");
|
|
205
|
+
throw new Error(`Version "${String(ref)}" is ambiguous. Matching versions: ${examples}`);
|
|
206
|
+
}
|
|
207
|
+
{
|
|
208
|
+
const examples = versions.slice(0, 8).map((v) => `${v.name} (${String(v.id)})`).join(", ");
|
|
209
|
+
throw new Error(`Version "${String(ref)}" not found. Available examples: ${examples}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function versionMatchesRef(version, ref) {
|
|
213
|
+
if (ref === undefined || ref === "")
|
|
214
|
+
return false;
|
|
215
|
+
const wanted = normalized(String(ref));
|
|
216
|
+
return normalized(String(version.id)) === wanted ||
|
|
217
|
+
normalized(version.name) === wanted ||
|
|
218
|
+
normalized(version.name).includes(wanted) ||
|
|
219
|
+
(version.version !== undefined && normalized(version.version).includes(wanted)) ||
|
|
220
|
+
(version.version_number !== undefined && normalized(version.version_number).includes(wanted));
|
|
221
|
+
}
|
|
222
|
+
async function resolveModpacksChPack(namespace, rawPackRef) {
|
|
223
|
+
let packId = maybeNumber(rawPackRef);
|
|
224
|
+
if (packId === undefined) {
|
|
225
|
+
if (rawPackRef === undefined)
|
|
226
|
+
throw new Error("Provide packId, packRef, or a pack name");
|
|
227
|
+
const search = await searchPacks(String(rawPackRef), 20);
|
|
228
|
+
const ids = namespace === "curseforge" ? (search?.curseforge ?? []) : (search?.packs ?? []);
|
|
229
|
+
if (ids.length === 0)
|
|
230
|
+
throw new Error(`No ${namespace} pack found for "${String(rawPackRef)}"`);
|
|
231
|
+
const packs = (await Promise.all(ids.slice(0, 10).map((id) => (namespace === "curseforge" ? getCfPack(id) : getPack(id)).catch(() => null)))).filter((p) => p !== null);
|
|
232
|
+
const wanted = normalized(String(rawPackRef));
|
|
233
|
+
const scored = packs
|
|
234
|
+
.map((pack) => {
|
|
235
|
+
const name = normalized(pack.name);
|
|
236
|
+
const score = name === wanted ? 0 : name.includes(wanted) ? 1 : wanted.includes(name) ? 2 : 3;
|
|
237
|
+
return { pack, score };
|
|
238
|
+
})
|
|
239
|
+
.sort((a, b) => a.score - b.score || b.pack.installs - a.pack.installs);
|
|
240
|
+
packId = scored[0]?.pack.id ?? ids[0];
|
|
241
|
+
}
|
|
242
|
+
const pack = namespace === "curseforge" ? await getCfPack(packId) : await getPack(packId);
|
|
243
|
+
if (!pack)
|
|
244
|
+
throw new Error(`Pack ${packId} not found on namespace "${namespace}"`);
|
|
245
|
+
return pack;
|
|
246
|
+
}
|
|
247
|
+
function normalizeOfficialFtbManifest(manifest) {
|
|
248
|
+
return {
|
|
249
|
+
id: manifest.id,
|
|
250
|
+
parent: manifest.parent,
|
|
251
|
+
name: manifest.name,
|
|
252
|
+
type: manifest.type,
|
|
253
|
+
version: manifest.version ?? manifest.name,
|
|
254
|
+
targets: manifest.targets ?? [],
|
|
255
|
+
specs: manifest.specs,
|
|
256
|
+
installs: manifest.installs ?? 0,
|
|
257
|
+
plays: manifest.plays ?? 0,
|
|
258
|
+
updated: manifest.updated ?? 0,
|
|
259
|
+
refreshed: manifest.refreshed ?? 0,
|
|
260
|
+
status: manifest.status ?? "success",
|
|
261
|
+
files: (manifest.files ?? []).map((f) => {
|
|
262
|
+
const cfProject = Number(f.curseforge?.project);
|
|
263
|
+
const cfFile = Number(f.curseforge?.file);
|
|
264
|
+
return {
|
|
265
|
+
id: f.id,
|
|
266
|
+
name: f.name,
|
|
267
|
+
type: f.type,
|
|
268
|
+
path: f.path,
|
|
269
|
+
url: f.url,
|
|
270
|
+
mirror: f.mirror ?? f.mirrors?.[0] ?? "",
|
|
271
|
+
sha1: f.sha1 ?? f.hashes?.sha1 ?? "",
|
|
272
|
+
size: f.size,
|
|
273
|
+
clientonly: f.clientonly,
|
|
274
|
+
serveronly: f.serveronly,
|
|
275
|
+
optional: f.optional,
|
|
276
|
+
tags: f.tags ?? [],
|
|
277
|
+
curseforge: Number.isFinite(cfProject) && Number.isFinite(cfFile)
|
|
278
|
+
? { project: cfProject, file: cfFile }
|
|
279
|
+
: undefined,
|
|
280
|
+
};
|
|
281
|
+
}),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
async function resolveOfficialFtbPack(rawPackRef) {
|
|
285
|
+
let packId = maybeNumber(rawPackRef);
|
|
286
|
+
if (packId === undefined) {
|
|
287
|
+
if (rawPackRef === undefined)
|
|
288
|
+
throw new Error("Provide packId, packRef, or a pack name");
|
|
289
|
+
const search = await searchOfficialFtbPacks(String(rawPackRef), 20, true);
|
|
290
|
+
const packs = (search?.packs ?? []).filter((p) => typeof p === "object");
|
|
291
|
+
if (packs.length === 0)
|
|
292
|
+
throw new Error(`No official Feed The Beast pack found for "${String(rawPackRef)}"`);
|
|
293
|
+
const wanted = normalized(String(rawPackRef));
|
|
294
|
+
const scored = packs
|
|
295
|
+
.map((pack) => {
|
|
296
|
+
const name = normalized(pack.name);
|
|
297
|
+
const slug = normalized(pack.slug ?? "");
|
|
298
|
+
const score = name === wanted || slug === wanted ? 0 :
|
|
299
|
+
name.includes(wanted) || slug.includes(wanted) ? 1 :
|
|
300
|
+
wanted.includes(name) ? 2 : 3;
|
|
301
|
+
return { pack, score };
|
|
302
|
+
})
|
|
303
|
+
.sort((a, b) => a.score - b.score || (b.pack.installs ?? 0) - (a.pack.installs ?? 0));
|
|
304
|
+
packId = scored[0]?.pack.id;
|
|
305
|
+
}
|
|
306
|
+
if (packId === undefined)
|
|
307
|
+
throw new Error(`No official Feed The Beast pack found for "${String(rawPackRef)}"`);
|
|
308
|
+
const pack = await getOfficialFtbPack(packId);
|
|
309
|
+
if (!pack)
|
|
310
|
+
throw new Error(`Official Feed The Beast pack ${packId} not found`);
|
|
311
|
+
return pack;
|
|
312
|
+
}
|
|
313
|
+
async function resolveOfficialFtbPackVersion(opts) {
|
|
314
|
+
const rawPackRef = opts.packId ?? opts.packRef;
|
|
315
|
+
const rawVersionRef = opts.versionId ?? opts.versionRef;
|
|
316
|
+
const pack = await resolveOfficialFtbPack(rawPackRef);
|
|
317
|
+
const version = resolveVersionByRef(pack.versions ?? [], rawVersionRef);
|
|
318
|
+
return {
|
|
319
|
+
namespace: "feedthebeast",
|
|
320
|
+
packId: pack.id,
|
|
321
|
+
versionId: version.id,
|
|
322
|
+
packName: pack.name,
|
|
323
|
+
versionName: version.name,
|
|
324
|
+
mcVersion: version.targets.find((t) => t.type === "game" || t.name === "minecraft")?.version ?? null,
|
|
325
|
+
loader: version.targets.find((t) => t.type === "modloader")?.name ?? null,
|
|
326
|
+
sourcePackId: pack.id,
|
|
327
|
+
sourceVersionId: version.id,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
async function resolveModpacksChPackVersion(opts) {
|
|
331
|
+
const namespace = opts.namespace === "curseforge" ? "curseforge" : "ftb";
|
|
332
|
+
const rawPackRef = opts.packId ?? opts.packRef;
|
|
333
|
+
const rawVersionRef = opts.versionId ?? opts.versionRef;
|
|
334
|
+
const pack = await resolveModpacksChPack(namespace, rawPackRef);
|
|
335
|
+
const version = resolveVersionByRef(pack.versions ?? [], rawVersionRef);
|
|
336
|
+
return {
|
|
337
|
+
namespace,
|
|
338
|
+
packId: pack.id,
|
|
339
|
+
versionId: version.id,
|
|
340
|
+
packName: pack.name,
|
|
341
|
+
versionName: version.name,
|
|
342
|
+
mcVersion: version.targets.find((t) => t.type === "game" || t.name === "minecraft")?.version ?? null,
|
|
343
|
+
loader: version.targets.find((t) => t.type === "modloader")?.name ?? null,
|
|
344
|
+
sourcePackId: pack.id,
|
|
345
|
+
sourceVersionId: version.id,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
export async function listRemotePackVersionsAction(opts) {
|
|
349
|
+
const namespace = opts.namespace ?? "ftb";
|
|
350
|
+
const versionRef = opts.versionId ?? opts.versionRef;
|
|
351
|
+
if (namespace === "modrinth") {
|
|
352
|
+
const parsed = parseModrinthWebRef(opts.packId ?? opts.packRef, versionRef);
|
|
353
|
+
if (parsed.mrpackUrl) {
|
|
354
|
+
const { index } = await downloadModrinthPackIndexFromUrl(parsed.mrpackUrl);
|
|
355
|
+
const row = {
|
|
356
|
+
id: parsed.versionRef ?? index.versionId,
|
|
357
|
+
internalVersionId: modrinthInternalVersionId(String(parsed.versionRef ?? index.versionId)),
|
|
358
|
+
name: index.versionId,
|
|
359
|
+
version: index.versionId,
|
|
360
|
+
type: "release",
|
|
361
|
+
mcVersions: index.dependencies.minecraft ? [index.dependencies.minecraft] : [],
|
|
362
|
+
loaders: [pickModrinthLoader(index.dependencies) ?? "unknown"].filter((v) => v !== "unknown"),
|
|
363
|
+
updated: null,
|
|
364
|
+
matchesRef: versionRef === undefined || versionMatchesRef({ id: parsed.versionRef ?? index.versionId, name: index.versionId, version_number: index.versionId }, versionRef),
|
|
365
|
+
};
|
|
366
|
+
return {
|
|
367
|
+
namespace,
|
|
368
|
+
packId: modrinthInternalPackId(String(parsed.packRef ?? parsed.mrpackUrl)),
|
|
369
|
+
sourcePackId: parsed.packRef ?? parsed.mrpackUrl,
|
|
370
|
+
packName: index.name,
|
|
371
|
+
total: 1,
|
|
372
|
+
matches: versionRef === undefined ? undefined : (row.matchesRef ? [row] : []),
|
|
373
|
+
versions: [row],
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
if (parsed.versionIdOnly) {
|
|
377
|
+
const version = await getModrinthVersion(parsed.versionIdOnly);
|
|
378
|
+
if (!version)
|
|
379
|
+
throw new Error(`Modrinth version ${parsed.versionIdOnly} not found`);
|
|
380
|
+
parsed.packRef = version.project_id;
|
|
381
|
+
parsed.versionRef = parsed.versionRef ?? version.id;
|
|
382
|
+
}
|
|
383
|
+
const project = await resolveModrinthPack(parsed.packRef);
|
|
384
|
+
const versions = await getModrinthProjectVersions(project.slug || project.id);
|
|
385
|
+
const rows = versions.map((v) => ({
|
|
386
|
+
id: v.id,
|
|
387
|
+
internalVersionId: modrinthInternalVersionId(v.id),
|
|
388
|
+
name: v.name,
|
|
389
|
+
version: v.version_number,
|
|
390
|
+
type: v.version_type,
|
|
391
|
+
mcVersions: v.game_versions ?? [],
|
|
392
|
+
loaders: v.loaders ?? [],
|
|
393
|
+
updated: v.date_published,
|
|
394
|
+
matchesRef: versionMatchesRef(v, versionRef),
|
|
395
|
+
}));
|
|
396
|
+
return {
|
|
397
|
+
namespace,
|
|
398
|
+
packId: modrinthInternalPackId(project.id),
|
|
399
|
+
sourcePackId: project.id,
|
|
400
|
+
slug: project.slug,
|
|
401
|
+
packName: project.title,
|
|
402
|
+
total: rows.length,
|
|
403
|
+
matches: versionRef === undefined ? undefined : rows.filter((v) => v.matchesRef),
|
|
404
|
+
versions: rows,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
if (namespace === "feedthebeast") {
|
|
408
|
+
const pack = await resolveOfficialFtbPack(opts.packId ?? opts.packRef);
|
|
409
|
+
const rows = (pack.versions ?? []).map((v) => ({
|
|
410
|
+
id: v.id,
|
|
411
|
+
name: v.name,
|
|
412
|
+
type: v.type,
|
|
413
|
+
mcVersions: (v.targets ?? []).filter((t) => t.type === "game" || t.name === "minecraft").map((t) => t.version),
|
|
414
|
+
loaders: (v.targets ?? []).filter((t) => t.type === "modloader").map((t) => t.name),
|
|
415
|
+
updated: v.updated ? new Date(v.updated * 1000).toISOString() : null,
|
|
416
|
+
matchesRef: versionMatchesRef(v, versionRef),
|
|
417
|
+
}));
|
|
418
|
+
return {
|
|
419
|
+
namespace,
|
|
420
|
+
packId: pack.id,
|
|
421
|
+
slug: pack.slug,
|
|
422
|
+
packName: pack.name,
|
|
423
|
+
total: rows.length,
|
|
424
|
+
matches: versionRef === undefined ? undefined : rows.filter((v) => v.matchesRef),
|
|
425
|
+
versions: rows,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
const ns = namespace === "curseforge" ? "curseforge" : "ftb";
|
|
429
|
+
const pack = await resolveModpacksChPack(ns, opts.packId ?? opts.packRef);
|
|
430
|
+
const rows = (pack.versions ?? []).map((v) => ({
|
|
431
|
+
id: v.id,
|
|
432
|
+
name: v.name,
|
|
433
|
+
type: v.type,
|
|
434
|
+
mcVersions: (v.targets ?? []).filter((t) => t.type === "game" || t.name === "minecraft").map((t) => t.version),
|
|
435
|
+
loaders: (v.targets ?? []).filter((t) => t.type === "modloader").map((t) => t.name),
|
|
436
|
+
updated: new Date(v.updated * 1000).toISOString(),
|
|
437
|
+
matchesRef: versionMatchesRef(v, versionRef),
|
|
438
|
+
}));
|
|
439
|
+
return {
|
|
440
|
+
namespace: ns,
|
|
441
|
+
packId: pack.id,
|
|
442
|
+
packName: pack.name,
|
|
443
|
+
total: rows.length,
|
|
444
|
+
matches: versionRef === undefined ? undefined : rows.filter((v) => v.matchesRef),
|
|
445
|
+
versions: rows,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
async function resolveModrinthPack(packRef) {
|
|
449
|
+
if (packRef === undefined)
|
|
450
|
+
throw new Error("Provide packId, packRef, or a pack name");
|
|
451
|
+
const parsed = parseModrinthWebRef(packRef);
|
|
452
|
+
if (parsed.versionIdOnly) {
|
|
453
|
+
const version = await getModrinthVersion(parsed.versionIdOnly);
|
|
454
|
+
if (!version)
|
|
455
|
+
throw new Error(`Modrinth version ${parsed.versionIdOnly} not found`);
|
|
456
|
+
return resolveModrinthPack(version.project_id);
|
|
457
|
+
}
|
|
458
|
+
if (parsed.packRef !== packRef)
|
|
459
|
+
return resolveModrinthPack(parsed.packRef);
|
|
460
|
+
const direct = await getModrinthProject(String(packRef)).catch(() => null);
|
|
461
|
+
if (direct?.project_type === "modpack" || (direct && direct.project_type === undefined))
|
|
462
|
+
return direct;
|
|
463
|
+
const search = await searchModrinthProjects(String(packRef), { projectType: "modpack", limit: 10 });
|
|
464
|
+
const hits = search?.hits ?? [];
|
|
465
|
+
if (hits.length === 0)
|
|
466
|
+
throw new Error(`No Modrinth modpack found for "${String(packRef)}"`);
|
|
467
|
+
const exact = hits.find((h) => normalized(h.slug) === normalized(String(packRef)) ||
|
|
468
|
+
normalized(h.title) === normalized(String(packRef)) ||
|
|
469
|
+
h.project_id === String(packRef));
|
|
470
|
+
const hit = exact ?? hits[0];
|
|
471
|
+
const project = await getModrinthProject(hit.project_id);
|
|
472
|
+
if (!project)
|
|
473
|
+
throw new Error(`Modrinth project ${hit.project_id} not found after search`);
|
|
474
|
+
return project;
|
|
475
|
+
}
|
|
476
|
+
async function resolveModrinthVersion(project, versionRef) {
|
|
477
|
+
const parsed = parseModrinthWebRef(project.slug || project.id, versionRef);
|
|
478
|
+
versionRef = parsed.versionRef;
|
|
479
|
+
if (versionRef !== undefined && versionRef !== "") {
|
|
480
|
+
const direct = await getModrinthProjectVersion(project.slug || project.id, String(versionRef)).catch(() => null);
|
|
481
|
+
if (direct)
|
|
482
|
+
return direct;
|
|
483
|
+
}
|
|
484
|
+
const versions = await getModrinthProjectVersions(project.slug || project.id);
|
|
485
|
+
return resolveVersionByRef(versions, versionRef);
|
|
486
|
+
}
|
|
487
|
+
export async function resolvePackAction(opts) {
|
|
488
|
+
const namespace = opts.namespace ?? "ftb";
|
|
489
|
+
if (namespace === "feedthebeast")
|
|
490
|
+
return resolveOfficialFtbPackVersion(opts);
|
|
491
|
+
if (namespace !== "modrinth")
|
|
492
|
+
return resolveModpacksChPackVersion(opts);
|
|
493
|
+
const parsed = parseModrinthWebRef(opts.packId ?? opts.packRef, opts.versionId ?? opts.versionRef);
|
|
494
|
+
if (parsed.mrpackUrl) {
|
|
495
|
+
const { index } = await downloadModrinthPackIndexFromUrl(parsed.mrpackUrl);
|
|
496
|
+
const sourcePackId = String(parsed.packRef ?? parsed.mrpackUrl);
|
|
497
|
+
const sourceVersionId = String(parsed.versionRef ?? index.versionId);
|
|
498
|
+
return {
|
|
499
|
+
namespace: "modrinth",
|
|
500
|
+
packId: modrinthInternalPackId(sourcePackId),
|
|
501
|
+
versionId: modrinthInternalVersionId(sourceVersionId),
|
|
502
|
+
packName: index.name,
|
|
503
|
+
versionName: index.versionId,
|
|
504
|
+
mcVersion: index.dependencies.minecraft ?? null,
|
|
505
|
+
loader: pickModrinthLoader(index.dependencies),
|
|
506
|
+
sourcePackId,
|
|
507
|
+
sourceVersionId,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
if (parsed.versionIdOnly) {
|
|
511
|
+
const version = await getModrinthVersion(parsed.versionIdOnly);
|
|
512
|
+
if (!version)
|
|
513
|
+
throw new Error(`Modrinth version ${parsed.versionIdOnly} not found`);
|
|
514
|
+
const project = await resolveModrinthPack(version.project_id);
|
|
515
|
+
return {
|
|
516
|
+
namespace: "modrinth",
|
|
517
|
+
packId: modrinthInternalPackId(project.id),
|
|
518
|
+
versionId: modrinthInternalVersionId(version.id),
|
|
519
|
+
packName: project.title,
|
|
520
|
+
versionName: version.name || version.version_number,
|
|
521
|
+
mcVersion: version.game_versions?.[0] ?? null,
|
|
522
|
+
loader: version.loaders?.find((l) => l !== "minecraft") ?? null,
|
|
523
|
+
sourcePackId: project.id,
|
|
524
|
+
sourceVersionId: version.id,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
const project = await resolveModrinthPack(parsed.packRef);
|
|
528
|
+
const version = await resolveModrinthVersion(project, parsed.versionRef);
|
|
529
|
+
return {
|
|
530
|
+
namespace: "modrinth",
|
|
531
|
+
packId: modrinthInternalPackId(project.id),
|
|
532
|
+
versionId: modrinthInternalVersionId(version.id),
|
|
533
|
+
packName: project.title,
|
|
534
|
+
versionName: version.name || version.version_number,
|
|
535
|
+
mcVersion: version.game_versions?.[0] ?? null,
|
|
536
|
+
loader: version.loaders?.find((l) => l !== "minecraft") ?? null,
|
|
537
|
+
sourcePackId: project.id,
|
|
538
|
+
sourceVersionId: version.id,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
59
541
|
export async function packInfoAction(packId, namespace = "ftb") {
|
|
542
|
+
if (namespace === "modrinth") {
|
|
543
|
+
const project = await getModrinthProject(String(packId));
|
|
544
|
+
if (!project)
|
|
545
|
+
throw new Error(`Modrinth pack ${packId} not found`);
|
|
546
|
+
const versions = await getModrinthProjectVersions(project.slug || project.id);
|
|
547
|
+
return {
|
|
548
|
+
id: project.id,
|
|
549
|
+
internalPackId: modrinthInternalPackId(project.id),
|
|
550
|
+
slug: project.slug,
|
|
551
|
+
name: project.title,
|
|
552
|
+
synopsis: project.description,
|
|
553
|
+
provider: "modrinth",
|
|
554
|
+
versions: versions.map((v) => ({
|
|
555
|
+
id: v.id,
|
|
556
|
+
internalVersionId: modrinthInternalVersionId(v.id),
|
|
557
|
+
name: v.name,
|
|
558
|
+
version: v.version_number,
|
|
559
|
+
type: v.version_type,
|
|
560
|
+
targets: [
|
|
561
|
+
...(v.game_versions ?? []).map((version) => ({ name: "minecraft", version })),
|
|
562
|
+
...(v.loaders ?? []).map((name) => ({ name, version: "" })),
|
|
563
|
+
],
|
|
564
|
+
updated: v.date_published,
|
|
565
|
+
})),
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
if (namespace === "feedthebeast") {
|
|
569
|
+
const pack = await getOfficialFtbPack(packId);
|
|
570
|
+
if (!pack)
|
|
571
|
+
throw new Error(`Official Feed The Beast pack ${packId} not found`);
|
|
572
|
+
return {
|
|
573
|
+
id: pack.id,
|
|
574
|
+
slug: pack.slug,
|
|
575
|
+
name: pack.name,
|
|
576
|
+
synopsis: pack.synopsis,
|
|
577
|
+
provider: "feedthebeast",
|
|
578
|
+
installs: pack.installs,
|
|
579
|
+
plays: pack.plays,
|
|
580
|
+
tags: (pack.tags ?? []).map((t) => t.name),
|
|
581
|
+
authors: (pack.authors ?? []).map((a) => ({ name: a.name, type: a.type })),
|
|
582
|
+
links: (pack.links ?? []).map((l) => ({ type: l.type, url: l.link })),
|
|
583
|
+
versions: (pack.versions ?? []).map((v) => ({
|
|
584
|
+
id: v.id,
|
|
585
|
+
name: v.name,
|
|
586
|
+
type: v.type,
|
|
587
|
+
targets: (v.targets ?? []).map((t) => ({ name: t.name, version: t.version })),
|
|
588
|
+
updated: v.updated ? new Date(v.updated * 1000).toISOString() : null,
|
|
589
|
+
})),
|
|
590
|
+
};
|
|
591
|
+
}
|
|
60
592
|
const pack = namespace === "curseforge"
|
|
61
593
|
? await getCfPack(packId)
|
|
62
594
|
: await getPack(packId);
|
|
@@ -82,9 +614,14 @@ export async function packInfoAction(packId, namespace = "ftb") {
|
|
|
82
614
|
};
|
|
83
615
|
}
|
|
84
616
|
export async function packManifestAction(packId, versionId, namespace = "ftb") {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
617
|
+
if (namespace === "modrinth") {
|
|
618
|
+
throw new Error("Use action=ingest_pack or action=resolve_pack for Modrinth packs; Modrinth manifests are stored inside the .mrpack download.");
|
|
619
|
+
}
|
|
620
|
+
const manifest = namespace === "feedthebeast"
|
|
621
|
+
? await getOfficialFtbPackManifest(packId, versionId).then((m) => m ? normalizeOfficialFtbManifest(m) : null)
|
|
622
|
+
: namespace === "curseforge"
|
|
623
|
+
? await getCfPackManifest(packId, versionId)
|
|
624
|
+
: await getPackManifest(packId, versionId);
|
|
88
625
|
if (!manifest)
|
|
89
626
|
throw new Error(`Manifest not found for pack ${packId} version ${versionId} (${namespace})`);
|
|
90
627
|
const fileSummary = (f) => ({
|
|
@@ -248,9 +785,11 @@ export async function syncPackModsAction(opts) {
|
|
|
248
785
|
const ingestTypes = opts.fileTypes ?? ["mod", "resource"];
|
|
249
786
|
const skipServer = opts.skipServer ?? false;
|
|
250
787
|
const skipOptional = opts.skipOptional ?? false;
|
|
251
|
-
const manifest = namespace === "
|
|
252
|
-
? await
|
|
253
|
-
:
|
|
788
|
+
const manifest = namespace === "feedthebeast"
|
|
789
|
+
? await getOfficialFtbPackManifest(packId, versionId).then((m) => m ? normalizeOfficialFtbManifest(m) : null)
|
|
790
|
+
: namespace === "curseforge"
|
|
791
|
+
? await getCfPackManifest(packId, versionId)
|
|
792
|
+
: await getPackManifest(packId, versionId);
|
|
254
793
|
if (!manifest) {
|
|
255
794
|
throw new Error(`Manifest not found for pack ${packId} v${versionId} (${namespace})`);
|
|
256
795
|
}
|
|
@@ -265,7 +804,7 @@ export async function syncPackModsAction(opts) {
|
|
|
265
804
|
if (!resolveFileUrl(f))
|
|
266
805
|
return false; // no URL → skip
|
|
267
806
|
return true;
|
|
268
|
-
});
|
|
807
|
+
}).slice(0, opts.maxFiles ?? undefined);
|
|
269
808
|
const packCacheDir = join(CACHE_ROOT, "packs", String(namespace), String(packId), String(versionId));
|
|
270
809
|
const results = [];
|
|
271
810
|
// Process `concurrency` files at a time
|
|
@@ -282,8 +821,8 @@ export async function syncPackModsAction(opts) {
|
|
|
282
821
|
namespace,
|
|
283
822
|
packId,
|
|
284
823
|
versionId,
|
|
285
|
-
packName: manifest.name,
|
|
286
|
-
versionName: manifest.version || manifest.name,
|
|
824
|
+
packName: opts.packName ?? manifest.name,
|
|
825
|
+
versionName: (opts.versionName ?? manifest.version) || manifest.name,
|
|
287
826
|
mcVersion,
|
|
288
827
|
modloader,
|
|
289
828
|
});
|
|
@@ -369,6 +908,266 @@ async function processFile(file, cacheDir) {
|
|
|
369
908
|
}
|
|
370
909
|
// ── Pack membership queries ───────────────────────────────────────────────────
|
|
371
910
|
/** List all pack versions recorded in the DB, optionally filtered by namespace / packId. */
|
|
911
|
+
async function downloadUrlToFile(url, destPath) {
|
|
912
|
+
const res = await fetchWithRetry(url, { headers: MOD_HEADERS }, DOWNLOAD_OPTS);
|
|
913
|
+
if (!res.ok)
|
|
914
|
+
throw new Error(`Download failed: HTTP ${res.status} from ${url}`);
|
|
915
|
+
const writer = createWriteStream(destPath);
|
|
916
|
+
await pipeline(res.body, writer);
|
|
917
|
+
}
|
|
918
|
+
async function processDownloadableFile(file, cacheDir) {
|
|
919
|
+
const key = file.sha1 || createHash("sha1").update(file.url).digest("hex");
|
|
920
|
+
const ext = extname(file.name).toLowerCase() || (file.type === "mod" ? ".jar" : "");
|
|
921
|
+
const destPath = join(cacheDir, `${key}${ext}`);
|
|
922
|
+
try {
|
|
923
|
+
await ensureDir(destPath);
|
|
924
|
+
if (!(await exists(destPath))) {
|
|
925
|
+
const tmpPath = destPath + ".tmp";
|
|
926
|
+
try {
|
|
927
|
+
await downloadUrlToFile(file.url, tmpPath);
|
|
928
|
+
await rename(tmpPath, destPath);
|
|
929
|
+
}
|
|
930
|
+
catch (err) {
|
|
931
|
+
await unlink(tmpPath).catch(() => { });
|
|
932
|
+
throw err;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
if (ext !== ".jar") {
|
|
936
|
+
return { name: file.name, type: file.type, fileId: file.id, status: ext === ".zip" ? "downloaded_zip" : "downloaded" };
|
|
937
|
+
}
|
|
938
|
+
const result = await ingestMod(destPath, /* skipSource */ true);
|
|
939
|
+
return {
|
|
940
|
+
name: file.name,
|
|
941
|
+
type: file.type,
|
|
942
|
+
fileId: file.id,
|
|
943
|
+
status: result.status,
|
|
944
|
+
dbId: result.status === "ingested" || result.status === "replaced"
|
|
945
|
+
? result.mod?.id
|
|
946
|
+
: result.status === "already_ingested" || result.status === "metadata_refreshed"
|
|
947
|
+
? result.mod?.id
|
|
948
|
+
: result.existingDbId,
|
|
949
|
+
message: "message" in result ? result.message : undefined,
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
catch (err) {
|
|
953
|
+
return {
|
|
954
|
+
name: file.name,
|
|
955
|
+
type: file.type,
|
|
956
|
+
fileId: file.id,
|
|
957
|
+
status: "error",
|
|
958
|
+
message: err instanceof Error ? err.message : String(err),
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
async function downloadModrinthPackIndex(project, version) {
|
|
963
|
+
const file = getModrinthPrimaryFile(version);
|
|
964
|
+
if (!file)
|
|
965
|
+
throw new Error(`Modrinth version ${version.id} has no downloadable files`);
|
|
966
|
+
if (!file.filename.endsWith(".mrpack")) {
|
|
967
|
+
throw new Error(`Primary file for Modrinth version ${version.id} is not an .mrpack: ${file.filename}`);
|
|
968
|
+
}
|
|
969
|
+
const packDir = join(CACHE_ROOT, "packs", "modrinth", project.id, version.id);
|
|
970
|
+
const mrpackPath = join(packDir, file.filename);
|
|
971
|
+
await mkdir(packDir, { recursive: true });
|
|
972
|
+
if (!(await exists(mrpackPath))) {
|
|
973
|
+
const tmpPath = mrpackPath + ".tmp";
|
|
974
|
+
try {
|
|
975
|
+
await downloadUrlToFile(file.url, tmpPath);
|
|
976
|
+
await rename(tmpPath, mrpackPath);
|
|
977
|
+
}
|
|
978
|
+
catch (err) {
|
|
979
|
+
await unlink(tmpPath).catch(() => { });
|
|
980
|
+
throw err;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
const index = readModrinthPackIndex(mrpackPath, file.filename);
|
|
984
|
+
return { index, mrpackPath };
|
|
985
|
+
}
|
|
986
|
+
function readModrinthPackIndex(mrpackPath, label) {
|
|
987
|
+
const zip = new AdmZip(mrpackPath);
|
|
988
|
+
const indexEntry = zip.getEntry("modrinth.index.json");
|
|
989
|
+
if (!indexEntry)
|
|
990
|
+
throw new Error(`Modrinth pack ${label} has no modrinth.index.json`);
|
|
991
|
+
return JSON.parse(indexEntry.getData().toString("utf8"));
|
|
992
|
+
}
|
|
993
|
+
async function downloadModrinthPackIndexFromUrl(mrpackUrl) {
|
|
994
|
+
const url = new URL(mrpackUrl);
|
|
995
|
+
if (url.protocol !== "https:")
|
|
996
|
+
throw new Error(`Modrinth .mrpack URL must use HTTPS: ${mrpackUrl}`);
|
|
997
|
+
if (!url.pathname.toLowerCase().endsWith(".mrpack"))
|
|
998
|
+
throw new Error(`Expected a .mrpack URL, got: ${mrpackUrl}`);
|
|
999
|
+
const urlKey = createHash("sha1").update(url.toString()).digest("hex");
|
|
1000
|
+
const filename = decodeURIComponent(url.pathname.split("/").pop() ?? "pack.mrpack");
|
|
1001
|
+
const packDir = join(CACHE_ROOT, "packs", "modrinth-url", urlKey);
|
|
1002
|
+
const mrpackPath = join(packDir, filename);
|
|
1003
|
+
await mkdir(packDir, { recursive: true });
|
|
1004
|
+
if (!(await exists(mrpackPath))) {
|
|
1005
|
+
const tmpPath = mrpackPath + ".tmp";
|
|
1006
|
+
try {
|
|
1007
|
+
await downloadUrlToFile(url.toString(), tmpPath);
|
|
1008
|
+
await rename(tmpPath, mrpackPath);
|
|
1009
|
+
}
|
|
1010
|
+
catch (err) {
|
|
1011
|
+
await unlink(tmpPath).catch(() => { });
|
|
1012
|
+
throw err;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
return { index: readModrinthPackIndex(mrpackPath, filename), mrpackPath };
|
|
1016
|
+
}
|
|
1017
|
+
async function syncModrinthIndex(opts, meta, index, mrpackPath) {
|
|
1018
|
+
const ingestTypes = opts.fileTypes ?? ["mod", "resource"];
|
|
1019
|
+
const skipServer = opts.skipServer ?? false;
|
|
1020
|
+
const skipOptional = opts.skipOptional ?? false;
|
|
1021
|
+
const concurrency = opts.concurrency ?? 3;
|
|
1022
|
+
const downloadable = index.files.map((f) => {
|
|
1023
|
+
const type = classifyModrinthPath(f.path);
|
|
1024
|
+
const url = f.downloads?.[0];
|
|
1025
|
+
if (!url)
|
|
1026
|
+
return null;
|
|
1027
|
+
return {
|
|
1028
|
+
id: hashToInt(`modrinth:file:${meta.sourceVersionId}:${f.path}`),
|
|
1029
|
+
name: basename(f.path),
|
|
1030
|
+
type,
|
|
1031
|
+
path: f.path,
|
|
1032
|
+
url,
|
|
1033
|
+
sha1: f.hashes?.sha1 ?? null,
|
|
1034
|
+
serveronly: f.env?.client === "unsupported" && f.env.server !== "unsupported",
|
|
1035
|
+
optional: f.env?.client === "optional" || f.env?.server === "optional",
|
|
1036
|
+
};
|
|
1037
|
+
}).filter((f) => f !== null);
|
|
1038
|
+
const candidates = downloadable.filter((f) => {
|
|
1039
|
+
if (!ingestTypes.includes(f.type))
|
|
1040
|
+
return false;
|
|
1041
|
+
if (skipServer && f.serveronly)
|
|
1042
|
+
return false;
|
|
1043
|
+
if (skipOptional && f.optional)
|
|
1044
|
+
return false;
|
|
1045
|
+
return true;
|
|
1046
|
+
}).slice(0, opts.maxFiles ?? undefined);
|
|
1047
|
+
const packCacheDir = join(CACHE_ROOT, "packs", "modrinth", meta.sourcePackId.replace(/[^a-zA-Z0-9._-]/g, "_"), meta.sourceVersionId.replace(/[^a-zA-Z0-9._-]/g, "_"), "files");
|
|
1048
|
+
const results = [];
|
|
1049
|
+
for (let i = 0; i < candidates.length; i += concurrency) {
|
|
1050
|
+
const batch = candidates.slice(i, i + concurrency);
|
|
1051
|
+
const batchResults = await Promise.all(batch.map((file) => processDownloadableFile(file, packCacheDir)));
|
|
1052
|
+
results.push(...batchResults);
|
|
1053
|
+
}
|
|
1054
|
+
const modloader = pickModrinthLoader(index.dependencies, meta.loaders);
|
|
1055
|
+
const pvId = await upsertPackVersion({
|
|
1056
|
+
namespace: "modrinth",
|
|
1057
|
+
packId: meta.packId,
|
|
1058
|
+
versionId: meta.versionId,
|
|
1059
|
+
packName: meta.packName,
|
|
1060
|
+
versionName: meta.versionName,
|
|
1061
|
+
mcVersion: index.dependencies.minecraft ?? meta.mcVersion ?? null,
|
|
1062
|
+
modloader,
|
|
1063
|
+
});
|
|
1064
|
+
const resultMap = new Map();
|
|
1065
|
+
for (const r of results) {
|
|
1066
|
+
if (r.fileId !== undefined)
|
|
1067
|
+
resultMap.set(r.fileId, r);
|
|
1068
|
+
}
|
|
1069
|
+
await Promise.all(downloadable.map((f) => {
|
|
1070
|
+
const processed = resultMap.get(f.id);
|
|
1071
|
+
return upsertPackFile({
|
|
1072
|
+
packVersionId: pvId,
|
|
1073
|
+
manifestFileId: f.id,
|
|
1074
|
+
fileType: f.type,
|
|
1075
|
+
fileName: f.name,
|
|
1076
|
+
filePath: f.path || null,
|
|
1077
|
+
sha1: f.sha1 || null,
|
|
1078
|
+
status: processed?.status ?? "not_synced",
|
|
1079
|
+
modId: processed?.dbId ?? null,
|
|
1080
|
+
});
|
|
1081
|
+
}));
|
|
1082
|
+
const ingested = results.filter((r) => r.status === "ingested" || r.status === "replaced" || r.status === "metadata_refreshed").length;
|
|
1083
|
+
const skipped = results.filter((r) => r.status === "already_ingested" || r.status === "duplicate_version" || r.status === "duplicate_hash").length;
|
|
1084
|
+
const failed = results.filter((r) => r.status === "error").length;
|
|
1085
|
+
return {
|
|
1086
|
+
packId: meta.packId,
|
|
1087
|
+
versionId: meta.versionId,
|
|
1088
|
+
namespace: "modrinth",
|
|
1089
|
+
packVersionDbId: pvId,
|
|
1090
|
+
sourcePackId: meta.sourcePackId,
|
|
1091
|
+
sourceVersionId: meta.sourceVersionId,
|
|
1092
|
+
mrpackPath,
|
|
1093
|
+
total: candidates.length,
|
|
1094
|
+
ingested,
|
|
1095
|
+
skipped,
|
|
1096
|
+
failed,
|
|
1097
|
+
files: results,
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
export async function syncModrinthPackAction(opts) {
|
|
1101
|
+
const parsed = parseModrinthWebRef(opts.packId ?? opts.packRef, opts.versionId ?? opts.versionRef);
|
|
1102
|
+
if (parsed.mrpackUrl) {
|
|
1103
|
+
const { index, mrpackPath } = await downloadModrinthPackIndexFromUrl(parsed.mrpackUrl);
|
|
1104
|
+
const sourcePackId = String(parsed.packRef ?? parsed.mrpackUrl);
|
|
1105
|
+
const sourceVersionId = String(parsed.versionRef ?? index.versionId);
|
|
1106
|
+
return syncModrinthIndex(opts, {
|
|
1107
|
+
packId: modrinthInternalPackId(sourcePackId),
|
|
1108
|
+
versionId: modrinthInternalVersionId(sourceVersionId),
|
|
1109
|
+
sourcePackId,
|
|
1110
|
+
sourceVersionId,
|
|
1111
|
+
packName: index.name,
|
|
1112
|
+
versionName: index.versionId,
|
|
1113
|
+
mcVersion: index.dependencies.minecraft ?? null,
|
|
1114
|
+
}, index, mrpackPath);
|
|
1115
|
+
}
|
|
1116
|
+
if (parsed.versionIdOnly) {
|
|
1117
|
+
const version = await getModrinthVersion(parsed.versionIdOnly);
|
|
1118
|
+
if (!version)
|
|
1119
|
+
throw new Error(`Modrinth version ${parsed.versionIdOnly} not found`);
|
|
1120
|
+
const project = await resolveModrinthPack(version.project_id);
|
|
1121
|
+
const { index, mrpackPath } = await downloadModrinthPackIndex(project, version);
|
|
1122
|
+
return syncModrinthIndex(opts, {
|
|
1123
|
+
packId: modrinthInternalPackId(project.id),
|
|
1124
|
+
versionId: modrinthInternalVersionId(version.id),
|
|
1125
|
+
sourcePackId: project.id,
|
|
1126
|
+
sourceVersionId: version.id,
|
|
1127
|
+
packName: project.title,
|
|
1128
|
+
versionName: version.name || version.version_number || index.versionId,
|
|
1129
|
+
mcVersion: version.game_versions?.[0] ?? null,
|
|
1130
|
+
loaders: version.loaders,
|
|
1131
|
+
}, index, mrpackPath);
|
|
1132
|
+
}
|
|
1133
|
+
const project = await resolveModrinthPack(parsed.packRef);
|
|
1134
|
+
const version = await resolveModrinthVersion(project, parsed.versionRef);
|
|
1135
|
+
const { index, mrpackPath } = await downloadModrinthPackIndex(project, version);
|
|
1136
|
+
return syncModrinthIndex(opts, {
|
|
1137
|
+
packId: modrinthInternalPackId(project.id),
|
|
1138
|
+
versionId: modrinthInternalVersionId(version.id),
|
|
1139
|
+
sourcePackId: project.id,
|
|
1140
|
+
sourceVersionId: version.id,
|
|
1141
|
+
packName: project.title,
|
|
1142
|
+
versionName: version.name || version.version_number || index.versionId,
|
|
1143
|
+
mcVersion: version.game_versions?.[0] ?? null,
|
|
1144
|
+
loaders: version.loaders,
|
|
1145
|
+
}, index, mrpackPath);
|
|
1146
|
+
}
|
|
1147
|
+
export async function ingestPackAction(opts) {
|
|
1148
|
+
const namespace = opts.namespace ?? "ftb";
|
|
1149
|
+
if (namespace === "modrinth")
|
|
1150
|
+
return syncModrinthPackAction(opts);
|
|
1151
|
+
const resolved = namespace === "feedthebeast"
|
|
1152
|
+
? await resolveOfficialFtbPackVersion(opts)
|
|
1153
|
+
: await resolveModpacksChPackVersion(opts);
|
|
1154
|
+
const result = await syncPackModsAction({
|
|
1155
|
+
packId: resolved.packId,
|
|
1156
|
+
versionId: resolved.versionId,
|
|
1157
|
+
namespace: resolved.namespace,
|
|
1158
|
+
packName: resolved.packName,
|
|
1159
|
+
versionName: resolved.versionName,
|
|
1160
|
+
fileTypes: opts.fileTypes,
|
|
1161
|
+
skipServer: opts.skipServer,
|
|
1162
|
+
skipOptional: opts.skipOptional,
|
|
1163
|
+
concurrency: opts.concurrency,
|
|
1164
|
+
maxFiles: opts.maxFiles,
|
|
1165
|
+
});
|
|
1166
|
+
return {
|
|
1167
|
+
...result,
|
|
1168
|
+
resolved,
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
372
1171
|
export async function listPackVersionsAction(namespace, packId) {
|
|
373
1172
|
const rows = await listPackVersions(namespace, packId);
|
|
374
1173
|
return {
|