diff options
| author | hachem <im@hachem.wtf> | 2026-08-29 02:17:12 +0200 |
|---|---|---|
| committer | hachem <im@hachem.wtf> | 2026-08-29 02:17:12 +0200 |
| commit | e56d21fa04450d118f76347a96a12d1d5477958f (patch) | |
| tree | 06d590ef64153e2ab92306a4a7cc63d6fef47286 | |
| parent | 43d92fd60d75c4a1260757c75b1e1eefc0ea8952 (diff) | |
setup: prepare go workspace
| -rw-r--r-- | .gitignore | 160 | ||||
| -rw-r--r-- | README.md | 60 | ||||
| -rw-r--r-- | bot/app.js | 162 | ||||
| -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 | ||||
| -rw-r--r-- | config/api.example.json | 3 | ||||
| -rw-r--r-- | config/config.example.json (renamed from config/default.json) | 8 | ||||
| -rw-r--r-- | go.mod | 3 | ||||
| -rw-r--r-- | internal/bot/bot.go | 13 | ||||
| -rw-r--r-- | internal/cli/cli.go | 12 | ||||
| -rw-r--r-- | lib/config.js | 62 | ||||
| -rw-r--r-- | lib/modrinth.js | 69 | ||||
| -rw-r--r-- | main.go | 64 | ||||
| -rw-r--r-- | package.json | 30 |
18 files changed, 119 insertions, 1234 deletions
@@ -1,145 +1,21 @@ -config/api.json -cache/ -server/ +# Binaries +/benson +/benson.exe +*.exe +*.test +*.out -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* +# Go build/test cache & coverage +/dist/ +coverage.out -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json +# Local runtime state (created by benson) +/config/config.json +/config/api.json +/cache/ +/server/ -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variable files -.env -.env.* -!.env.example - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# vuepress v2.x temp and cache directory -.temp -.cache - -# Sveltekit cache directory -.svelte-kit/ - -# vitepress build output -**/.vitepress/dist - -# vitepress cache directory -**/.vitepress/cache - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# Firebase cache directory -.firebase/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v3 -.pnp.* -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/sdks -!.yarn/versions - -# Vite logs files -vite.config.js.timestamp-* -vite.config.ts.timestamp-* - -package-lock.json
\ No newline at end of file +# Editor / OS +.DS_Store +.idea/ +.vscode/ @@ -5,65 +5,5 @@ Benson started as a little server manager for the Minecraft server a few friends It's probably way more complicated than it needs to be, but that's kind of the point. The only reason this is public is because a friend of mine told me it might be useful for other people. -> [!WARNING] -> This is currently held together with duct tape and Node.js. I plan to rewrite the entire thing in Go properly at some point. - -## Setup -Install the Node dependencies with `npm i` and create the configuration files under `config/`. - -```json -# `config/config.json` -{ - "channel_id": "YOUR_CHANNEL_ID_HERE", - "check_interval_minutes": 60, - "minecraft_version": "1.21.10", - "fabric_loader_version": "0.17.3", - "loader_meta_version": "1.1.0", - "java_path": null, - "server_location": "C:/path/to/your/server", - "backup_location": "C:/path/to/backups", - "modrinth_mods": [ - "fabric-api", - "lithium", - "simple-voice-chat" - ] -} - -# `config/api.json` -{ - "token": "your-bot-token-here" -} -``` - -## Discord bot -The Discord bot periodically checks the configured mods on Modrinth and posts compatibility reports and update information to a given Discord channel. - -The bot runs in a separate process so it can be hosted on a server independently from the server manager. Just run: - -```bash -npm run bot -``` - -## Server manager -To interact with Benson, use the interactive CLI: - -```bash -npm run benson -``` - -The CLI provides the following commands: - -| Command | Description | -| ----------------- | ----------------------------------------------------------------- | -| `list-mods` | List the configured mods | -| `check` | Check whether the configured mods are compatible | -| `generate` | Download the server and mods and generate a server directory | -| `merge` | Back up the existing server and merge the generated files into it | -| `clean generated` | Remove the generated server directory | -| `clean latest` | Remove the latest backup | -| `clean all` | Remove both generated files and the latest backup | -| `help` | Show available commands | -| `exit` | Exit Benson | - ## License Benson is licensed under the Apache License 2.0. See [LICENSE](LICENSE) for more details. diff --git a/bot/app.js b/bot/app.js deleted file mode 100644 index 59219eb..0000000 --- a/bot/app.js +++ /dev/null @@ -1,162 +0,0 @@ -const -{ - Client, - Events, - GatewayIntentBits -} = require("discord.js") - -const { LoadConfig, LoadApi, LoadCache, SaveCache } = require("../lib/config") -const { GetModVersions, GetLatestVersion, GetCompatibleVersion } = require("../lib/modrinth") - -const config = LoadConfig() -const api = LoadApi() - -const -{ - channel_id, - check_interval_minutes, - minecraft_version, - modrinth_mods -} = config - -const { token } = api - -const client = new Client -({ - intents: - [ - GatewayIntentBits.Guilds, - GatewayIntentBits.GuildMessages - ] -}) - -const CheckMods = async () => -{ - const cache = await LoadCache() - const newCache = { ...cache } - - const noCompatible = [] - const newlyCompatible = [] - const stillCompatibleUpdate = [] - - for (const slug of modrinth_mods) - { - const versions = await GetModVersions(slug) - if (!versions) - continue - - const latest = GetLatestVersion(versions) - const compatible = GetCompatibleVersion(versions, minecraft_version) - - const cached = cache[slug] || - { - compatible: null, - latest: null - } - - newCache[slug] = - { - compatible: compatible ? compatible.id : null, - latest: latest ? latest.id : null - } - - if (!compatible) - { - noCompatible.push(slug) - continue - } - - if (!cached.compatible && compatible) - { - newlyCompatible.push(slug) - continue - } - - if (latest && latest.id !== cached.latest) - stillCompatibleUpdate.push(slug) - } - - await SaveCache(newCache) - - return { noCompatible, newlyCompatible, stillCompatibleUpdate } -} - -const SendUpdate = async (channel, noCompatible, newlyCompatible, stillCompatibleUpdate) => -{ - if (!noCompatible.length && !newlyCompatible.length && !stillCompatibleUpdate.length) - return - - let msg = `**Modrinth Compatibility — ${minecraft_version}**\n\n` - - if (noCompatible.length) - { - msg += `**❌ No compatible version available:**\n` - for (const slug of noCompatible) - msg += `❌ ${slug}\n` - msg += `\n` - } - - if (newlyCompatible.length) - { - msg += `**🎉 Now Compatible:**\n` - for (const slug of newlyCompatible) - msg += `🎉 ${slug}\n` - msg += `\n` - } - - if (stillCompatibleUpdate.length) - { - msg += `**🟢 Updated (still compatible):**\n` - for (const slug of stillCompatibleUpdate) - msg += `🟢 ${slug}\n` - msg += `\n` - } - - await channel.send - ({ - content: msg, - allowedMentions: { parse: [] } - }) - - console.log("[Discord] Update message sent.") -} - -client.once(Events.ClientReady, async c => -{ - console.log(`[OK] Logged in as ${c.user.tag}`) - - let channel = null - try - { - channel = await client.channels.fetch(channel_id) - console.log("Fetched channel:", channel.name, "type:", channel.type) - } - catch - { - console.error("❌ Failed to fetch channel:", channel_id) - return - } - - const - { - noCompatible, - newlyCompatible, - stillCompatibleUpdate - } = await CheckMods() - - await SendUpdate(channel, noCompatible, newlyCompatible, stillCompatibleUpdate) - - setInterval(async () => - { - const - { - noCompatible, - newlyCompatible, - stillCompatibleUpdate - } = await CheckMods() - - await SendUpdate(channel, noCompatible, newlyCompatible, stillCompatibleUpdate) - }, check_interval_minutes * 60 * 1000) -}) - -client.login(token) diff --git a/cli/app.js b/cli/app.js deleted file mode 100644 index 1252358..0000000 --- a/cli/app.js +++ /dev/null @@ -1,138 +0,0 @@ -#!/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 deleted file mode 100644 index f91b710..0000000 --- a/cli/commands/check.js +++ /dev/null @@ -1,81 +0,0 @@ -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 deleted file mode 100644 index 99a96f6..0000000 --- a/cli/commands/clean.js +++ /dev/null @@ -1,80 +0,0 @@ -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 deleted file mode 100644 index 183810d..0000000 --- a/cli/commands/generate.js +++ /dev/null @@ -1,237 +0,0 @@ -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 deleted file mode 100644 index 48bb2cf..0000000 --- a/cli/commands/list.js +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index 138dd54..0000000 --- a/cli/commands/merge.js +++ /dev/null @@ -1,156 +0,0 @@ -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 } - diff --git a/config/api.example.json b/config/api.example.json new file mode 100644 index 0000000..df9b602 --- /dev/null +++ b/config/api.example.json @@ -0,0 +1,3 @@ +{ + "token": "your-discord-bot-token-here" +} diff --git a/config/default.json b/config/config.example.json index e7c0844..647f2e7 100644 --- a/config/default.json +++ b/config/config.example.json @@ -1,5 +1,5 @@ { - "channel_id": "[channel]", + "channel_id": "YOUR_CHANNEL_ID_HERE", "check_interval_minutes": 60, "minecraft_version": "1.21.10", "fabric_loader_version": "0.17.3", @@ -11,5 +11,9 @@ "merge_pre_command": null, "merge_post_command": null, - "modrinth_mods": [ ] + "modrinth_mods": [ + "fabric-api", + "lithium", + "simple-voice-chat" + ] } @@ -0,0 +1,3 @@ +module github.com/le-hachem/benson + +go 1.26.6 diff --git a/internal/bot/bot.go b/internal/bot/bot.go new file mode 100644 index 0000000..31c776d --- /dev/null +++ b/internal/bot/bot.go @@ -0,0 +1,13 @@ +// Package bot implements Benson's Discord bot, which periodically checks +// configured Modrinth mods for compatibility and posts reports to a channel. +package bot + +import "fmt" + +// Run starts the Discord bot and blocks until it is stopped. +// +// TODO: port the Modrinth compatibility checker and Discord integration. +func Run() error { + fmt.Println("benson bot — not yet implemented") + return nil +} diff --git a/internal/cli/cli.go b/internal/cli/cli.go new file mode 100644 index 0000000..337cb71 --- /dev/null +++ b/internal/cli/cli.go @@ -0,0 +1,12 @@ +// Package cli implements Benson's interactive server-manager REPL. +package cli + +import "fmt" + +// Run starts the interactive CLI. version is displayed in the banner. +// +// TODO: port the REPL and commands (list-mods, check, generate, merge, clean). +func Run(version string) error { + fmt.Printf("benson CLI v%s — not yet implemented\n", version) + return nil +} diff --git a/lib/config.js b/lib/config.js deleted file mode 100644 index 9d2ed39..0000000 --- a/lib/config.js +++ /dev/null @@ -1,62 +0,0 @@ -const path = require("path") -const fs = require("fs-extra") - -const configPath = path.join(__dirname, "../config/config.json") -const apiPath = path.join(__dirname, "../config/api.json") -const cachePath = path.join(__dirname, "../cache/mod_versions.json") - -const LoadConfig = () => -{ - return require(configPath) -} - -const LoadApi = () => -{ - try - { - return require(apiPath) - } - catch - { - return {} - } -} - -const LoadCache = async () => -{ - try - { - if (await fs.pathExists(cachePath)) - return JSON.parse(await fs.readFile(cachePath, "utf8")) - } - catch (err) - { - console.error("[Cache] Failed to load:", err) - } - return {} -} - -const SaveCache = async cache => -{ - try - { - await fs.ensureDir(path.dirname(cachePath)) - await fs.writeFile(cachePath, JSON.stringify(cache, null, 2)) - } - catch (err) - { - console.error("[Cache] Failed to save:", err) - } -} - -module.exports = -{ - configPath, - apiPath, - cachePath, - LoadConfig, - LoadApi, - LoadCache, - SaveCache -} - diff --git a/lib/modrinth.js b/lib/modrinth.js deleted file mode 100644 index cf0f697..0000000 --- a/lib/modrinth.js +++ /dev/null @@ -1,69 +0,0 @@ -const axios = require("axios") - -const MODRINTH_API = "https://api.modrinth.com/v2" - -const GetModVersions = async slug => -{ - try - { - const response = await axios.get(`${MODRINTH_API}/project/${slug}/version`) - return response.data - } - catch (err) - { - console.error(`[Modrinth] Failed for ${slug}:`, err.message) - return null - } -} - -const GetLatestVersion = versions => -{ - if (!versions || !versions.length) - return null - return versions.sort((a, b) => new Date(b.date_published) - new Date(a.date_published))[0] -} - -const GetCompatibleVersion = (versions, minecraftVersion, loader = null) => -{ - if (!versions || !versions.length) - return null - - const compatible = versions.filter(v => - { - const hasVersion = v.game_versions.includes(minecraftVersion) - const hasLoader = loader ? v.loaders.includes(loader) : true - return hasVersion && hasLoader - }) - - if (!compatible.length) - return null - - return compatible.sort((a, b) => new Date(b.date_published) - new Date(a.date_published))[0] -} - -const DownloadFile = async (url, destPath) => -{ - const fs = require("fs-extra") - - try - { - const response = await axios.get(url, { responseType: "arraybuffer" }) - await fs.writeFile(destPath, response.data) - return true - } - catch (err) - { - console.error(`[Download] Failed:`, err.message) - return false - } -} - -module.exports = -{ - MODRINTH_API, - GetModVersions, - GetLatestVersion, - GetCompatibleVersion, - DownloadFile -} - @@ -0,0 +1,64 @@ +// Command benson is a Minecraft server manager from hell. +// +// It is a single binary that exposes two modes: +// +// benson Start the interactive server-manager CLI +// benson bot Run the Discord bot +// +// Both modes share the same configuration and Modrinth client. +package main + +import ( + "fmt" + "os" + + "github.com/le-hachem/benson/internal/bot" + "github.com/le-hachem/benson/internal/cli" +) + +// version is the Benson release version. +const version = "1.1.0" + +func usage() { + fmt.Fprint(os.Stderr, `benson — a Minecraft server manager from hell + +Usage: + benson Start the interactive server-manager CLI + benson bot Run the Discord bot + benson version Print the version + benson help Show this help +`) +} + +func main() { + os.Exit(run(os.Args[1:])) +} + +func run(args []string) int { + if len(args) == 0 { + if err := cli.Run(version); err != nil { + fmt.Fprintln(os.Stderr, "error:", err) + return 1 + } + return 0 + } + + switch args[0] { + case "bot": + if err := bot.Run(); err != nil { + fmt.Fprintln(os.Stderr, "error:", err) + return 1 + } + return 0 + case "version", "--version", "-v": + fmt.Println("benson", version) + return 0 + case "help", "--help", "-h": + usage() + return 0 + default: + fmt.Fprintf(os.Stderr, "benson: unknown command %q\n\n", args[0]) + usage() + return 2 + } +} diff --git a/package.json b/package.json deleted file mode 100644 index ad276ad..0000000 --- a/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "benson", - "version": "1.1.0", - "description": "A server mod manager from hell.", - "homepage": "https://github.com/le-hachem/Benson#readme", - "bugs": { - "url": "https://github.com/le-hachem/Benson/issues" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/le-hachem/Benson.git" - }, - "license": "Apache-2.0", - "author": "hachem", - "type": "commonjs", - "main": "bot/app.js", - "scripts": { - "bot": "node bot/app.js", - "benson": "node cli/app.js" - }, - "dependencies": { - "archiver": "^7.0.1", - "axios": "^1.12.2", - "discord.js": "^14.23.2", - "fs-extra": "^11.3.2" - }, - "bin": { - "benson": "./cli/app.js" - } -} |
