@zaxlck/modder-installer 0.0.0-stage → 0.1.3

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.
@@ -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();