diff options
| author | hachem <im@hachem.wtf> | 2026-08-29 11:26:09 +0200 |
|---|---|---|
| committer | hachem <im@hachem.wtf> | 2026-08-29 11:26:09 +0200 |
| commit | 4806ee7ef95f0f04079856f7753399b7b7aa6330 (patch) | |
| tree | 5766e1f8cd714ae76b2f81da4ab3045e7193b256 /internal | |
| parent | d8b2d27b1f1ccccfe26fe0d45dbb624d453272d3 (diff) | |
feat: recommended option filters
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/bot/bot.go | 4 | ||||
| -rw-r--r-- | internal/cli/cli.go | 16 | ||||
| -rw-r--r-- | internal/fabric/fabric.go | 79 | ||||
| -rw-r--r-- | internal/setup/setup.go | 84 |
4 files changed, 168 insertions, 15 deletions
diff --git a/internal/bot/bot.go b/internal/bot/bot.go index 4fb8fc8..48e395b 100644 --- a/internal/bot/bot.go +++ b/internal/bot/bot.go @@ -152,8 +152,8 @@ func checkMods(cfg *config.Config, client *modrinth.Client) report { } const ( - colorGood = 0x22C55E // green — everything compatible - colorWarn = 0xEF4444 // red — something has no compatible build + colorGood = 0x22C55E + colorWarn = 0xEF4444 ) func buildEmbed(cfg *config.Config, rep report) *discordgo.MessageEmbed { diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 7e7705e..4f98b6d 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -11,6 +11,7 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/hachem-wtf/benson/internal/config" + "github.com/hachem-wtf/benson/internal/setup" ) type command struct { @@ -42,10 +43,19 @@ type model struct { func Run(version string) error { cfg, err := config.Load() if err != nil { - if os.IsNotExist(err) { - return fmt.Errorf("no config found — run `benson setup` to create one") + if !os.IsNotExist(err) { + return fmt.Errorf("failed to load %s: %w", config.Path(), err) + } + + fmt.Println("No configuration found — let's set one up.") + if err := setup.Run(); err != nil { + return err + } + + cfg, err = config.Load() + if err != nil { + return fmt.Errorf("setup did not complete — run `benson setup` to configure") } - return fmt.Errorf("failed to load %s: %w", config.Path(), err) } _, err = tea.NewProgram(newModel(cfg, version)).Run() diff --git a/internal/fabric/fabric.go b/internal/fabric/fabric.go new file mode 100644 index 0000000..3e52e6f --- /dev/null +++ b/internal/fabric/fabric.go @@ -0,0 +1,79 @@ +package fabric + +import ( + "encoding/json" + "fmt" + "net/http" + "time" +) + +const ( + apiBase = "https://meta.fabricmc.net/v2" + userAgent = "benson/2.0 (github.com/hachem-wtf/benson)" +) + +type GameVersion struct { + Version string `json:"version"` + Stable bool `json:"stable"` +} + +type LoaderVersion struct { + Version string `json:"version"` + Stable bool `json:"stable"` +} + +type InstallerVersion struct { + Version string `json:"version"` + Stable bool `json:"stable"` +} + +type Client struct { + http *http.Client +} + +func NewClient() *Client { + return &Client{http: &http.Client{Timeout: 30 * time.Second}} +} + +func (c *Client) get(path string, out any) error { + req, err := http.NewRequest(http.MethodGet, apiBase+path, nil) + if err != nil { + return err + } + req.Header.Set("User-Agent", userAgent) + + resp, err := c.http.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("fabric: %s returned %s", path, resp.Status) + } + return json.NewDecoder(resp.Body).Decode(out) +} + +func (c *Client) GameVersions() ([]GameVersion, error) { + var versions []GameVersion + if err := c.get("/versions/game", &versions); err != nil { + return nil, err + } + return versions, nil +} + +func (c *Client) LoaderVersions() ([]LoaderVersion, error) { + var versions []LoaderVersion + if err := c.get("/versions/loader", &versions); err != nil { + return nil, err + } + return versions, nil +} + +func (c *Client) InstallerVersions() ([]InstallerVersion, error) { + var versions []InstallerVersion + if err := c.get("/versions/installer", &versions); err != nil { + return nil, err + } + return versions, nil +} diff --git a/internal/setup/setup.go b/internal/setup/setup.go index eafaaa0..c2f8c10 100644 --- a/internal/setup/setup.go +++ b/internal/setup/setup.go @@ -12,10 +12,9 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/hachem-wtf/benson/internal/config" + "github.com/hachem-wtf/benson/internal/fabric" ) -// 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() @@ -32,6 +31,8 @@ func Run() error { confirm = true ) + games, loaders, installers := fetchFabricVersions() + form := huh.NewForm( huh.NewGroup( huh.NewNote(). @@ -39,12 +40,16 @@ func Run() error { 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"), + versionField("Minecraft version", "The Minecraft release your server runs — newest stable is recommended", games, &mcVersion, "1.21.10"), + ), + huh.NewGroup( + versionField("Fabric loader version", "The Fabric mod loader that boots your mods — newest is recommended", loaders, &fabricLoader, "0.17.3"), + ), + huh.NewGroup( + versionField("Fabric installer version", "Builds the server jar from the loader — newest is recommended", installers, &loaderMeta, "1.1.0"), + ), huh.NewGroup( + huh.NewInput().Title("Java path").Description("Leave blank to use 'java' from PATH").Value(&javaPath), 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), @@ -81,8 +86,6 @@ func Run() error { 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() @@ -136,6 +139,68 @@ func RunBot() error { return nil } +func fetchFabricVersions() (games, loaders, installers []string) { + client := fabric.NewClient() + + if gv, err := client.GameVersions(); err == nil { + for _, v := range gv { + if v.Stable { + games = append(games, v.Version) + } + } + } + if lv, err := client.LoaderVersions(); err == nil { + for _, v := range lv { + loaders = append(loaders, v.Version) + } + } + if iv, err := client.InstallerVersions(); err == nil { + for _, v := range iv { + installers = append(installers, v.Version) + } + } + return games, loaders, installers +} + +func versionField(title, desc string, options []string, value *string, placeholder string) huh.Field { + if len(options) == 0 { + return huh.NewInput(). + Title(title). + Placeholder(placeholder). + Value(value). + Validate(required(title)) + } + + *value = defaultOr(*value, options) + return huh.NewSelect[string](). + Title(title). + Description(desc). + Options(recommendedOptions(options)...). + Height(12). + Value(value) +} + +func recommendedOptions(versions []string) []huh.Option[string] { + options := make([]huh.Option[string], len(versions)) + for i, v := range versions { + label := v + if i == 0 { + label = v + " · recommended" + } + options[i] = huh.NewOption(label, v) + } + return options +} + +func defaultOr(current string, options []string) string { + for _, o := range options { + if o == current { + return current + } + } + return options[0] +} + func loadConfig() *config.Config { if cfg, err := config.Load(); err == nil { return cfg @@ -150,7 +215,6 @@ func loadAPI() *config.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) { |
