TUISTREAM/internal/system/deps.go
28allday 959e507a83 Initial commit: TUISTREAM — headless Jellyfin TUI for Omarchy/Arch
Three-tab Bubble Tea TUI (Setup / Manage / Monitor) for setting up and
running a headless Jellyfin server over SSH:

- Setup: install/uninstall Jellyfin, add media drives (single or btrfs
  RAID pool), import an existing detached btrfs pool non-destructively,
  firewall, move Jellyfin storage onto a media drive
- Boot-drive-safe drive classifier (walks LUKS/LVM/RAID to the physical
  disk; never offers a disk hosting /, /boot or swap)
- Keep-existing-filesystem path previews on-disk content, skips starter
  folders when content exists, and grants Jellyfin recursive read access
- Manage: copy from external drives, mount/eject, rename/delete
- Monitor: per-core CPU, load/mem/uptime, btrfs error counters, SMART

Single static binary; installs to /usr/local/bin so sudo can find it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 16:47:18 +01:00

73 lines
2.5 KiB
Go

// Package system handles host-level concerns that don't fit drives /
// jellyfin / firewall — most notably the one-time install of every Arch
// package tuistream's flows actually invoke.
//
// The TUI's individual flows used to install their own missing tools on
// the fly, which interrupted the user mid-action and broke the "one
// confirm screen, then runs to completion" promise. Doing it once at
// launch keeps the flows themselves dependency-free.
package system
import (
"fmt"
"os"
"os/exec"
"strings"
)
// Dep is one Arch package + the binary tuistream invokes from it. The
// binary check lets us decide whether the package is missing without
// shelling out to `pacman -Qi`, which is much slower.
type Dep struct {
Pkg string
Binary string
}
// Required is the closed set of userspace tools tuistream needs. Anything
// in Arch's `base` group (util-linux, e2fsprogs, coreutils) is assumed
// present and intentionally NOT listed.
var Required = []Dep{
{Pkg: "rsync", Binary: "rsync"}, // Manage > Copy
{Pkg: "acl", Binary: "setfacl"}, // Setup > add-drive ACL grant, Copy ACL re-grant
{Pkg: "btrfs-progs", Binary: "mkfs.btrfs"}, // Setup > format btrfs / pool
{Pkg: "xfsprogs", Binary: "mkfs.xfs"}, // Setup > format xfs
{Pkg: "gptfdisk", Binary: "sgdisk"}, // Setup > wipe-whole-disk
{Pkg: "parted", Binary: "partprobe"}, // Setup > partprobe after wipe
{Pkg: "smartmontools", Binary: "smartctl"}, // Monitor > SMART health per disk
}
// Missing returns the subset of Required whose binary isn't on $PATH.
func Missing() []Dep {
var out []Dep
for _, d := range Required {
if _, err := exec.LookPath(d.Binary); err != nil {
out = append(out, d)
}
}
return out
}
// EnsureInstalled checks for missing deps and runs `pacman -S --needed
// --noconfirm` for them. Output is streamed to stderr so the user can see
// what's happening before the TUI takes over the terminal.
//
// Returns nil if everything's already present or the install succeeded.
// On pacman failure, returns the error but doesn't exit the process —
// the caller decides whether to abort or warn-and-continue.
func EnsureInstalled() error {
miss := Missing()
if len(miss) == 0 {
return nil
}
pkgs := make([]string, len(miss))
for i, d := range miss {
pkgs[i] = d.Pkg
}
fmt.Fprintf(os.Stderr,
"tuistream: installing missing tools: %s\n", strings.Join(pkgs, " "))
args := append([]string{"-S", "--needed", "--noconfirm"}, pkgs...)
cmd := exec.Command("pacman", args...)
cmd.Stdout = os.Stderr
cmd.Stderr = os.Stderr
return cmd.Run()
}