package cli import ( "bufio" "fmt" "io" "os" "os/exec" "path/filepath" "strings" tea "github.com/charmbracelet/bubbletea" "github.com/hachem-wtf/benson/internal/config" "github.com/hachem-wtf/benson/internal/modrinth" ) var ( serverDir = "server" modsDir = filepath.Join("server", "mods") ) var postGenerateDeleteList = []string{ "audioplayer_uploads", "config", "cache", "world", "logs", "banned-players.json", "server.properties", "banned-ips.json", "usercache.json", "whitelist.json", "ops.json", "eula.txt", } type streamMsg struct{ text string } type streamDoneMsg struct{ text string } func waitForEvent(ch chan tea.Msg) tea.Cmd { return func() tea.Msg { return <-ch } } func runGenerate(m *model) tea.Cmd { m.busy = true ch := make(chan tea.Msg, 256) m.events = ch cfg := m.cfg go generateWorker(cfg, ch) return tea.Batch(m.spin.Tick, waitForEvent(ch)) } func generateWorker(cfg *config.Config, ch chan<- tea.Msg) { emit := func(s string) { ch <- streamMsg{s} } emit(headingStyle.Render("Generating server for MC " + cfg.MinecraftVersion)) if err := os.MkdirAll(modsDir, 0o755); err != nil { ch <- streamDoneMsg{errorStyle.Render("Failed to create directories: " + err.Error())} return } client := modrinth.NewClient() downloaded, failed := 0, 0 for _, mod := range cfg.ModrinthMods { versions, err := client.GetVersions(mod.Slug) if err != nil { failed++ emit(" " + errorStyle.Render("✗ ") + mod.Slug + mutedStyle.Render(" — "+err.Error())) continue } var match *modrinth.Version if mod.Pinned() { match = modrinth.FindVersion(versions, mod.Version) } else { match = modrinth.CompatibleVersion(versions, cfg.MinecraftVersion, "fabric") } if match == nil || len(match.Files) == 0 { failed++ reason := "no fabric build for MC " + cfg.MinecraftVersion if mod.Pinned() { reason = "version " + mod.Version + " not found" } emit(" " + errorStyle.Render("✗ ") + mod.Slug + mutedStyle.Render(" — "+reason)) continue } file := match.Files[0] if err := client.Download(file.URL, filepath.Join(modsDir, file.Filename)); err != nil { failed++ emit(" " + errorStyle.Render("✗ ") + mod.Slug + mutedStyle.Render(" — "+err.Error())) continue } downloaded++ emit(" " + successStyle.Render("✓ ") + file.Filename) } emit(mutedStyle.Render(fmt.Sprintf("Downloaded %d mods · %d failed", downloaded, failed))) emit(headingStyle.Render("Downloading Fabric server")) serverURL := fmt.Sprintf( "https://meta.fabricmc.net/v2/versions/loader/%s/%s/%s/server/jar", cfg.MinecraftVersion, cfg.FabricLoaderVersion, cfg.LoaderMetaVersion, ) if err := client.Download(serverURL, filepath.Join(serverDir, "server.jar")); err != nil { ch <- streamDoneMsg{errorStyle.Render("Failed to download Fabric server: " + err.Error())} return } emit(" " + successStyle.Render("✓ ") + "server.jar") if err := runServerAutoStop(cfg, emit); err != nil { ch <- streamDoneMsg{errorStyle.Render("Server run failed: " + err.Error())} return } removed := deletePostGenerate() emit(mutedStyle.Render(fmt.Sprintf("Removed %d generated items", removed))) ch <- streamDoneMsg{successStyle.Render("Generation complete.")} } func runServerAutoStop(cfg *config.Config, emit func(string)) error { eulaPath := filepath.Join(serverDir, "eula.txt") if _, err := os.Stat(eulaPath); os.IsNotExist(err) { if err := os.WriteFile(eulaPath, []byte("eula=true\n"), 0o644); err != nil { return err } } java := cfg.JavaPath if java == "" { java = "java" } emit(headingStyle.Render("Starting server (auto-stops after loading)")) cmd := exec.Command(java, "-jar", "server.jar", "nogui") cmd.Dir = serverDir stdin, err := cmd.StdinPipe() if err != nil { return err } pr, pw, err := os.Pipe() if err != nil { return err } cmd.Stdout = pw cmd.Stderr = pw if err := cmd.Start(); err != nil { pw.Close() return err } pw.Close() scanner := bufio.NewScanner(pr) scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024) stopped := false for scanner.Scan() { line := scanner.Text() emit(mutedStyle.Render(" │ " + line)) if !stopped && strings.Contains(line, "Done") { stopped = true emit(successStyle.Render(" Server loaded, stopping…")) io.WriteString(stdin, "stop\n") } } stdin.Close() if err := cmd.Wait(); err != nil && !stopped { return err } emit(successStyle.Render(" Server stopped.")) return nil } func deletePostGenerate() int { removed := 0 for _, item := range postGenerateDeleteList { path := filepath.Join(serverDir, item) if _, err := os.Stat(path); err != nil { continue } if err := os.RemoveAll(path); err == nil { removed++ } } return removed }