hachem.wtf
aboutsummaryrefslogtreecommitdiff
path: root/internal/setup
diff options
context:
space:
mode:
Diffstat (limited to 'internal/setup')
-rw-r--r--internal/setup/setup.go224
1 files changed, 224 insertions, 0 deletions
diff --git a/internal/setup/setup.go b/internal/setup/setup.go
new file mode 100644
index 0000000..eafaaa0
--- /dev/null
+++ b/internal/setup/setup.go
@@ -0,0 +1,224 @@
+package setup
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "os"
+ "strconv"
+ "strings"
+
+ "github.com/charmbracelet/huh"
+ "github.com/charmbracelet/lipgloss"
+
+ "github.com/hachem-wtf/benson/internal/config"
+)
+
+// Run is the server-manager wizard. It configures Minecraft/Fabric versions,
+// paths, merge hooks and mods — everything except the Discord bot.
+func Run() error {
+ cfg := loadConfig()
+
+ var (
+ mcVersion = cfg.MinecraftVersion
+ fabricLoader = cfg.FabricLoaderVersion
+ loaderMeta = cfg.LoaderMetaVersion
+ javaPath = cfg.JavaPath
+ serverLoc = cfg.ServerLocation
+ backupLoc = cfg.BackupLocation
+ preCmd = cfg.MergePreCommand
+ postCmd = cfg.MergePostCommand
+ modsText = strings.Join(cfg.ModrinthMods, "\n")
+ confirm = true
+ )
+
+ form := huh.NewForm(
+ huh.NewGroup(
+ huh.NewNote().
+ Title("Benson setup").
+ Description("Configure the server manager.\nThe Discord bot is optional — run `benson setup bot` for that."),
+ ),
+ huh.NewGroup(
+ huh.NewInput().Title("Minecraft version").Placeholder("1.21.10").Value(&mcVersion).Validate(required("Minecraft version")),
+ huh.NewInput().Title("Fabric loader version").Placeholder("0.17.3").Value(&fabricLoader).Validate(required("Fabric loader version")),
+ huh.NewInput().Title("Loader / installer version").Placeholder("1.1.0").Value(&loaderMeta).Validate(required("loader version")),
+ huh.NewInput().Title("Java path").Description("Leave blank to use 'java' from PATH").Value(&javaPath),
+ ).Title("Minecraft / Fabric"),
+ huh.NewGroup(
+ huh.NewInput().Title("Server location").Description("Live server directory, merged into by 'merge'").Value(&serverLoc),
+ huh.NewInput().Title("Backup location").Description("Where server backups are written").Value(&backupLoc),
+ huh.NewInput().Title("Pre-merge command").Description("Optional shell command run before merging").Value(&preCmd),
+ huh.NewInput().Title("Post-merge command").Description("Optional shell command run after merging").Value(&postCmd),
+ ).Title("Paths & hooks (optional)"),
+ huh.NewGroup(
+ huh.NewText().Title("Modrinth mods").Description("One mod slug per line").Lines(8).Value(&modsText),
+ ).Title("Mods"),
+ huh.NewGroup(
+ huh.NewConfirm().Title("Write config/config.json?").Affirmative("Write").Negative("Cancel").Value(&confirm),
+ ),
+ )
+
+ if done, err := runForm(form); err != nil || !done {
+ return err
+ }
+
+ cfg.MinecraftVersion = strings.TrimSpace(mcVersion)
+ cfg.FabricLoaderVersion = strings.TrimSpace(fabricLoader)
+ cfg.LoaderMetaVersion = strings.TrimSpace(loaderMeta)
+ cfg.JavaPath = strings.TrimSpace(javaPath)
+ cfg.ServerLocation = strings.TrimSpace(serverLoc)
+ cfg.BackupLocation = strings.TrimSpace(backupLoc)
+ cfg.MergePreCommand = strings.TrimSpace(preCmd)
+ cfg.MergePostCommand = strings.TrimSpace(postCmd)
+ cfg.ModrinthMods = parseMods(modsText)
+
+ if err := writeConfigFile(cfg); err != nil {
+ return err
+ }
+
+ fmt.Printf("\n %s Wrote %s\n\n Run `benson` for the CLI, or `benson setup bot` to set up the Discord bot.\n\n",
+ check(), config.Path())
+ return nil
+}
+
+// RunBot is the Discord-bot wizard. It configures the bot token, channel and
+// check interval, leaving the server-manager settings untouched.
+func RunBot() error {
+ cfg := loadConfig()
+ api := loadAPI()
+
+ interval := cfg.CheckIntervalMinutes
+ if interval <= 0 {
+ interval = 60
+ }
+
+ var (
+ token = api.Token
+ channelID = cfg.ChannelID
+ intervalStr = strconv.Itoa(interval)
+ confirm = true
+ )
+
+ form := huh.NewForm(
+ huh.NewGroup(
+ huh.NewNote().
+ Title("Benson bot setup").
+ Description("Configure the Discord bot that posts Modrinth compatibility reports."),
+ ),
+ huh.NewGroup(
+ huh.NewInput().Title("Discord bot token").Description("Developer Portal → Bot → Reset/Copy Token").Password(true).Value(&token).Validate(required("token")),
+ huh.NewInput().Title("Channel ID").Description("Channel where reports are posted").Value(&channelID).Validate(required("channel ID")),
+ huh.NewInput().Title("Check interval (minutes)").Value(&intervalStr).Validate(positiveInt),
+ ).Title("Discord"),
+ huh.NewGroup(
+ huh.NewConfirm().Title("Write config/config.json and config/api.json?").Affirmative("Write").Negative("Cancel").Value(&confirm),
+ ),
+ )
+
+ if done, err := runForm(form); err != nil || !done {
+ return err
+ }
+
+ minutes, _ := strconv.Atoi(strings.TrimSpace(intervalStr))
+ cfg.ChannelID = strings.TrimSpace(channelID)
+ cfg.CheckIntervalMinutes = minutes
+ api.Token = strings.TrimSpace(token)
+
+ if err := writeConfigFile(cfg); err != nil {
+ return err
+ }
+ if err := writeAPIFile(api); err != nil {
+ return err
+ }
+
+ fmt.Printf("\n %s Wrote %s\n %s Wrote %s\n\n Run `benson bot` to start the bot.\n\n",
+ check(), config.Path(), check(), config.APIPath())
+ return nil
+}
+
+func loadConfig() *config.Config {
+ if cfg, err := config.Load(); err == nil {
+ return cfg
+ }
+ return &config.Config{}
+}
+
+func loadAPI() *config.API {
+ if api, err := config.LoadAPI(); err == nil {
+ return api
+ }
+ return &config.API{}
+}
+
+// runForm runs a themed form, reporting whether the user confirmed the write.
+func runForm(form *huh.Form) (bool, error) {
+ if err := form.WithTheme(emberTheme()).Run(); err != nil {
+ if errors.Is(err, huh.ErrUserAborted) {
+ fmt.Println("Setup cancelled.")
+ return false, nil
+ }
+ return false, err
+ }
+ return true, nil
+}
+
+func required(label string) func(string) error {
+ return func(s string) error {
+ if strings.TrimSpace(s) == "" {
+ return fmt.Errorf("%s is required", label)
+ }
+ return nil
+ }
+}
+
+func positiveInt(s string) error {
+ n, err := strconv.Atoi(strings.TrimSpace(s))
+ if err != nil || n <= 0 {
+ return errors.New("must be a positive number")
+ }
+ return nil
+}
+
+func parseMods(text string) []string {
+ mods := []string{}
+ for _, line := range strings.Split(text, "\n") {
+ if slug := strings.TrimSpace(line); slug != "" {
+ mods = append(mods, slug)
+ }
+ }
+ return mods
+}
+
+func check() string {
+ return lipgloss.NewStyle().Foreground(lipgloss.Color("#22C55E")).Render("✓")
+}
+
+func emberTheme() *huh.Theme {
+ theme := huh.ThemeCharm()
+ ember := lipgloss.Color("#F97316")
+ theme.Focused.Title = theme.Focused.Title.Foreground(ember).Bold(true)
+ theme.Focused.Base = theme.Focused.Base.BorderForeground(ember)
+ return theme
+}
+
+func writeConfigFile(cfg *config.Config) error {
+ if err := os.MkdirAll(config.Dir, 0o755); err != nil {
+ return err
+ }
+ data, err := json.MarshalIndent(cfg, "", " ")
+ if err != nil {
+ return err
+ }
+ return os.WriteFile(config.Path(), append(data, '\n'), 0o644)
+}
+
+func writeAPIFile(api *config.API) error {
+ if err := os.MkdirAll(config.Dir, 0o755); err != nil {
+ return err
+ }
+ data, err := json.MarshalIndent(api, "", " ")
+ if err != nil {
+ return err
+ }
+ return os.WriteFile(config.APIPath(), append(data, '\n'), 0o600)
+}