diff options
| author | hachem <im@hachem.wtf> | 2025-12-09 21:55:02 +0100 |
|---|---|---|
| committer | hachem <im@hachem.wtf> | 2025-12-09 21:55:02 +0100 |
| commit | 50e62e246fef7975866c244e08ead7c681d50349 (patch) | |
| tree | b863e4b818d5bfa4fbbdcb774351b094d323e3d1 /updater/cli.js | |
| parent | ee977e942c8deabde89f7979fc00850922062926 (diff) | |
[add]: prompt+banner+refactor
Diffstat (limited to 'updater/cli.js')
| -rw-r--r-- | updater/cli.js | 393 |
1 files changed, 393 insertions, 0 deletions
diff --git a/updater/cli.js b/updater/cli.js new file mode 100644 index 0000000..8731ab6 --- /dev/null +++ b/updater/cli.js @@ -0,0 +1,393 @@ +#!/usr/bin/env node + +const readline = require("readline") +const fs = require("fs-extra") +const path = require("path") +const { spawn } = require("child_process") + +const { LoadConfig } = require("../lib/config") +const { GetModVersions, GetCompatibleVersion, DownloadFile } = require("../lib/modrinth") + +const pkg = require("../package.json") +const config = LoadConfig() + +const +{ + minecraft_version, + fabric_loader_version, + loader_meta_version, + java_path, + modrinth_mods +} = config + +const javaCmd = java_path || "java" + +const serverDir = path.join(__dirname, "../server") +const modsDir = path.join(serverDir, "mods") + +const deleteList = +[ + "audioplayer_uploads", + "config", + "cache", + "world", + "logs", + "banned-players.json", + "server.properties", + "banned-ips.json", + "usercache.json", + "whitelist.json", + "ops.json", + "eula.txt" +] + + +const EnsureDirs = async () => +{ + await fs.ensureDir(serverDir) + await fs.ensureDir(modsDir) +} + +const DownloadModrinthMods = async (modList, mcVersion) => +{ + const downloadedFiles = [] + const failedMods = [] + + for (const slug of modList) + { + try + { + const versions = await GetModVersions(slug) + if (!versions) + { + console.log(` [Modrinth] Could not get versions for ${slug}`) + failedMods.push(slug) + continue + } + + const compatible = GetCompatibleVersion(versions, mcVersion, "fabric") + if (!compatible) + { + console.log(` [Modrinth] No Fabric version for ${slug} on MC ${mcVersion}`) + failedMods.push(slug) + continue + } + + const fileInfo = compatible.files[0] + const jarUrl = fileInfo.url + const filename = fileInfo.filename + const destPath = path.join(modsDir, filename) + + console.log(` [Modrinth] Downloading ${slug} -> ${filename} ...`) + + const success = await DownloadFile(jarUrl, destPath) + if (!success) + { + console.log(` [Modrinth] Couldn't download jar for ${slug}`) + failedMods.push(slug) + continue + } + + downloadedFiles.push(destPath) + console.log(` [Modrinth] Downloaded: ${filename}`) + } + catch (err) + { + console.error(` [Modrinth] Error downloading ${slug}:`, err.message) + failedMods.push(slug) + } + } + + return { downloadedFiles, failedMods } +} + +const DownloadFabricServer = async (mcVersion, loaderVersion, metaVersion) => +{ + const url = `https://meta.fabricmc.net/v2/versions/loader/${mcVersion}/${loaderVersion}/${metaVersion}/server/jar` + const filename = "server.jar" + const destPath = path.join(serverDir, filename) + + console.log(` [Fabric] Downloading Fabric server for MC ${mcVersion} ...`) + + const success = await DownloadFile(url, destPath) + if (!success) + { + console.log(` [Fabric] Failed to download Fabric server JAR`) + return null + } + + console.log(` [Fabric] Downloaded: ${filename}`) + return destPath +} + +const RunServerAutoStop = (rl) => +{ + return new Promise((resolve) => + { + const jarPath = path.join(serverDir, "server.jar") + + if (!fs.existsSync(jarPath)) + { + console.log(" Server JAR not found.") + resolve(false) + return + } + + const eulaPath = path.join(serverDir, "eula.txt") + if (!fs.existsSync(eulaPath)) + { + console.log(" Accepting EULA...") + fs.writeFileSync(eulaPath, "eula=true\n") + } + + console.log(" Starting server (will auto-stop after loading)...\n") + + rl.pause() + + let hadError = false + + const serverProcess = spawn(javaCmd, ["-jar", "server.jar", "nogui"], + { + cwd: serverDir, + stdio: ["pipe", "pipe", "pipe"] + }) + + serverProcess.stdout.on("data", (data) => + { + const line = data.toString() + process.stdout.write(line) + + if (line.includes("Done")) + { + console.log("\n Server loaded, stopping...") + serverProcess.stdin.write("stop\n") + } + }) + + serverProcess.stderr.on("data", (data) => + { + process.stderr.write(data.toString()) + }) + + serverProcess.on("error", err => + { + hadError = true + console.error(" Failed to start server:", err.message) + rl.resume() + resolve(false) + }) + + serverProcess.on("close", () => + { + if (!hadError) + { + console.log("\n Server stopped.") + rl.resume() + resolve(true) + } + }) + }) +} + +const DeletePostGenerate = async () => +{ + console.log(" Cleaning up server-generated files...") + + let removed = 0 + for (const item of deleteList) + { + const itemPath = path.join(serverDir, item) + + try + { + if (await fs.pathExists(itemPath)) + { + await fs.remove(itemPath) + removed++ + } + } + catch (err) + { + console.error(` Failed to remove ${item}:`, err.message) + } + } + + console.log(` Removed ${removed} items.`) +} + +const CleanGenerated = async () => +{ + try + { + if (await fs.pathExists(serverDir)) + { + await fs.remove(serverDir) + console.log(" Removed server directory.") + } + else + { + console.log(" Server directory doesn't exist.") + } + } + catch (err) + { + console.error(" Failed to remove server directory:", err.message) + } +} + +const CleanAll = async () => +{ + // Placeholder - will be implemented later + console.log(" clean all - not implemented yet") +} + +const ListMods = () => +{ + console.log(`\n Mods for MC ${minecraft_version}:\n`) + for (const slug of modrinth_mods) + console.log(` - ${slug}`) + console.log(`\n Total: ${modrinth_mods.length} mods\n`) +} + +const Generate = async (rl) => +{ + await EnsureDirs() + + console.log(`\n Generating server for MC ${minecraft_version}...\n`) + + const { downloadedFiles, failedMods } = await DownloadModrinthMods(modrinth_mods, minecraft_version) + console.log(`\n ✅ Downloaded ${downloadedFiles.length} mods`) + if (failedMods.length) + console.log(` ❌ Failed: ${failedMods.join(", ")}`) + + const serverFile = await DownloadFabricServer(minecraft_version, fabric_loader_version, loader_meta_version) + + if (serverFile) + { + console.log("") + await RunServerAutoStop(rl) + await DeletePostGenerate() + } + + console.log("") +} + +const Merge = async () => +{ + // Placeholder - will be implemented later + console.log(" merge - not implemented yet") +} + +const ShowHelp = () => +{ + console.log(` + Commands: + + list-mods List configured mods + generate Download mods + server, run to generate files + merge (coming soon) + clean generated Remove generated server files + clean all (coming soon) + help Show this help + exit Exit +`) +} + +const ProcessCommand = async (input, rl) => +{ + const parts = input.trim().toLowerCase().split(/\s+/) + const cmd = parts[0] + const arg = parts[1] + + switch (cmd) + { + case "list-mods": + ListMods() + break + + case "generate": + await Generate(rl) + break + + case "merge": + await Merge() + break + + case "clean": + if (arg === "generated") + await CleanGenerated() + else if (arg === "all") + await CleanAll() + else + console.log(" Usage: clean generated | clean all") + break + + case "help": + case "?": + ShowHelp() + break + + case "exit": + case "quit": + case "q": + console.log(" Goodbye!\n") + process.exit(0) + + case "": + break + + default: + console.log(` Unknown command: ${cmd}. Type 'help' for commands.`) + } +} + +const DrawBox = (lines) => +{ + const maxLen = Math.max(...lines.map(l => l.length)) + const pad = (str) => str + " ".repeat(maxLen - str.length) + + const top = "╔" + "═".repeat(maxLen + 2) + "╗" + const bot = "╚" + "═".repeat(maxLen + 2) + "╝" + const mid = lines.map(l => "║ " + pad(l) + " ║").join("\n ") + + return ` ${top}\n ${mid}\n ${bot}` +} + +const Main = () => +{ + const title = `BENSON CLI v${pkg.version}` + const subtitle = `Fabric Server for MC ${minecraft_version}` + + const box = DrawBox([title, subtitle]) + + console.log(` +${box} + + Type 'help' for commands. +`) + + const rl = readline.createInterface + ({ + input: process.stdin, + output: process.stdout + }) + + const Prompt = () => + { + rl.question("benson> ", async (input) => + { + await ProcessCommand(input, rl) + Prompt() + }) + } + + rl.on("close", () => + { + console.log("\n Goodbye!\n") + process.exit(0) + }) + + Prompt() +} + +Main() |
