diff options
| author | hachem <im@hachem.wtf> | 2025-12-09 23:02:55 +0100 |
|---|---|---|
| committer | hachem <im@hachem.wtf> | 2025-12-09 23:02:55 +0100 |
| commit | 5cc4c5a689e124e3951c80f0cfda0de4a797fec7 (patch) | |
| tree | 7cb412c67f54636a4926161f4c948b68d87e74e8 /cli | |
| parent | 50e62e246fef7975866c244e08ead7c681d50349 (diff) | |
[add]: merging and refactor
Diffstat (limited to 'cli')
| -rw-r--r-- | cli/app.js | 138 | ||||
| -rw-r--r-- | cli/commands/check.js | 81 | ||||
| -rw-r--r-- | cli/commands/clean.js | 80 | ||||
| -rw-r--r-- | cli/commands/generate.js | 237 | ||||
| -rw-r--r-- | cli/commands/list.js | 15 | ||||
| -rw-r--r-- | cli/commands/merge.js | 156 |
6 files changed, 707 insertions, 0 deletions
diff --git a/cli/app.js b/cli/app.js new file mode 100644 index 0000000..1252358 --- /dev/null +++ b/cli/app.js @@ -0,0 +1,138 @@ +#!/usr/bin/env node + +const readline = require("readline") + +const { LoadConfig } = require("../lib/config") + +const { Check } = require("./commands/check") +const { Generate } = require("./commands/generate") +const { Merge } = require("./commands/merge") +const { CleanGenerated, CleanLatest, CleanAll } = require("./commands/clean") +const { ListMods } = require("./commands/list") + +const pkg = require("../package.json") +const config = LoadConfig() +const { minecraft_version } = config + +const ShowHelp = () => +{ + console.log(` + Commands: + + list-mods List configured mods + check Check if all mods are compatible before generating + generate Download mods + server, run to generate files + merge Backup server, move generated files to server location + clean generated Remove server directory + clean latest Remove latest backup zip + clean all Remove both generated and latest backup + 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 "check": + await Check() + break + + case "generate": + await Generate(rl) + break + + case "merge": + await Merge() + break + + case "clean": + if (arg === "generated") + await CleanGenerated() + else if (arg === "latest") + await CleanLatest() + else if (arg === "all") + await CleanAll() + else + console.log(" Usage: clean generated | clean latest | 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() + diff --git a/cli/commands/check.js b/cli/commands/check.js new file mode 100644 index 0000000..f91b710 --- /dev/null +++ b/cli/commands/check.js @@ -0,0 +1,81 @@ +const { LoadConfig } = require("../../lib/config") +const { GetModVersions, GetCompatibleVersion } = require("../../lib/modrinth") + +const config = LoadConfig() +const { minecraft_version, modrinth_mods } = config + +const Check = async () => +{ + console.log(`\n Checking mod compatibility for MC ${minecraft_version} (Fabric)...\n`) + + const compatible = [] + const incompatible = [] + const failed = [] + + for (const slug of modrinth_mods) + { + try + { + const versions = await GetModVersions(slug) + if (!versions) + { + failed.push(slug) + continue + } + + const match = GetCompatibleVersion(versions, minecraft_version, "fabric") + if (match) + { + const filename = match.files[0]?.filename || match.version_number + compatible.push({ slug, version: filename }) + } + else + { + incompatible.push(slug) + } + } + catch (err) + { + failed.push(slug) + } + } + + // Display results + if (compatible.length) + { + console.log(" [OK] Compatible:") + for (const { slug, version } of compatible) + console.log(` ${slug} -> ${version}`) + console.log("") + } + + if (incompatible.length) + { + console.log(" [FAIL] No compatible version:") + for (const slug of incompatible) + console.log(` ${slug}`) + console.log("") + } + + if (failed.length) + { + console.log(" [WARN] Failed to check:") + for (const slug of failed) + console.log(` ${slug}`) + console.log("") + } + + // Summary + const total = modrinth_mods.length + const ok = compatible.length + + console.log(` Summary: ${ok}/${total} mods compatible\n`) + + if (incompatible.length === 0 && failed.length === 0) + console.log(" [OK] Safe to generate!\n") + else + console.log(" [FAIL] Not all mods are compatible. Generate may have issues.\n") +} + +module.exports = { Check } + diff --git a/cli/commands/clean.js b/cli/commands/clean.js new file mode 100644 index 0000000..99a96f6 --- /dev/null +++ b/cli/commands/clean.js @@ -0,0 +1,80 @@ +const fs = require("fs-extra") +const path = require("path") + +const { LoadConfig } = require("../../lib/config") +const { serverDir } = require("./generate") + +const config = LoadConfig() +const { backup_location } = config + +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 CleanLatest = async () => +{ + if (!backup_location) + { + console.log(" Error: backup_location must be set in config.") + return + } + + if (!await fs.pathExists(backup_location)) + { + console.log(" Backup directory doesn't exist.") + return + } + + try + { + const files = await fs.readdir(backup_location) + const zipFiles = files + .filter(f => f.startsWith("backup_") && f.endsWith(".zip")) + .sort() + .reverse() + + if (zipFiles.length === 0) + { + console.log(" No backup files found.") + return + } + + const latestZip = zipFiles[0] + const latestPath = path.join(backup_location, latestZip) + + await fs.remove(latestPath) + console.log(` Removed latest backup: ${latestZip}`) + } + catch (err) + { + console.error(" Failed to remove latest backup:", err.message) + } +} + +const CleanAll = async () => +{ + console.log(" Cleaning generated files...") + await CleanGenerated() + console.log("") + console.log(" Cleaning latest backup...") + await CleanLatest() +} + +module.exports = { CleanGenerated, CleanLatest, CleanAll } + diff --git a/cli/commands/generate.js b/cli/commands/generate.js new file mode 100644 index 0000000..183810d --- /dev/null +++ b/cli/commands/generate.js @@ -0,0 +1,237 @@ +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 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 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 [OK] Downloaded ${downloadedFiles.length} mods`) + if (failedMods.length) + console.log(` [FAIL] 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("") +} + +module.exports = { Generate, serverDir } + diff --git a/cli/commands/list.js b/cli/commands/list.js new file mode 100644 index 0000000..48bb2cf --- /dev/null +++ b/cli/commands/list.js @@ -0,0 +1,15 @@ +const { LoadConfig } = require("../../lib/config") + +const config = LoadConfig() +const { minecraft_version, modrinth_mods } = config + +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`) +} + +module.exports = { ListMods } + diff --git a/cli/commands/merge.js b/cli/commands/merge.js new file mode 100644 index 0000000..138dd54 --- /dev/null +++ b/cli/commands/merge.js @@ -0,0 +1,156 @@ +const fs = require("fs-extra") +const path = require("path") +const { spawn } = require("child_process") +const archiver = require("archiver") + +const { LoadConfig } = require("../../lib/config") +const { serverDir } = require("./generate") + +const config = LoadConfig() + +const +{ + server_location, + backup_location, + merge_pre_command, + merge_post_command +} = config + +const RunCommand = (command) => +{ + return new Promise((resolve) => + { + console.log(` Running: ${command}`) + + const proc = spawn(command, [], { shell: true, stdio: "inherit" }) + + proc.on("close", (code) => + { + if (code === 0) + console.log(" Command completed.") + else + console.log(` Command exited with code ${code}`) + resolve(code) + }) + + proc.on("error", (err) => + { + console.error(" Command failed:", err.message) + resolve(1) + }) + }) +} + +const CreateBackup = (sourceDir, backupDir) => +{ + return new Promise(async (resolve, reject) => + { + const now = new Date() + const timestamp = now.toISOString() + .replace(/[:.]/g, "-") + .replace("T", "_") + .slice(0, 19) + + const zipName = `backup_${timestamp}.zip` + const zipPath = path.join(backupDir, zipName) + + await fs.ensureDir(backupDir) + + const output = fs.createWriteStream(zipPath) + const archive = archiver("zip", { zlib: { level: 9 } }) + + output.on("close", () => + { + console.log(` Created backup: ${zipName} (${archive.pointer()} bytes)`) + resolve(zipPath) + }) + + archive.on("error", err => reject(err)) + + archive.pipe(output) + archive.directory(sourceDir, false) + archive.finalize() + }) +} + +const Merge = async () => +{ + if (!server_location || !backup_location) + { + console.log(" Error: server_location and backup_location must be set in config.") + return + } + + if (!await fs.pathExists(serverDir)) + { + console.log(" Error: No generated files found. Run 'generate' first.") + return + } + + // Run pre-command + if (merge_pre_command) + { + console.log("\n Running pre-merge command...") + await RunCommand(merge_pre_command) + console.log("") + } + + // Check if server location exists and back it up + if (await fs.pathExists(server_location)) + { + console.log(` Backing up ${server_location}...`) + try + { + await CreateBackup(server_location, backup_location) + } + catch (err) + { + console.error(" Failed to create backup:", err.message) + return + } + } + else + { + console.log(" Server location doesn't exist, creating it...") + await fs.ensureDir(server_location) + } + + // Move generated files to server location + console.log(" Moving generated files to server location...") + try + { + const items = await fs.readdir(serverDir) + for (const item of items) + { + const src = path.join(serverDir, item) + const dest = path.join(server_location, item) + + // Remove existing item at destination + if (await fs.pathExists(dest)) + await fs.remove(dest) + + await fs.move(src, dest) + console.log(` Moved: ${item}`) + } + + // Remove the now-empty server directory + await fs.remove(serverDir) + + console.log(" Merge complete!") + } + catch (err) + { + console.error(" Failed to move files:", err.message) + return + } + + // Run post-command + if (merge_post_command) + { + console.log("\n Running post-merge command...") + await RunCommand(merge_post_command) + } +} + +module.exports = { Merge } + |
