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
127
internal/preset/preset.go
Normal file
127
internal/preset/preset.go
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
// Package preset definiert das Preset-Schema und das Laden/Speichern
|
||||
// von Built-in- und User-Presets.
|
||||
package preset
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
|
||||
"motd-assist/internal/assets"
|
||||
)
|
||||
|
||||
// Entry beschreibt den Soll-Zustand eines Scripts innerhalb eines Presets.
|
||||
// Die Reihenfolge im Scripts-Array bestimmt die Ziel-Reihenfolge im MOTD.
|
||||
type Entry struct {
|
||||
Name string `json:"name"` // Zieldateiname, z.B. "10-brand-header"
|
||||
Enabled bool `json:"enabled"` // Executable-Bit gesetzt
|
||||
Install string `json:"install,omitempty"` // gebündelte Vorlage, leer = vorhandenes Script
|
||||
}
|
||||
|
||||
// Preset ist eine benannte MOTD-Konfiguration.
|
||||
type Preset struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Scripts []Entry `json:"scripts"`
|
||||
|
||||
Source string `json:"-"` // "builtin" oder "user"
|
||||
}
|
||||
|
||||
var safeName = regexp.MustCompile(`[^a-zA-Z0-9._-]+`)
|
||||
|
||||
// Dir liefert das User-Preset-Verzeichnis (XDG-konform).
|
||||
func Dir() (string, error) {
|
||||
if d := os.Getenv("XDG_CONFIG_HOME"); d != "" {
|
||||
return filepath.Join(d, "motd-assist", "presets"), nil
|
||||
}
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(home, ".config", "motd-assist", "presets"), nil
|
||||
}
|
||||
|
||||
// LoadAll liefert alle Presets: Built-ins, durch User-Presets mit
|
||||
// gleichem Namen überschrieben. Sortiert nach Name.
|
||||
func LoadAll() ([]Preset, error) {
|
||||
byName := make(map[string]Preset)
|
||||
|
||||
for file, content := range assets.PresetFiles() {
|
||||
var p Preset
|
||||
if err := json.Unmarshal([]byte(content), &p); err != nil {
|
||||
return nil, fmt.Errorf("builtin preset %s: %w", file, err)
|
||||
}
|
||||
p.Source = "builtin"
|
||||
byName[p.Name] = p
|
||||
}
|
||||
|
||||
dir, err := Dir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err == nil {
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || filepath.Ext(e.Name()) != ".json" {
|
||||
continue
|
||||
}
|
||||
b, err := os.ReadFile(filepath.Join(dir, e.Name()))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var p Preset
|
||||
if err := json.Unmarshal(b, &p); err != nil {
|
||||
continue
|
||||
}
|
||||
p.Source = "user"
|
||||
byName[p.Name] = p
|
||||
}
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
presets := make([]Preset, 0, len(byName))
|
||||
for _, p := range byName {
|
||||
presets = append(presets, p)
|
||||
}
|
||||
sort.Slice(presets, func(i, j int) bool { return presets[i].Name < presets[j].Name })
|
||||
return presets, nil
|
||||
}
|
||||
|
||||
// SaveUser schreibt ein Preset ins User-Verzeichnis (überschreibt Built-ins
|
||||
// mit gleichem Namen zur Laufzeit).
|
||||
func SaveUser(p Preset) (string, error) {
|
||||
if p.Name == "" {
|
||||
return "", errors.New("Preset-Name fehlt")
|
||||
}
|
||||
dir, err := Dir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
b, err := json.MarshalIndent(p, "", " ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
path := filepath.Join(dir, safeName.ReplaceAllString(p.Name, "-")+".json")
|
||||
return path, os.WriteFile(path, append(b, '\n'), 0o644)
|
||||
}
|
||||
|
||||
// DeleteUser entfernt ein User-Preset. Built-ins können nicht gelöscht werden.
|
||||
func DeleteUser(name string) error {
|
||||
dir, err := Dir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path := filepath.Join(dir, safeName.ReplaceAllString(name, "-")+".json")
|
||||
if err := os.Remove(path); err != nil {
|
||||
return fmt.Errorf("nur User-Presets sind löschbar: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue