gsmc-pack 2.0.1

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 ADDED
@@ -0,0 +1,76 @@
1
+ # Geesecraft Minecraft Autopacker
2
+
3
+ ## About
4
+
5
+ A tool to downloads mods, shaders, resourcepacks, and more automatically, so you don't have to do it manually!
6
+
7
+ ## Setup
8
+
9
+ > [!NOTE]
10
+ > Requires [Bun](https://bun.com) as runtime.
11
+
12
+ ```sh
13
+ git clone https://github.com/DmmDGM/gsmc-pack
14
+ cd gsmc-pack
15
+ bun i
16
+ bun . --help
17
+ ```
18
+
19
+ ```sh
20
+ bun add -g gsmc-pack
21
+ gsmc-pack --help
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ```sh
27
+ bunx gsmc-pack [--flags...]
28
+ ```
29
+
30
+ ## Flags
31
+
32
+ | Short | Long | Description |
33
+ | - | - | - |
34
+ | `-d` | `--directory {path-to-dir}` | Specifies a directory as the output directory. |
35
+ | `-f` | `--file {path-to-file}` | Specifies a .jsonc file as the input file. |
36
+ | `-P` | `--check-peers` | Checks for peers in addition to the origin list. |
37
+ | `-C` | `--clean` | Deletes the output direct to simulate a fresh start. |
38
+ | | | (Takes effect only if `--test` flag is disabled.) |
39
+ | | | (Effectively enables `--force` flag.) |
40
+ | `-M` | `--dot-minecraft` | Places files in a .minecraft-like structure. |
41
+ | `-E` | `--elaborate` | Makes extra requests to give more details about an error. |
42
+ | | | (Takes effect only if `--verbose` flag is enabled.) |
43
+ | `-F` | `--force` | Packs files even if they already exist. |
44
+ | `-h` | `--help` | Prints this help menu. |
45
+ | `-i` | `--ignore-warnings` | Ignores all warnings. |
46
+ | `-N` | `--nyaa` | Nyaa~! :3 |
47
+ | `-T` | `--test` | Performs a dummy run but does not download the actual files. |
48
+ | | | (Effectively enables `--force` flag.) |
49
+ | `-V` | `--verbose` | Prints more information about an error. |
50
+ | `-v` | `--version` | Prints the current version of gsmc-pack. |
51
+
52
+ ## Tutorial
53
+
54
+ 1. Create a `pack.jsonc` in your local directory.
55
+ 2. Insert an array of `origins` inside the file, each line in the following format:
56
+ - `$TARGET;download;$TYPE;$URL`
57
+ - `$TARGET;modrinth;$PLATFORM;$VERSION`
58
+
59
+ ```jsonc
60
+ [
61
+ // Examples
62
+ "fabric-api;modrinth;fabric;1.21.5",
63
+ "travelersbackpack;modrinth;fabric;1.21.5",
64
+ "complementary-reimagined;modrinth;iris;1.21.5",
65
+ "default-dark-mode;modrinth;minecraft;1.21.5",
66
+ "tech-reborn;download;mod;https://github.com/TechReborn/TechReborn/releases/download/5.13.4/TechReborn-5.13.4.jar"
67
+
68
+ // Note: You can also find this in the example.jsonc file.
69
+ ]
70
+ ```
71
+
72
+ 3. Run `bunx gsmc-pack`.
73
+
74
+ ---
75
+
76
+ ###### Last Updated: 2026-01-02 16:18 (EST).
package/example.jsonc ADDED
@@ -0,0 +1,8 @@
1
+ [
2
+ // Examples
3
+ "fabric-api;modrinth;fabric;1.21.5",
4
+ "travelersbackpack;modrinth;fabric;1.21.5",
5
+ "complementary-reimagined;modrinth;iris;1.21.5",
6
+ "default-dark-mode;modrinth;minecraft;1.21.5",
7
+ "tech-reborn;download;mod;https://github.com/TechReborn/TechReborn/releases/download/5.13.4/TechReborn-5.13.4.jar"
8
+ ]
package/index.ts ADDED
@@ -0,0 +1,468 @@
1
+ #!/usr/bin/env bun
2
+
3
+ // Imports
4
+ import nodeFS from "node:fs/promises";
5
+ import nodePath from "node:path";
6
+ import nodeUtil from "node:util";
7
+ import chalk from "chalk";
8
+ import nodePackage from "./package.json";
9
+
10
+ // Defines types
11
+ interface ModrinthMatch {
12
+ filename: string;
13
+ hash: string;
14
+ platforms: string[];
15
+ rejects: string[];
16
+ requires: string[];
17
+ slug: string;
18
+ type: "mod" | "resource" | "shader" | "unknown";
19
+ url: string;
20
+ versions: string[];
21
+ }
22
+
23
+ // Defines flags
24
+ const { values: flags } = nodeUtil.parseArgs({
25
+ allowNegative: false,
26
+ allowPositionals: true,
27
+ args: Bun.argv,
28
+ options: {
29
+ "directory": { default: "./pack/", multiple: false, short: "d", type: "string" },
30
+ "file": { default: "./pack.jsonc", multiple: false, short: "f", type: "string" },
31
+ "check-peers": { default: false, multiple: false, short: "P", type: "boolean" },
32
+ "clean": { default: false, multiple: false, short: "C", type: "boolean" },
33
+ "dot-minecraft": { default: false, multiple: false, short: "M", type: "boolean" },
34
+ "elaborate": { default: false, multiple: false, short: "E", type: "boolean" },
35
+ "force": { default: false, multiple: false, short: "F", type: "boolean" },
36
+ "help": { default: false, multiple: false, short: "h", type: "boolean" },
37
+ "ignore-warnings": { default: false, multiple: false, short: "i", type: "boolean" },
38
+ "nyaa": { default: false, multiple: false, short: "N", type: "boolean" },
39
+ "test": { default: false, multiple: false, short: "T", type: "boolean" },
40
+ "verbose": { default: false, multiple: false, short: "V", type: "boolean" },
41
+ "version": { default: false, multiple: false, short: "v", type: "boolean" },
42
+ },
43
+ strict: true,
44
+ tokens: false
45
+ });
46
+
47
+ // Defines shortcuts
48
+ const { blue, bold, cyan, dim, gray, green, magenta: pink, red, yellow } = chalk;
49
+ const err = (message: unknown) => console.error(flags["nyaa"] ? `${message} ${red("Nyuh!")}` : message);
50
+ const log = (message: unknown) => console.log(flags["nyaa"] ? `${message} ${pink("Nyaa~! :3")}` : message);
51
+ const warn = (message: unknown) => alert(flags["nyaa"] ? `${message} ${pink("Nywa!!!")}` : message);
52
+
53
+ // Defines paths
54
+ const packd = nodePath.resolve(flags["directory"]);
55
+ const packf = nodePath.resolve(flags["file"]);
56
+
57
+ // Defines functions
58
+ function fetchFile(filename: string, type: string): Bun.BunFile {
59
+ // Checks dot-minecraft flag
60
+ if(flags["dot-minecraft"]) {
61
+ // Defines structures
62
+ const structures = {
63
+ "mod": "./mods/",
64
+ "resource": "./resourcepacks/",
65
+ "shader": "./shaderpacks/",
66
+ "texture": "./texturepacks/",
67
+ "unknown": "./unknown/",
68
+ "world": "./saves/"
69
+ } as const;
70
+
71
+ // Creates structured file
72
+ if(!(type in structures)) throw new Error(`Unknown type ${blue(type)}.`);
73
+ const structure = structures[type as keyof typeof structures];
74
+ return Bun.file(nodePath.resolve(packd, structure, filename));
75
+ }
76
+
77
+ // Creates flat file
78
+ return Bun.file(nodePath.resolve(packd, filename));
79
+ }
80
+ function parseDisposition(response: Response): string | null {
81
+ // Fetches disposition
82
+ const disposition = response.headers.get("content-disposition");
83
+ if(disposition === null) return null;
84
+
85
+ // Parses filename
86
+ const matches = disposition.match(/filename=(?:"(.+)"|(.+))/);
87
+ if(matches === null) return null;
88
+ return matches[1] ?? matches[2] ?? null;
89
+ }
90
+ function parseOrigin<Pattern extends readonly string[]>(
91
+ origin: string,
92
+ pattern: Pattern
93
+ ): Record<Pattern[number], string> {
94
+ // Checks parameters
95
+ const parameters = origin.split(";");
96
+ if(parameters.length < pattern.length) throw new Error(`Invalid origin ${blue(origin)}, expects pattern ${blue(pattern.join(";"))}.`);
97
+
98
+ // Parses origin
99
+ return Object.fromEntries(pattern.map((key, index) => [ key as Pattern[number], parameters[index] ] as const)) as Record<Pattern[number], string>;
100
+ }
101
+ function verifyHash(algorithm: Bun.SupportedCryptoAlgorithms, bytes: Uint8Array<ArrayBuffer>, hash: string): boolean {
102
+ // Verifies hash
103
+ return Bun.CryptoHasher.hash(algorithm, bytes).toHex() === hash;
104
+ }
105
+ async function modrinthFetch(request: Request): Promise<Response> {
106
+ // Configures request
107
+ if(!request.url.startsWith("https://api.modrinth.com/v2/")) throw new Error("Request does not interact with the modrinth API.");
108
+ request.headers.append("user-agent", `DmmDGM/gsmc-pack/${nodePackage.version}`);
109
+
110
+ // Attempts fetch
111
+ for(let attempts = 0; attempts < 3; attempts++) {
112
+ // Fetches response
113
+ const response = await fetch(request.clone());
114
+
115
+ // Checks ratelimit
116
+ const xratelimit = response.headers.get("x-ratelimit-remaining");
117
+ if(xratelimit === null) throw new Error("Missing header 'x-ratelimit-remaining' in response.");
118
+ const ratelimit = parseInt(xratelimit) ?? 0;
119
+ if(ratelimit < 1) {
120
+ const xtimeout = response.headers.get("x-ratelimit-reset");
121
+ if(xtimeout === null) throw new Error("Missing header 'x-ratelimit-reset' in response.");
122
+ const timeout = parseInt(xtimeout) ?? 0;
123
+ await Bun.sleep(timeout * 1000 + 1000);
124
+ continue;
125
+ }
126
+
127
+ // Returns response
128
+ return response;
129
+ }
130
+
131
+ // Throws error
132
+ throw new Error("Cannot reach Modrinth API.");
133
+ }
134
+ async function modrinthMatch(id: string, platforms: string[] | null, versions: string[] | null): Promise<ModrinthMatch | null> {
135
+ // Defines maps
136
+ const types = {
137
+ "mod": "mod",
138
+ "modpack": "unknown",
139
+ "resourcepack": "resource",
140
+ "shader": "shader"
141
+ } as const;
142
+
143
+ // Fetches modrinth project
144
+ const modrinthProjectURL = new URL(`./project/${id}`, "https://api.modrinth.com/v2/");
145
+ const modrinthProjectRequest = new Request(modrinthProjectURL);
146
+ const modrinthProjectResponse = await modrinthFetch(modrinthProjectRequest);
147
+ if(!modrinthProjectResponse.ok) return null;
148
+ const modrinthProjectJSON = await modrinthProjectResponse.json() as Record<string, unknown>;
149
+
150
+ // Fetches modrinth versions
151
+ const modrinthVersionsURL = new URL(`./project/${id}/version`, "https://api.modrinth.com/v2/");
152
+ if(platforms !== null) modrinthVersionsURL.searchParams.append("loaders", JSON.stringify(platforms));
153
+ if(versions !== null) modrinthVersionsURL.searchParams.append("game_versions", JSON.stringify(versions));
154
+ const modrinthVersionsRequest = new Request(modrinthVersionsURL);
155
+ const modrinthVersionsResponse = await modrinthFetch(modrinthVersionsRequest);
156
+ if(!modrinthVersionsResponse.ok) return null;
157
+ const modrinthVersionsJSON = await modrinthVersionsResponse.json() as Record<string, unknown>[];
158
+
159
+ // Parses data
160
+ const modrinthProject = {
161
+ slug: modrinthProjectJSON["slug"] as string,
162
+ type: types[modrinthProjectJSON["project_type"] as keyof typeof types]
163
+ };
164
+ const modrinthVersions = modrinthVersionsJSON
165
+ .map((data) => ({
166
+ date: new Date(data["date_published"] as string),
167
+ dependencies: (data["dependencies"] as Record<string, unknown>[])
168
+ .map((subdata) => ({
169
+ id: subdata["project_id"] as string,
170
+ type: subdata["dependency_type"] as "embedded" | "incompatible" | "optional" | "required"
171
+ })),
172
+ files: (data["files"] as Record<string, unknown>[])
173
+ .map((subdata) => ({
174
+ filename: subdata["filename"] as string,
175
+ hash: (subdata["hashes"] as { sha512: string; }).sha512,
176
+ primary: subdata["primary"] as boolean,
177
+ url: subdata["url"] as string
178
+ })),
179
+ platforms: data["loaders"] as string[],
180
+ versions: data["game_versions"] as string[]
181
+ }))
182
+ .sort((a, b) => +b.date - +a.date)
183
+ .filter((data) => data.files.length > 0);
184
+ if(modrinthVersions.length === 0) return null;
185
+ const modrinthVersion = modrinthVersions[0];
186
+ const modrinthDependencies = modrinthVersion.dependencies;
187
+ const modrinthFile = modrinthVersion.files.find((data) => data.primary) || modrinthVersion.files[0];
188
+ return {
189
+ filename: modrinthFile.filename,
190
+ hash: modrinthFile.hash,
191
+ platforms: modrinthVersion.platforms,
192
+ rejects: modrinthDependencies.filter((data) => data.type === "incompatible").map((data) => data.id),
193
+ requires: modrinthDependencies.filter((data) => data.type === "required").map((data) => data.id),
194
+ slug: modrinthProject.slug,
195
+ type: modrinthProject.type,
196
+ url: modrinthFile.url,
197
+ versions: modrinthVersion.versions
198
+ };
199
+ }
200
+ async function modrinthPeers(match: ModrinthMatch): Promise<{ rejects: string[]; requires: string[]; }> {
201
+ // Fetches modrinth rejects
202
+ const modrinthRejectsURL = new URL("./projects", "https://api.modrinth.com/v2/");
203
+ modrinthRejectsURL.searchParams.append("ids", JSON.stringify(match.rejects));
204
+ const modrinthRejectsRequest = new Request(modrinthRejectsURL);
205
+ const modrinthRejectsResponse = await modrinthFetch(modrinthRejectsRequest);
206
+ if(!modrinthRejectsResponse.ok) return { rejects: [], requires: [] };
207
+ const modrinthRejectsJSON = await modrinthRejectsResponse.json() as Record<string, unknown>[];
208
+
209
+ // Fetches modrinth requires
210
+ const modrinthRequiresURL = new URL("./projects", "https://api.modrinth.com/v2/");
211
+ modrinthRequiresURL.searchParams.append("ids", JSON.stringify(match.requires));
212
+ const modrinthRequiresRequest = new Request(modrinthRequiresURL);
213
+ const modrinthRequiresResponse = await modrinthFetch(modrinthRequiresRequest);
214
+ if(!modrinthRequiresResponse.ok) return { rejects: [], requires: [] };
215
+ const modrinthRequiresJSON = await modrinthRequiresResponse.json() as Record<string, unknown>[];
216
+
217
+ // Parses data
218
+ const modrinthRejects = modrinthRejectsJSON.map((data) => data["slug"] as string);
219
+ const modrinthRequires = modrinthRequiresJSON.map((data) => data["slug"] as string);
220
+ return { rejects: modrinthRejects, requires: modrinthRequires };
221
+ }
222
+ async function writeBytes(file: Bun.BunFile, bytes: Uint8Array<ArrayBuffer>): Promise<void> {
223
+ // Writes file
224
+ try { await file.write(bytes); }
225
+ catch(error) { await file.delete(); throw new Error(`Failed to write file ${blue(file.name)}.`); }
226
+ }
227
+
228
+ // Handles exceptions
229
+ process.on("uncaughtException", (error) => {
230
+ err(red(`[-] ${error.message}`));
231
+ process.exit(1);
232
+ });
233
+ process.on("unhandledRejection", (reason) => {
234
+ err(red(`[-] ${String(reason)}`));
235
+ process.exit(1);
236
+ });
237
+
238
+ // Prints help menu
239
+ if(flags["help"]) {
240
+ flags
241
+ console.log(cyan(bold("<<========== Geesecraft Minecraft Autopacker ==========>>")));
242
+ console.log(gray("A tool to downloads mods, shaders, resourcepacks, and more automatically, so you don't have to do it manually!"));
243
+ console.log("");
244
+ console.log(`${bold("Usage:")} ${blue("bunx gsmc-pack [--flags...]")}`);
245
+ console.log(`${bold("Flags:")}`);
246
+ console.log(` ${blue("-d --directory {path-to-dir} ")} Specifies a directory as the output directory.`);
247
+ console.log(` ${blue("-f --file {path-to-file} ")} Specifies a .jsonc file as the input file.`);
248
+ console.log(` ${blue("-P --check-peers ")} Checks for peers in addition to the origin list.`);
249
+ console.log(` ${blue("-C --clean ")} Deletes the output direct to simulate a fresh start.`);
250
+ console.log(` (Takes effect only if ${blue("--test")} flag is disabled.)`);
251
+ console.log(` (Effectively enables ${blue("--force")} flag.)`);
252
+ console.log(` ${blue("-M --dot-minecraft ")} Places files in a .minecraft-like structure.`);
253
+ console.log(` ${blue("-E --elaborate ")} Makes extra requests to give more details about an error.`);
254
+ console.log(` (Takes effect only if ${blue("--verbose")} flag is enabled.)`);
255
+ console.log(` ${blue("-F --force ")} Packs files even if they already exist.`);
256
+ console.log(` ${blue("-h --help ")} Prints this help menu.`);
257
+ console.log(` ${blue("-i --ignore-warnings ")} Ignores all warnings.`);
258
+ console.log(` ${blue("-N --nyaa ")} ${pink("Nyaa~! :3")}`);
259
+ console.log(` ${blue("-T --test ")} Performs a dummy run but does not download the actual files.`);
260
+ console.log(` (Effectively enables ${blue("--force")} flag.)`);
261
+ console.log(` ${blue("-V --verbose ")} Prints more information about an error.`);
262
+ console.log(` ${blue("-v --version ")} Prints the current version of gsmc-pack.`);
263
+ console.log("");
264
+ console.log(`${bold("Tutorial:")}`);
265
+ console.log(` 1. Create a ${blue("pack.jsonc")} in your local directory.`);
266
+ console.log(` 2. Insert an array of ${blue("origins")} inside the file, each line in the following format:`);
267
+ console.log(` - "${gray("$TARGET;download;$TYPE;$URL")}"`);
268
+ console.log(` - "${gray("$TARGET;modrinth;$PLATFORM;$VERSION")}"`);
269
+ console.log(` 3. Run ${blue("bunx gsmc-pack")}.`);
270
+ console.log("");
271
+ console.log(` You can read more about it at ${blue("https://github.com/DmmDGM/gsmc-pack")}.`);
272
+ process.exit(0);
273
+ }
274
+
275
+ // Prints version
276
+ if(flags["version"]) {
277
+ console.log(nodePackage.version);
278
+ process.exit(0);
279
+ }
280
+
281
+ // Fetches list
282
+ const list: string[] = [];
283
+ try { const result = await import(packf) as { default: string[]; }; Object.assign(list, result.default); }
284
+ catch { throw new Error(`File ${blue(packf)} is not found.`); }
285
+
286
+ // Defines cache
287
+ const finals: Map<string, string> = new Map();
288
+ const origins: Map<string, string> = new Map();
289
+ const rejects: Set<string> = new Set();
290
+
291
+ // Cleans directory
292
+ if(flags["clean"] && !flags["test"]) {
293
+ if(!flags["ignore-warnings"]) warn(yellow(`[$] You are about to clean the directory ${blue(packd)}, continue?`));
294
+ try { await nodeFS.rm(packd, { force: true, recursive: true }) }
295
+ catch {}
296
+ log(red(`[!] Cleared directory ${blue(packd)}.`));
297
+ }
298
+
299
+ // Registers origins
300
+ for(const origin of list) {
301
+ try {
302
+ // Parses origin
303
+ const { $TARGET } = parseOrigin(origin, [ "$TARGET" ] as const);
304
+
305
+ // Checks duplicate
306
+ if(origins.has($TARGET)) {
307
+ log(dim(`[#] Duplicate found for target ${blue($TARGET)}, skipped.`));
308
+ continue;
309
+ }
310
+
311
+ // Appends origin
312
+ origins.set($TARGET, origin);
313
+ }
314
+ catch(error) {
315
+ // Prints error
316
+ err(red(`[-] Origin ${blue(origin)} failed.`));
317
+ if(flags["verbose"])
318
+ err(red(dim(` [-] ${error instanceof Error ? error.message : String(error)}`)));
319
+ }
320
+ }
321
+
322
+ // Evaluates origins
323
+ for(const origin of origins.values()) {
324
+ try {
325
+ // Parses origin
326
+ const { $TARGET, $METHOD } = parseOrigin(origin, [ "$TARGET", "$METHOD" ] as const);
327
+
328
+ // Evaluates origin
329
+ switch($METHOD) {
330
+ case "download": {
331
+ // Parses origin
332
+ const { $TYPE, $URL } = parseOrigin(origin, [ "$TARGET", "$METHOD", "$TYPE", "$URL" ] as const);
333
+
334
+ // Fetches lookup
335
+ const lookup = await fetch($URL, { method: "HEAD" });
336
+ if(!lookup.ok) throw new Error(`Failed to fetch URL ${blue($URL)}.`);
337
+
338
+ // Fetches file
339
+ const filename = parseDisposition(lookup) ?? $URL.split("/").at(-1) ?? $TARGET;
340
+ const file = fetchFile(filename, $TYPE);
341
+
342
+ // Packs origin
343
+ switch(true) {
344
+ case flags["test"]: {
345
+ // Prints conclusion
346
+ log(green(`[+] Target ${blue($TARGET)} is okay.`));
347
+ finals.set($TARGET, file.name ?? "file");
348
+ continue;
349
+ }
350
+ case await file.exists() && !flags["force"]: {
351
+ // Prints conclusion
352
+ log(dim(`[#] Target ${blue($TARGET)} already exists, filename ${blue(file.name ?? "file")}.`));
353
+ finals.set($TARGET, file.name ?? "file");
354
+ continue;
355
+ }
356
+ default: {
357
+ // Creates response
358
+ const response = await fetch($URL);
359
+ if(!response.ok) throw new Error(`Failed to fetch URL ${blue($URL)}.`);
360
+
361
+ // Writes bytes
362
+ const bytes = await response.bytes();
363
+ const size = Math.round(bytes.length / 1024 / 1024 * 100) / 100;
364
+ await writeBytes(file, bytes);
365
+
366
+ // Prints conclusion
367
+ log(green(`[+] Target ${blue($TARGET)} packed, file size ${blue(size)} MiB, filename ${blue(file.name ?? "file")}.`));
368
+ finals.set($TARGET, file.name ?? "file");
369
+ continue;
370
+ }
371
+ }
372
+ }
373
+ case "modrinth": {
374
+ // Parses origin
375
+ const { $PLATFORM, $VERSION } = parseOrigin(origin, [ "$TARGET", "$METHOD", "$PLATFORM", "$VERSION" ] as const);
376
+
377
+ // Fetches match
378
+ const match = await modrinthMatch($TARGET, [ $PLATFORM ], [ $VERSION ]);
379
+ if(!match) {
380
+ // Checks nearest
381
+ if(flags["verbose"] && flags["elaborate"]) {
382
+ const nearest = await modrinthMatch($TARGET, [ $PLATFORM ], null);
383
+ if(nearest === null) throw new Error(`Target ${blue($TARGET)} does not supported platform ${blue($PLATFORM)} on Modrinth.`);
384
+ throw new Error(`Target ${blue($TARGET)} only supports platform ${blue($PLATFORM)}, version(s) ${blue(nearest.versions.join(", "))} on Modrinth.`);
385
+ }
386
+
387
+ // Throws error
388
+ throw new Error(`Cannot find target ${blue($TARGET)} on Modrinth.`);
389
+ }
390
+
391
+ // Checks peers
392
+ if(flags["check-peers"]) {
393
+ const peers = await modrinthPeers(match);
394
+ for(const peer of peers.rejects) {
395
+ log(dim(`[#] Target ${blue($TARGET)} is incompatible with peer ${blue(peer)}, flagged in origins.`));
396
+ rejects.add(peer);
397
+ }
398
+ for(const peer of peers.requires) {
399
+ log(dim(`[#] Target ${blue($TARGET)} requires peer ${blue(peer)}, added to origins.`));
400
+ if(!origins.has(peer)) origins.set(peer, `${peer};modrinth;${$PLATFORM};${$VERSION}`);
401
+ }
402
+ }
403
+
404
+ // Fetches file
405
+ const file = fetchFile(match.filename, match.type);
406
+
407
+ // Packs origin
408
+ switch(true) {
409
+ case flags["test"]: {
410
+ // Fetches lookup
411
+ const lookup = await fetch(match.url, { method: "HEAD" });
412
+ if(!lookup.ok) throw new Error("Cannot reach modrinth CDN.");
413
+
414
+ // Prints conclusion
415
+ log(green(`[+] Target ${blue($TARGET)} is okay.`));
416
+ finals.set($TARGET, file.name ?? "file");
417
+ continue;
418
+ }
419
+ case await file.exists() && !flags["force"]: {
420
+ // Prints conclusion
421
+ log(dim(`[#] Target ${blue($TARGET)} already exists, filename ${blue(file.name ?? "file")}.`));
422
+ finals.set($TARGET, file.name ?? "file");
423
+ continue;
424
+ }
425
+ default: {
426
+ // Creates response
427
+ const response = await fetch(match.url);
428
+ if(!response.ok) throw new Error("Cannot reach modrinth CDN.");
429
+
430
+ // Verifies bytes
431
+ const bytes = await response.bytes();
432
+ if(!verifyHash("sha512", bytes, match.hash)) throw new Error("File hash does not match expected hash.");
433
+
434
+ // Writes bytes
435
+ const size = Math.round(bytes.length / 1024 / 1024 * 100) / 100;
436
+ await writeBytes(file, bytes);
437
+
438
+ // Prints conclusion
439
+ log(green(`[+] Target ${blue($TARGET)} packed, file size ${blue(size)} MiB, filename ${blue(file.name ?? "file")}.`));
440
+ finals.set($TARGET, file.name ?? "file");
441
+ continue;
442
+ }
443
+ }
444
+ }
445
+ default: {
446
+ throw new Error(`Invalid origin ${blue(origin)}, unknown method ${blue($METHOD)}.`);
447
+ }
448
+ }
449
+ }
450
+ catch(error) {
451
+ // Prints error
452
+ err(red(`[-] Origin ${blue(origin)} failed.`));
453
+ if(flags["verbose"])
454
+ err(red(dim(` [-] ${error instanceof Error ? error.message : String(error)}`)));
455
+ }
456
+ }
457
+
458
+ // Prints rejects
459
+ if(flags["check-peers"]) {
460
+ for(const peer of rejects) {
461
+ if(finals.has(peer)) {
462
+ err(red(`[!] Peer ${blue(peer)} conflicts with one or more targets.`));
463
+ }
464
+ }
465
+ }
466
+
467
+ // Prints final
468
+ log(cyan(`[@] Total ${pink(origins.size)} origin(s), final ${pink(finals.size)} okay.`));
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "bin": "./index.ts",
3
+ "dependencies": {
4
+ "chalk": "^5.6.2"
5
+ },
6
+ "devDependencies": {
7
+ "@types/bun": "latest"
8
+ },
9
+ "module": "index.ts",
10
+ "name": "gsmc-pack",
11
+ "peerDependencies": {
12
+ "typescript": "^5.9.3"
13
+ },
14
+ "private": false,
15
+ "scripts": {},
16
+ "type": "module",
17
+ "version": "2.0.1"
18
+ }