motd-assist/internal/motd/script.go
Tronax d56659e21a
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
2026-08-24 14:19:28 +02:00

74 lines
2 KiB
Go

// Package motd enthält die Domain-Logik: Discovery der update-motd.d-Scripte,
// Diff/Plan von Soll-Zuständen, das Ausführen des Plans und die Preview.
package motd
import (
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"motd-assist/internal/assets"
)
// Script ist ein Eintrag in /etc/update-motd.d.
type Script struct {
Name string // Dateiname inkl. Nummern-Präfix, z.B. "00-header"
Path string // Absoluter Pfad
Enabled bool // Executable-Bit gesetzt (run-parts führt nur diese aus)
Bundled bool // von motd-assist installiert (Inhalt entspricht einer Vorlage)
}
// MotdDir liefert den Pfad des update-motd.d-Verzeichnisses unter root.
func MotdDir(root string) string {
return filepath.Join(root, "etc", "update-motd.d")
}
// run-parts im pam_motd-Kontext ignoriert Dateien mit Punkten; wir halten
// uns an dieselbe Namenskonvention ([A-Za-z0-9_-]).
var validName = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
// Discover listet alle Scripte im update-motd.d-Verzeichnis in
// run-parts-Reihenfolge (lexikalische Sortierung). Gebündelte Scripte
// werden per Inhaltsvergleich erkannt.
func Discover(root string) ([]Script, error) {
dir := MotdDir(root)
entries, err := os.ReadDir(dir)
if err != nil {
return nil, fmt.Errorf("update-motd.d nicht lesbar: %w", err)
}
scripts := make([]Script, 0, len(entries))
for _, e := range entries {
if e.IsDir() || !validName.MatchString(e.Name()) {
continue
}
info, err := e.Info()
if err != nil {
continue
}
path := filepath.Join(dir, e.Name())
s := Script{
Name: e.Name(),
Path: path,
Enabled: info.Mode()&0o111 != 0,
}
if b, err := os.ReadFile(path); err == nil {
_, s.Bundled = assets.MatchScript(string(b))
}
scripts = append(scripts, s)
}
sort.Slice(scripts, func(i, j int) bool { return scripts[i].Name < scripts[j].Name })
return scripts, nil
}
// EnabledCount zählt die aktiven Scripte.
func EnabledCount(scripts []Script) int {
n := 0
for _, s := range scripts {
if s.Enabled {
n++
}
}
return n
}