hachem.wtf
aboutsummaryrefslogtreecommitdiff
path: root/cli/commands
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2025-12-09 23:02:55 +0100
committerhachem <im@hachem.wtf>2025-12-09 23:02:55 +0100
commit5cc4c5a689e124e3951c80f0cfda0de4a797fec7 (patch)
tree7cb412c67f54636a4926161f4c948b68d87e74e8 /cli/commands
parent50e62e246fef7975866c244e08ead7c681d50349 (diff)
[add]: merging and refactor
Diffstat (limited to 'cli/commands')
-rw-r--r--cli/commands/check.js81
-rw-r--r--cli/commands/clean.js80
-rw-r--r--cli/commands/generate.js237
-rw-r--r--cli/commands/list.js15
-rw-r--r--cli/commands/merge.js156
5 files changed, 569 insertions, 0 deletions
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 }
+