package main import ( "fmt" "io" "log" "net/http" "os" "os/exec" "runtime" "strings" "github.com/charmbracelet/bubbles/progress" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/huh" "github.com/charmbracelet/lipgloss" "github.com/spf13/cobra" "github.com/spf13/viper" ) func getLinuxDistribution() (string, error) { data, err := os.ReadFile("/etc/os-release") if err != nil { return "", err } lines := strings.Split(string(data), "\n") for _, line := range lines { if strings.HasPrefix(line, "ID=") { return strings.Trim(strings.TrimPrefix(line, "ID="), "\""), nil } } return "", fmt.Errorf("Linux distribution not found") } func getPackageManager(os string) (string, error) { switch os { case "debian", "ubuntu": return "apt", nil case "arch": return "pacman", nil case "fedora": return "dnf", nil default: return "", fmt.Errorf("no packagemanager found for os: %s", os) } } func getInstallCommand(pm string) (string, error) { switch pm { case "apt": return "apt install -y", nil case "pacman": return "pacman -S --noconfirm", nil case "dnf": return "dnf install -y", nil default: return "", fmt.Errorf("no install command found for package manager: %s", pm) } } func installPackage(cmd, pkg string) error { fullCmd := fmt.Sprintf("%s %s", cmd, pkg) command := exec.Command("sudo", "sh", "-c", fullCmd) output, err := command.CombinedOutput() if err != nil { return fmt.Errorf("failed to install %s: %v\n%s", pkg, err, string(output)) } return nil } func downloadGolang(golangVersion string) error { var link string if runtime.GOARCH == "arm64" { link = "https://go.dev/dl/go" + golangVersion + ".linux-arm64.tar.gz" } else { link = "https://go.dev/dl/go" + golangVersion + ".linux-amd64.tar.gz" } resp, err := http.Get(link) if err != nil { return fmt.Errorf("Fehler beim Herunterladen von Go: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("Unerwarteter HTTP-Status: %s", resp.Status) } outFile, err := os.Create("go" + golangVersion + ".linux-" + runtime.GOARCH + ".tar.gz") if err != nil { return fmt.Errorf("Fehler beim Erstellen der Ausgabedatei: %v", err) } defer outFile.Close() _, err = io.Copy(outFile, resp.Body) if err != nil { return fmt.Errorf("Fehler beim Schreiben der Ausgabedatei: %v", err) } return nil } func installSpecialSoftware() error { if _, err := exec.LookPath("go"); err == nil { fmt.Println("Go ist bereits installiert.") } else { golangVersion := "1.23.4" if err := downloadGolang(golangVersion); err != nil { return fmt.Errorf("Fehler beim Herunterladen von Go: %v", err) } golangCommand := fmt.Sprintf("sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go%s.linux-%s.tar.gz", golangVersion, runtime.GOARCH) if err := installPackage(golangCommand, ""); err != nil { return err } } if _, err := exec.LookPath("rustc"); err == nil { fmt.Println("Rust ist bereits installiert.") } else { rustupCommand := "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -- -q -y" if err := installPackage(rustupCommand, ""); err != nil { return err } } if _, err := exec.LookPath("pipx"); err == nil { fmt.Println("Pipx ist bereits installiert.") } else { pipXCommand := "python3 -m pip install --user pipx && python3 -m pipx ensurepath" if err := installPackage(pipXCommand, ""); err != nil { return err } } return nil } type model struct { packages []string index int width int height int spinner spinner.Model progress progress.Model done bool } var ( currentPkgNameStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("211")) doneStyle = lipgloss.NewStyle().Margin(1, 2) checkMark = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).SetString("✓") ) func newModel(packages []string) model { p := progress.New( progress.WithDefaultGradient(), progress.WithWidth(40), progress.WithoutPercentage(), ) s := spinner.New() s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("63")) return model{ packages: packages, spinner: s, progress: p, } } func (m model) Init() tea.Cmd { return tea.Batch(installPackageCmd(m.packages[m.index]), m.spinner.Tick) } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: m.width, m.height = msg.Width, msg.Height case tea.KeyMsg: switch msg.String() { case "ctrl+c", "esc", "q": return m, tea.Quit } case installedPkgMsg: pkg := m.packages[m.index] if m.index >= len(m.packages)-1 { m.done = true return m, tea.Sequence( tea.Printf("%s %s", checkMark, pkg), tea.Quit, ) } m.index++ progressCmd := m.progress.SetPercent(float64(m.index) / float64(len(m.packages))) return m, tea.Batch( progressCmd, tea.Printf("%s %s", checkMark, pkg), installPackageCmd(m.packages[m.index]), ) case spinner.TickMsg: var cmd tea.Cmd m.spinner, cmd = m.spinner.Update(msg) return m, cmd case progress.FrameMsg: newModel, cmd := m.progress.Update(msg) if newModel, ok := newModel.(progress.Model); ok { m.progress = newModel } return m, cmd } return m, nil } func (m model) View() string { n := len(m.packages) w := lipgloss.Width(fmt.Sprintf("%d", n)) if m.done { return doneStyle.Render(fmt.Sprintf("Done! Installed %d packages.\n", n)) } pkgCount := fmt.Sprintf(" %*d/%*d", w, m.index, w, n) spin := m.spinner.View() + " " prog := m.progress.View() cellsAvail := max(0, m.width-lipgloss.Width(spin+prog+pkgCount)) pkgName := currentPkgNameStyle.Render(m.packages[m.index]) info := lipgloss.NewStyle().MaxWidth(cellsAvail).Render("Installing " + pkgName) cellsRemaining := max(0, m.width-lipgloss.Width(spin+info+prog+pkgCount)) gap := strings.Repeat(" ", cellsRemaining) return spin + info + gap + prog + pkgCount } type installedPkgMsg string func installPackageCmd(pkg string) tea.Cmd { return func() tea.Msg { if err := installPackage(installCommand, pkg); err != nil { log.Printf("Fehler beim Installieren von %s: %v", pkg, err) } return installedPkgMsg(pkg) } } func max(a, b int) int { if a > b { return a } return b } var rootCmd = &cobra.Command{ Use: "package-installer", Short: "Installiert Pakete basierend auf TOML-Konfiguration", Run: run, } type Packages struct { Headless []string `mapstructure:"headless"` NonHeadless []string `mapstructure:"non_headless"` } type SpecialPackages struct { Go []string `mapstructure:"go"` Cargo []string `mapstructure:"cargo"` Pipx []string `mapstructure:"pipx"` } type Config struct { Headless bool `mapstructure:"headless"` Packages Packages `mapstructure:"packages"` SpecialPackages SpecialPackages `mapstructure:"special_packages"` } var installCommand string func init() { cobra.OnInitialize(initConfig) rootCmd.PersistentFlags().StringP("config", "c", "", "Pfad zur Konfigurationsdatei") viper.BindPFlag("config", rootCmd.PersistentFlags().Lookup("config")) } func initConfig() { if cfgFile := viper.GetString("config"); cfgFile != "" { viper.SetConfigFile(cfgFile) } else { viper.SetConfigName("config") viper.AddConfigPath(".") } if err := viper.ReadInConfig(); err != nil { fmt.Println("Fehler beim Lesen der Konfigurationsdatei:", err) os.Exit(1) } } func installSpecialPackages(sp SpecialPackages) error { for _, pkg := range sp.Go { cmd := exec.Command("go", "install", pkg+"@latest") if err := cmd.Run(); err != nil { return fmt.Errorf("Fehler bei der Installation von %s: %v", pkg, err) } } for _, pkg := range sp.Cargo { cmd := exec.Command("cargo", "install", pkg) if err := cmd.Run(); err != nil { return fmt.Errorf("Fehler bei der Installation von %s: %v", pkg, err) } } for _, pkg := range sp.Pipx { cmd := exec.Command("pipx", "install", pkg) if err := cmd.Run(); err != nil { return fmt.Errorf("Fehler bei der Installation von %s: %v", pkg, err) } } return nil } func run(cmd *cobra.Command, args []string) { os, err := getLinuxDistribution() if err != nil { log.Fatal(err) } pm, err := getPackageManager(os) if err != nil { log.Fatal(err) } installCommand, err = getInstallCommand(pm) if err != nil { log.Fatal(err) } var cfg Config if err := viper.Unmarshal(&cfg); err != nil { log.Fatalf("Fehler beim Lesen der Konfiguration: %v", err) } form := huh.NewForm( huh.NewGroup( huh.NewConfirm(). Title("Möchten Sie eine headless Installation durchführen?"). Value(&cfg.Headless), ), ).WithTheme(huh.ThemeCatppuccin()) if err := form.Run(); err != nil { log.Fatalf("Fehler bei der Benutzerabfrage: %v", err) } var packages []string packages = append(packages, cfg.Packages.Headless...) if !cfg.Headless { packages = append(packages, cfg.Packages.NonHeadless...) } m := newModel(packages) p := tea.NewProgram(m) if _, err := p.Run(); err != nil { log.Fatal(err) } var installSpecial bool form = huh.NewForm( huh.NewGroup( huh.NewConfirm(). Title("Möchten Sie die speziellen Pakete (Go, Rust, Pipx) installieren?"). Value(&installSpecial), ), ).WithTheme(huh.ThemeCatppuccin()) err = form.Run() if err != nil { log.Fatal(err) } if installSpecial { if err := installSpecialSoftware(); err != nil { log.Printf("Fehler beim Installieren der speziellen Software: %v", err) } else { fmt.Println("Spezielle Software erfolgreich installiert") } } else { fmt.Println("Installation der speziellen Pakete übersprungen") } if err := installSpecialPackages(cfg.SpecialPackages); err != nil { log.Printf("Fehler bei der Installation spezieller Pakete: %v", err) } } func main() { if err := rootCmd.Execute(); err != nil { fmt.Println(err) os.Exit(1) } }