// motd-assist verwaltet dynamische MOTDs (Ubuntu/Debian, /etc/update-motd.d): // Presets wählen, Scripte schalten, Live-Vorschau, geführtes Anwenden per sudo. package main import ( "fmt" "os" "os/exec" "strconv" tea "github.com/charmbracelet/bubbletea" "motd-assist/internal/motd" "motd-assist/internal/preset" "motd-assist/internal/ui" ) const version = "0.1.0" func main() { root := os.Getenv("MOTD_ASSIST_ROOT") if root == "" { root = "/" } if len(os.Args) < 2 { runTUI(root) return } switch os.Args[1] { case "preview": cmdPreview(root) case "list": cmdList(root) case "presets": cmdPresets() case "apply": cmdApply(root) case "restore": cmdRestore(root) case "apply-state": // intern: wird über sudo aufgerufen cmdApplyState(root) case "version", "--version", "-v": fmt.Println("motd-assist", version) case "help", "--help", "-h": usage() default: fmt.Fprintln(os.Stderr, "unbekannter Befehl:", os.Args[1]) usage() os.Exit(1) } } func runTUI(root string) { p := tea.NewProgram(ui.New(root), tea.WithAltScreen()) if _, err := p.Run(); err != nil { fmt.Fprintln(os.Stderr, "TUI-Fehler:", err) os.Exit(1) } } func fatal(err error) { fmt.Fprintln(os.Stderr, "Fehler:", err) os.Exit(1) } func cmdPreview(root string) { width := 72 args := os.Args[2:] for i := 0; i < len(args); i++ { if (args[i] == "--width" || args[i] == "-w") && i+1 < len(args) { if n, err := strconv.Atoi(args[i+1]); err == nil { width = n } i++ } } scripts, err := motd.Discover(root) if err != nil { fatal(err) } preview := make([]motd.PreviewScript, 0, len(scripts)) for _, s := range scripts { preview = append(preview, motd.PreviewScript{Name: s.Name, Path: s.Path, Enabled: s.Enabled}) } fmt.Println(motd.Render(preview, width)) } func cmdList(root string) { host := motd.DetectHost(root) fmt.Printf("%s · %s · dynamischer MOTD: %s\n\n", host.Hostname, host.Distro, boolText(host.PAM)) scripts, err := motd.Discover(root) if err != nil { fatal(err) } fmt.Printf("%d Scripte, %d aktiv:\n", len(scripts), motd.EnabledCount(scripts)) for _, s := range scripts { mark := "○ aus" if s.Enabled { mark = "● an" } note := "" if s.Bundled { note = " (motd-assist)" } fmt.Printf(" %s %s%s\n", mark, s.Name, note) } } func boolText(b bool) string { if b { return "aktiv" } return "inaktiv" } func cmdPresets() { presets, err := preset.LoadAll() if err != nil { fatal(err) } for _, p := range presets { fmt.Printf(" %-18s %s (%s)\n", p.Name, p.Description, p.Source) } } func cmdApply(root string) { if len(os.Args) < 3 { fatal(fmt.Errorf("Usage: motd-assist apply ")) } name := os.Args[2] presets, err := preset.LoadAll() if err != nil { fatal(err) } var chosen *preset.Preset for i := range presets { if presets[i].Name == name { chosen = &presets[i] break } } if chosen == nil { fatal(fmt.Errorf("Preset %q nicht gefunden (siehe: motd-assist presets)", name)) } current, err := motd.Discover(root) if err != nil { fatal(err) } entries := make([]motd.DesiredEntry, 0, len(chosen.Scripts)) for _, e := range chosen.Scripts { entries = append(entries, motd.DesiredEntry{Name: e.Name, Enabled: e.Enabled, Install: e.Install}) } plan := motd.Diff(current, motd.DesiredFromPreset(current, entries)) if plan.Empty() { fmt.Println("System entspricht bereits dem Preset.") return } if motd.IsRoot() { if err := motd.Execute(root, plan); err != nil { fatal(err) } fmt.Println("Angewendet:", plan.Summary()) fmt.Println("Undo: motd-assist restore") return } planFile, err := plan.WriteFile() if err != nil { fatal(err) } defer os.Remove(planFile) if err := relaySudo("apply-state", planFile); err != nil { fatal(err) } } func cmdRestore(root string) { if motd.IsRoot() { if err := motd.Restore(root); err != nil { fatal(err) } fmt.Println("Letzter Apply wieder zurückgesetzt.") return } if err := relaySudo("restore"); err != nil { fatal(err) } } func cmdApplyState(root string) { if !motd.IsRoot() { fatal(fmt.Errorf("apply-state benötigt Root – bitte mit sudo starten")) } if len(os.Args) < 3 { fatal(fmt.Errorf("Usage: motd-assist apply-state ")) } plan, err := motd.ReadPlanFile(os.Args[2]) if err != nil { fatal(err) } if err := motd.Execute(root, plan); err != nil { fatal(err) } fmt.Println("Angewendet:", plan.Summary()) fmt.Println("Undo: motd-assist restore") } // relaySudo führt dieses Binary mit sudo und den Argumenten args aus // (Terminal-Ein-/Ausgabe bleibt erhalten, Passwortabfrage möglich). func relaySudo(args ...string) error { self, err := os.Executable() if err != nil { return err } sudo, err := exec.LookPath("sudo") if err != nil { return fmt.Errorf("sudo nicht gefunden: %w", err) } cmd := exec.Command(sudo, append([]string{self}, args...)...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() } func usage() { fmt.Print(`motd-assist ` + version + ` – MOTD-Verwaltung für /etc/update-motd.d Usage: motd-assist TUI starten (Standard) motd-assist preview [-w N] aktuellen MOTD rendern und ausgeben motd-assist list Scripte und Zustand auflisten motd-assist presets verfügbare Presets auflisten motd-assist apply Preset anwenden (fragt ggf. sudo-Passwort) motd-assist restore letzten Apply zurücksetzen motd-assist help diese Hilfe TUI-Kürzel: tab Panes wechseln · enter/space Preset laden/Script schalten · u/d Reihenfolge · a anwenden · s als Preset speichern · r neu einlesen · ? Hilfe `) }