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:
Tronax 2026-08-24 14:19:28 +02:00
commit d56659e21a
Signed by: Tronax
SSH key fingerprint: SHA256:2pKKXDZucWvaF/GzXNz0FY53EAO1YDLN80bqS+TTz/o
34 changed files with 3260 additions and 0 deletions

View file

@ -0,0 +1,118 @@
//go:build unix
package motd
import (
"os"
"path/filepath"
"testing"
"motd-assist/internal/assets"
)
func TestExecuteAppliesPlanAndRestore(t *testing.T) {
root := setupDir(t, map[string]string{
"00-header": "#!/bin/sh\necho header\n",
"10-sysinfo": "#!/bin/sh\necho sysinfo\n",
})
dir := MotdDir(root)
if err := os.Chmod(filepath.Join(dir, "10-sysinfo"), 0o644); err != nil {
t.Fatal(err)
}
current, err := Discover(root)
if err != nil {
t.Fatal(err)
}
// Ziel: sysinfo vor header, sysinfo aktiv, header aus.
desired := []DesiredEntry{
{Name: "10-sysinfo", Enabled: true},
{Name: "00-header", Enabled: false},
}
plan := Diff(current, desired)
if plan.Empty() {
t.Fatal("Plan sollte Änderungen enthalten")
}
if err := Execute(root, plan); err != nil {
t.Fatalf("Execute: %v", err)
}
after, err := Discover(root)
if err != nil {
t.Fatal(err)
}
if len(after) != 2 {
t.Fatalf("nach Execute: %v", after)
}
if after[0].Name != "00-sysinfo" || !after[0].Enabled {
t.Errorf("after[0] = %+v, want 00-sysinfo aktiv", after[0])
}
if after[1].Name != "10-header" || after[1].Enabled {
t.Errorf("after[1] = %+v, want 10-header inaktiv", after[1])
}
if _, err := os.Stat(SnapshotPath(root)); err != nil {
t.Errorf("Snapshot fehlt: %v", err)
}
if err := Restore(root); err != nil {
t.Fatalf("Restore: %v", err)
}
restored, err := Discover(root)
if err != nil {
t.Fatal(err)
}
if restored[0].Name != "00-header" || !restored[0].Enabled {
t.Errorf("restored[0] = %+v, want 00-header aktiv", restored[0])
}
if restored[1].Name != "10-sysinfo" || restored[1].Enabled {
t.Errorf("restored[1] = %+v, want 10-sysinfo inaktiv", restored[1])
}
}
func TestExecuteInstallAndRemove(t *testing.T) {
root := setupDir(t, map[string]string{
"00-header": "#!/bin/sh\necho header\n",
"95-footer": "#!/bin/sh\ntrue\n", // wird unten als gebündelt ersetzt
})
dir := MotdDir(root)
// echtes gebündeltes Script einsetzen, damit Remove validiert
content, ok := assets.ScriptContent("footer")
if !ok {
t.Fatal("footer-Vorlage nicht gefunden")
}
if err := os.WriteFile(filepath.Join(dir, "95-footer"), []byte(content), 0o755); err != nil {
t.Fatal(err)
}
current, err := Discover(root)
if err != nil {
t.Fatal(err)
}
desired := []DesiredEntry{
{Name: "10-brand-header", Enabled: true, Install: "brand-header"},
{Name: "00-header", Enabled: true},
{Name: "95-footer", Remove: true},
}
if err := Execute(root, Diff(current, desired)); err != nil {
t.Fatalf("Execute: %v", err)
}
after, err := Discover(root)
if err != nil {
t.Fatal(err)
}
var names []string
for _, s := range after {
names = append(names, s.Name)
}
want := []string{"00-brand-header", "10-header"}
if len(names) != 2 || names[0] != want[0] || names[1] != want[1] {
t.Errorf("names = %v, want %v", names, want)
}
if !after[0].Bundled {
t.Error("brand-header sollte als gebündelt erkannt werden")
}
if !after[0].Enabled || !after[1].Enabled {
t.Errorf("Modi falsch: %+v", after)
}
}