hachem.wtf
aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-08-29 02:17:12 +0200
committerhachem <im@hachem.wtf>2026-08-29 02:17:12 +0200
commite56d21fa04450d118f76347a96a12d1d5477958f (patch)
tree06d590ef64153e2ab92306a4a7cc63d6fef47286
parent43d92fd60d75c4a1260757c75b1e1eefc0ea8952 (diff)
setup: prepare go workspace
-rw-r--r--.gitignore160
-rw-r--r--README.md60
-rw-r--r--bot/app.js162
-rw-r--r--cli/app.js138
-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
-rw-r--r--config/api.example.json3
-rw-r--r--config/config.example.json (renamed from config/default.json)8
-rw-r--r--go.mod3
-rw-r--r--internal/bot/bot.go13
-rw-r--r--internal/cli/cli.go12
-rw-r--r--lib/config.js62
-rw-r--r--lib/modrinth.js69
-rw-r--r--main.go64
-rw-r--r--package.json30
18 files changed, 119 insertions, 1234 deletions
diff --git a/.gitignore b/.gitignore
index 0b238f8..296d50d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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/
diff --git a/README.md b/README.md
index 8ba948a..7777024 100644
--- a/README.md
+++ b/README.md
@@ -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"
+ ]
}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..5968163
--- /dev/null
+++ b/go.mod
@@ -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
-}
-
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..cd9c607
--- /dev/null
+++ b/main.go
@@ -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"
- }
-}