@zaxlck/modder-installer 0.0.0-stage → 0.1.4
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/backend/data/mods.json +114 -0
- package/backend/package-lock.json +987 -0
- package/backend/package.json +23 -0
- package/backend/server.js +476 -0
- package/bin/modder-cli.js +489 -0
- package/bin/modder-format.js +767 -0
- package/bin/modder-launcher.cmd +2 -0
- package/bin/modder.js +72 -0
- package/modder.js +91 -0
- package/package.json +35 -6
- package/scripts/install.ps1 +189 -0
- package/README.md +0 -3
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const readline = require("readline/promises");
|
|
6
|
+
const modderFormat = require("./modder-format");
|
|
7
|
+
|
|
8
|
+
const API_URL = "http://localhost:3000";
|
|
9
|
+
const VERSION = "0.3.0";
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
function showHelp() {
|
|
13
|
+
console.log(`
|
|
14
|
+
🌴 MODDER CLI v0.3.0
|
|
15
|
+
|
|
16
|
+
Manage your Modder community directly from PowerShell.
|
|
17
|
+
|
|
18
|
+
USAGE:
|
|
19
|
+
modder <command> [options]
|
|
20
|
+
|
|
21
|
+
COMMANDS:
|
|
22
|
+
upload <file> Upload a .geode or .zip mod
|
|
23
|
+
list List all community mods
|
|
24
|
+
search <name> Search for mods
|
|
25
|
+
info <id> Display mod information
|
|
26
|
+
create <name> Create a Modder project
|
|
27
|
+
open <file> Extract a Modder project
|
|
28
|
+
build <folder> Package a compiled Geode mod
|
|
29
|
+
pack <folder> Archive a Modder project
|
|
30
|
+
validate <file> Validate a .modder archive
|
|
31
|
+
install <file> Add a package to your local library
|
|
32
|
+
--help, -h Show this help menu
|
|
33
|
+
--version, -v Show the CLI version
|
|
34
|
+
download <id> Download an authorised mod
|
|
35
|
+
|
|
36
|
+
EXAMPLES:
|
|
37
|
+
modder download 123
|
|
38
|
+
modder --help
|
|
39
|
+
modder --version
|
|
40
|
+
modder list
|
|
41
|
+
modder search "Neon"
|
|
42
|
+
modder info 1
|
|
43
|
+
modder upload example.geode
|
|
44
|
+
|
|
45
|
+
API:
|
|
46
|
+
${API_URL}
|
|
47
|
+
|
|
48
|
+
🌴 Made for the Geometry Dash community.
|
|
49
|
+
`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async function ask(rl, question) {
|
|
54
|
+
const answer = await rl.question(question);
|
|
55
|
+
return answer.trim();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function uploadMod(filePath) {
|
|
59
|
+
const fullPath = path.resolve(filePath);
|
|
60
|
+
|
|
61
|
+
if (!fs.existsSync(fullPath)) {
|
|
62
|
+
console.error("⌠File not found:", fullPath);
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!fs.statSync(fullPath).isFile()) {
|
|
68
|
+
console.error("⌠The supplied path is not a file.");
|
|
69
|
+
process.exitCode = 1;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const extension = path.extname(fullPath).toLowerCase();
|
|
74
|
+
|
|
75
|
+
if (![".geode", ".zip"].includes(extension)) {
|
|
76
|
+
console.error("⌠Only .geode and .zip files are supported.");
|
|
77
|
+
process.exitCode = 1;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log("\n🌴 MODDER UPLOAD");
|
|
82
|
+
console.log("------------------------------");
|
|
83
|
+
console.log("File:", path.basename(fullPath));
|
|
84
|
+
|
|
85
|
+
const rl = readline.createInterface({
|
|
86
|
+
input: process.stdin,
|
|
87
|
+
output: process.stdout
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const name = await ask(rl, "\nMod name: ");
|
|
92
|
+
const category = await ask(rl, "Category: ");
|
|
93
|
+
const version = await ask(rl, "Version (default 1.0.0): ") || "1.0.0";
|
|
94
|
+
const compatibility = await ask(rl, "GD compatibility (optional): ");
|
|
95
|
+
const author = await ask(rl, "Author (optional): ");
|
|
96
|
+
const description = await ask(rl, "Description: ");
|
|
97
|
+
|
|
98
|
+
if (!name || !category || !description) {
|
|
99
|
+
console.error("\n⌠Name, category and description are required.");
|
|
100
|
+
process.exitCode = 1;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const confirmed = await ask(rl, "\nUpload this mod? (y/n): ");
|
|
105
|
+
|
|
106
|
+
if (!["y", "yes"].includes(confirmed.toLowerCase())) {
|
|
107
|
+
console.log("\nUpload cancelled.");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log("\nUploading to Modder...");
|
|
112
|
+
|
|
113
|
+
const fileBuffer = fs.readFileSync(fullPath);
|
|
114
|
+
const formData = new FormData();
|
|
115
|
+
|
|
116
|
+
formData.append(
|
|
117
|
+
"modFile",
|
|
118
|
+
new Blob([fileBuffer]),
|
|
119
|
+
path.basename(fullPath)
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
formData.append("name", name);
|
|
123
|
+
formData.append("category", category);
|
|
124
|
+
formData.append("version", version);
|
|
125
|
+
formData.append("compatibility", compatibility);
|
|
126
|
+
formData.append("author", author);
|
|
127
|
+
formData.append("description", description);
|
|
128
|
+
|
|
129
|
+
const response = await fetch(`${API_URL}/api/mods`, {
|
|
130
|
+
method: "POST",
|
|
131
|
+
body: formData
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const result = await response.json();
|
|
135
|
+
|
|
136
|
+
if (!response.ok) {
|
|
137
|
+
throw new Error(result.error || `HTTP ${response.status}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
console.log("\n✅ Mod uploaded successfully!");
|
|
141
|
+
console.log("Name:", result.mod.name);
|
|
142
|
+
console.log("ID:", result.mod.id);
|
|
143
|
+
console.log("Status: Community listing");
|
|
144
|
+
console.log("\n🌴 Thanks for contributing to Modder!");
|
|
145
|
+
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.error("\n⌠Upload failed:", error.message);
|
|
148
|
+
process.exitCode = 1;
|
|
149
|
+
} finally {
|
|
150
|
+
rl.close();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function listMods() {
|
|
155
|
+
try {
|
|
156
|
+
const response = await fetch(`${API_URL}/api/mods`);
|
|
157
|
+
|
|
158
|
+
if (!response.ok) {
|
|
159
|
+
throw new Error(`HTTP ${response.status}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const data = await response.json();
|
|
163
|
+
|
|
164
|
+
console.log("\n🌴 MODDER COMMUNITY MODS");
|
|
165
|
+
console.log("------------------------------");
|
|
166
|
+
|
|
167
|
+
if (!data.mods.length) {
|
|
168
|
+
console.log("No mods found.");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
for (const mod of data.mods) {
|
|
173
|
+
console.log(`
|
|
174
|
+
${mod.name}
|
|
175
|
+
ID: ${mod.id}
|
|
176
|
+
Category: ${mod.category}
|
|
177
|
+
Version: ${mod.version || "Unknown"}
|
|
178
|
+
Author: ${mod.author || "Unknown"}
|
|
179
|
+
`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.log(`Total: ${data.mods.length} mods`);
|
|
183
|
+
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.error("\n⌠Could not load mods:", error.message);
|
|
186
|
+
process.exitCode = 1;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function searchMods(searchTerm) {
|
|
191
|
+
if (!searchTerm) {
|
|
192
|
+
console.error('⌠Usage: modder search "mod name"');
|
|
193
|
+
process.exitCode = 1;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const response = await fetch(`${API_URL}/api/mods`);
|
|
199
|
+
|
|
200
|
+
if (!response.ok) {
|
|
201
|
+
throw new Error(`HTTP ${response.status}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const data = await response.json();
|
|
205
|
+
|
|
206
|
+
const matches = data.mods.filter(mod => {
|
|
207
|
+
const searchable = [
|
|
208
|
+
mod.name,
|
|
209
|
+
mod.category,
|
|
210
|
+
mod.description,
|
|
211
|
+
mod.author
|
|
212
|
+
].join(" ").toLowerCase();
|
|
213
|
+
|
|
214
|
+
return searchable.includes(searchTerm.toLowerCase());
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
console.log(`\n🔎 SEARCH RESULTS: "${searchTerm}"`);
|
|
218
|
+
console.log("------------------------------");
|
|
219
|
+
|
|
220
|
+
if (!matches.length) {
|
|
221
|
+
console.log("No matching mods found.");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
for (const mod of matches) {
|
|
226
|
+
console.log(`
|
|
227
|
+
${mod.name}
|
|
228
|
+
ID: ${mod.id}
|
|
229
|
+
Category: ${mod.category}
|
|
230
|
+
Author: ${mod.author || "Unknown"}
|
|
231
|
+
`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
console.log(`Found ${matches.length} matching mods.`);
|
|
235
|
+
|
|
236
|
+
} catch (error) {
|
|
237
|
+
console.error("\n⌠Search failed:", error.message);
|
|
238
|
+
process.exitCode = 1;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function showModInfo(id) {
|
|
243
|
+
if (!id) {
|
|
244
|
+
console.error("⌠Usage: modder info <id>");
|
|
245
|
+
process.exitCode = 1;
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const response = await fetch(
|
|
251
|
+
`${API_URL}/api/mods/${encodeURIComponent(id)}`
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
if (response.status === 404) {
|
|
255
|
+
console.error("⌠Mod not found.");
|
|
256
|
+
process.exitCode = 1;
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!response.ok) {
|
|
261
|
+
throw new Error(`HTTP ${response.status}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const mod = await response.json();
|
|
265
|
+
|
|
266
|
+
console.log(`
|
|
267
|
+
🌴 MOD INFORMATION
|
|
268
|
+
------------------------------
|
|
269
|
+
|
|
270
|
+
Name: ${mod.name}
|
|
271
|
+
ID: ${mod.id}
|
|
272
|
+
Category: ${mod.category}
|
|
273
|
+
Version: ${mod.version || "Unknown"}
|
|
274
|
+
Compatibility: ${mod.compatibility || "Not verified"}
|
|
275
|
+
Author: ${mod.author || "Unknown"}
|
|
276
|
+
|
|
277
|
+
Description:
|
|
278
|
+
${mod.description}
|
|
279
|
+
|
|
280
|
+
Download: ${mod.download || "Not available"}
|
|
281
|
+
Screenshots: ${mod.screenshots?.length || 0}
|
|
282
|
+
Authorised: ${mod.authorised ? "Yes" : "No"}
|
|
283
|
+
Featured: ${mod.featured ? "Yes" : "No"}
|
|
284
|
+
|
|
285
|
+
------------------------------
|
|
286
|
+
`);
|
|
287
|
+
} catch (error) {
|
|
288
|
+
console.error("\n⌠Could not retrieve mod:", error.message);
|
|
289
|
+
process.exitCode = 1;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function downloadMod(id) {
|
|
294
|
+
if (!id) {
|
|
295
|
+
console.error("⌠Usage: modder download <id>");
|
|
296
|
+
process.exitCode = 1;
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
console.log("\n🌴 MODDER DOWNLOAD");
|
|
302
|
+
console.log("------------------------------");
|
|
303
|
+
|
|
304
|
+
// Retrieve mod information.
|
|
305
|
+
const infoResponse = await fetch(
|
|
306
|
+
`${API_URL}/api/mods/${encodeURIComponent(id)}`
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
if (infoResponse.status === 404) {
|
|
310
|
+
throw new Error("Mod not found.");
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (!infoResponse.ok) {
|
|
314
|
+
throw new Error(`API error: ${infoResponse.status}`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const mod = await infoResponse.json();
|
|
318
|
+
|
|
319
|
+
console.log("Mod:", mod.name);
|
|
320
|
+
console.log("Author:", mod.author || "Unknown");
|
|
321
|
+
|
|
322
|
+
// Only approved listings can be downloaded.
|
|
323
|
+
if (mod.authorised !== true) {
|
|
324
|
+
throw new Error(
|
|
325
|
+
"This listing has not been authorised for download."
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (!mod.download) {
|
|
330
|
+
throw new Error("This mod has no download file.");
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Only allow downloads hosted by this Modder API.
|
|
334
|
+
if (
|
|
335
|
+
typeof mod.download !== "string" ||
|
|
336
|
+
!mod.download.startsWith("/uploads/mods/") ||
|
|
337
|
+
mod.download.includes("..") ||
|
|
338
|
+
mod.download.includes("\\")
|
|
339
|
+
) {
|
|
340
|
+
throw new Error("Invalid mod download path.");
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const downloadUrl = new URL(
|
|
344
|
+
`/api/mods/${encodeURIComponent(id)}/download`,
|
|
345
|
+
API_URL
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
console.log("\nDownloading through the protected API...");
|
|
349
|
+
|
|
350
|
+
const response = await fetch(downloadUrl);
|
|
351
|
+
|
|
352
|
+
if (!response.ok) {
|
|
353
|
+
throw new Error(`Download failed: HTTP ${response.status}`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const filename = path.basename(downloadUrl.pathname);
|
|
357
|
+
|
|
358
|
+
if (!/\.(geode|zip)$/i.test(filename)) {
|
|
359
|
+
throw new Error("The downloaded file has an unsupported extension.");
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const destination = path.resolve(process.cwd(), filename);
|
|
363
|
+
|
|
364
|
+
if (fs.existsSync(destination)) {
|
|
365
|
+
throw new Error(
|
|
366
|
+
`A file named "${filename}" already exists in this folder.`
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
371
|
+
|
|
372
|
+
fs.writeFileSync(destination, buffer);
|
|
373
|
+
|
|
374
|
+
console.log("\n✅ Download completed!");
|
|
375
|
+
console.log("Saved to:", destination);
|
|
376
|
+
console.log("\n🌴 Enjoy your mod!");
|
|
377
|
+
} catch (error) {
|
|
378
|
+
console.error("\n⌠Download failed:", error.message);
|
|
379
|
+
process.exitCode = 1;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
async function main() {
|
|
386
|
+
const args = process.argv.slice(2);
|
|
387
|
+
const command = args[0];
|
|
388
|
+
|
|
389
|
+
if (!command || command === "--help" || command === "-h") {
|
|
390
|
+
showHelp();
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (command === "--version" || command === "-v") {
|
|
395
|
+
console.log(`Modder CLI v${VERSION}`);
|
|
396
|
+
return;
|
|
397
|
+
|
|
398
|
+
}
|
|
399
|
+
const formatCommands = ["create", "open", "build", "pack", "validate", "install"];
|
|
400
|
+
|
|
401
|
+
if (formatCommands.includes(command)) {
|
|
402
|
+
try {
|
|
403
|
+
switch (command) {
|
|
404
|
+
case "create":
|
|
405
|
+
if (!args[1]) throw new Error("Specify a project name.");
|
|
406
|
+
modderFormat.createProject(args[1]);
|
|
407
|
+
break;
|
|
408
|
+
|
|
409
|
+
case "open":
|
|
410
|
+
if (!args[1]) throw new Error("Specify a .modder project file.");
|
|
411
|
+
modderFormat.openProject(args[1]);
|
|
412
|
+
break;
|
|
413
|
+
|
|
414
|
+
case "pack":
|
|
415
|
+
if (!args[1]) throw new Error("Specify a project folder.");
|
|
416
|
+
modderFormat.packProject(args[1], args[2]);
|
|
417
|
+
break;
|
|
418
|
+
case "build":
|
|
419
|
+
if (!args[1]) throw new Error("Specify a project folder.");
|
|
420
|
+
modderFormat.buildPackage(args[1], args[2]);
|
|
421
|
+
break;
|
|
422
|
+
|
|
423
|
+
case "validate":
|
|
424
|
+
if (!args[1]) throw new Error("Specify a .modder file.");
|
|
425
|
+
modderFormat.validateArchive(args[1]);
|
|
426
|
+
break;
|
|
427
|
+
|
|
428
|
+
case "install":
|
|
429
|
+
if (!args[1]) throw new Error("Specify a .modder package.");
|
|
430
|
+
modderFormat.installPackage(args[1]);
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
} catch (error) {
|
|
434
|
+
console.error("\nModder:", error.message);
|
|
435
|
+
process.exitCode = 1;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (command === "download") {
|
|
441
|
+
await downloadMod(args[1]);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if (command === "list") {
|
|
445
|
+
await listMods();
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (command === "search") {
|
|
450
|
+
const searchTerm = args.slice(1).join(" ");
|
|
451
|
+
await searchMods(searchTerm);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (command === "info") {
|
|
456
|
+
await showModInfo(args[1]);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (command === "upload") {
|
|
461
|
+
if (!args[1]) {
|
|
462
|
+
console.error("⌠Please specify a mod file.");
|
|
463
|
+
console.log("Example: modder upload example.geode");
|
|
464
|
+
process.exitCode = 1;
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
await uploadMod(args[1]);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Shorthand: modder example.geode
|
|
473
|
+
// Also accepts: modder upload-example.geode
|
|
474
|
+
const filePath = command.startsWith("upload-")
|
|
475
|
+
? command.slice("upload-".length)
|
|
476
|
+
: command;
|
|
477
|
+
|
|
478
|
+
if (filePath.toLowerCase().endsWith(".geode") ||
|
|
479
|
+
filePath.toLowerCase().endsWith(".zip")) {
|
|
480
|
+
await uploadMod(filePath);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
console.error(`⌠Unknown command: ${command}`);
|
|
485
|
+
console.log('Run "modder --help" to see available commands.');
|
|
486
|
+
process.exitCode = 1;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
main();
|