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 +76 -0
- package/example.jsonc +8 -0
- package/index.ts +468 -0
- package/package.json +18 -0
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
|
+
}
|