feat: add motd-assist TUI for managing dynamic MOTDs
Initial Go project built with Bubble Tea and Lip Gloss: - Live preview and management of /etc/update-motd.d scripts - Embedded presets (ubuntu-default, minimal, server-dashboard) - Bundled POSIX sh style scripts (brand header, sysinfo panel, footer) - Snapshot/restore safety around applying changes - CLI subcommands preview, apply, restore, list-presets - Unit tests for detect, sanitize, and apply logic
This commit is contained in:
commit
d56659e21a
34 changed files with 3260 additions and 0 deletions
249
main.go
Normal file
249
main.go
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
// 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 <preset>"))
|
||||
}
|
||||
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 <plandatei>"))
|
||||
}
|
||||
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> 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
|
||||
`)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue