Replace the shell-only user workflow with `banger` and `bangerd`: Cobra commands, XDG/SQLite-backed state, managed VM and image lifecycle, and a Bubble Tea TUI for browsing and operating VMs.\n\nKeep Firecracker orchestration behind the daemon so VM specs become persistent objects, and add repo entrypoints for building, installing, and documenting the new flow while still delegating rootfs customization to the existing shell tooling.\n\nHarden the control plane around real usage by reclaiming Firecracker API sockets for the user, restarting stale daemons after rebuilds, and returning the correct `vm.create` payload so the CLI and TUI creation flow work reliably.\n\nValidation: `go test ./...`, `make build`, and a host-side smoke test with `./banger vm create --name codex-smoke`.
154 lines
3.7 KiB
Go
154 lines
3.7 KiB
Go
package paths
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type Layout struct {
|
|
ConfigHome string
|
|
StateHome string
|
|
CacheHome string
|
|
RuntimeHome string
|
|
ConfigDir string
|
|
StateDir string
|
|
CacheDir string
|
|
RuntimeDir string
|
|
SocketPath string
|
|
DBPath string
|
|
DaemonLog string
|
|
VMsDir string
|
|
ImagesDir string
|
|
}
|
|
|
|
func Resolve() (Layout, error) {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return Layout{}, err
|
|
}
|
|
configHome := getenvDefault("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
|
|
stateHome := getenvDefault("XDG_STATE_HOME", filepath.Join(home, ".local", "state"))
|
|
cacheHome := getenvDefault("XDG_CACHE_HOME", filepath.Join(home, ".cache"))
|
|
runtimeHome := os.Getenv("XDG_RUNTIME_DIR")
|
|
if runtimeHome == "" {
|
|
runtimeHome = filepath.Join(os.TempDir(), fmt.Sprintf("banger-runtime-%d", os.Getuid()))
|
|
}
|
|
|
|
layout := Layout{
|
|
ConfigHome: configHome,
|
|
StateHome: stateHome,
|
|
CacheHome: cacheHome,
|
|
RuntimeHome: runtimeHome,
|
|
ConfigDir: filepath.Join(configHome, "banger"),
|
|
StateDir: filepath.Join(stateHome, "banger"),
|
|
CacheDir: filepath.Join(cacheHome, "banger"),
|
|
RuntimeDir: filepath.Join(runtimeHome, "banger"),
|
|
}
|
|
layout.SocketPath = filepath.Join(layout.RuntimeDir, "bangerd.sock")
|
|
layout.DBPath = filepath.Join(layout.StateDir, "state.db")
|
|
layout.DaemonLog = filepath.Join(layout.StateDir, "bangerd.log")
|
|
layout.VMsDir = filepath.Join(layout.StateDir, "vms")
|
|
layout.ImagesDir = filepath.Join(layout.StateDir, "images")
|
|
return layout, nil
|
|
}
|
|
|
|
func Ensure(layout Layout) error {
|
|
for _, dir := range []string{layout.ConfigDir, layout.StateDir, layout.CacheDir, layout.RuntimeDir, layout.VMsDir, layout.ImagesDir} {
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func DetectRepoRoot() string {
|
|
if env := os.Getenv("BANGER_REPO_ROOT"); env != "" {
|
|
return env
|
|
}
|
|
candidates := []string{}
|
|
if wd, err := os.Getwd(); err == nil {
|
|
candidates = append(candidates, wd)
|
|
}
|
|
if exe, err := os.Executable(); err == nil {
|
|
candidates = append(candidates, filepath.Dir(exe))
|
|
}
|
|
if look, err := exec.LookPath("firecracker"); err == nil {
|
|
candidates = append(candidates, filepath.Dir(look))
|
|
}
|
|
for _, candidate := range candidates {
|
|
if root := walkForRepoRoot(candidate); root != "" {
|
|
return root
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func walkForRepoRoot(start string) string {
|
|
current := start
|
|
for {
|
|
if hasRepoArtifacts(current) {
|
|
return current
|
|
}
|
|
parent := filepath.Dir(current)
|
|
if parent == current {
|
|
return ""
|
|
}
|
|
current = parent
|
|
}
|
|
}
|
|
|
|
func hasRepoArtifacts(dir string) bool {
|
|
required := []string{"firecracker", "README.md"}
|
|
for _, name := range required {
|
|
if _, err := os.Stat(filepath.Join(dir, name)); err != nil {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func BangerdPath() (string, error) {
|
|
if env := os.Getenv("BANGER_DAEMON_BIN"); env != "" {
|
|
return env, nil
|
|
}
|
|
exe, err := os.Executable()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
dir := filepath.Dir(exe)
|
|
for _, candidate := range []string{
|
|
filepath.Join(dir, "bangerd"),
|
|
filepath.Join(dir, "bangerd.exe"),
|
|
} {
|
|
if _, err := os.Stat(candidate); err == nil {
|
|
return candidate, nil
|
|
}
|
|
}
|
|
if root := DetectRepoRoot(); root != "" {
|
|
for _, candidate := range []string{
|
|
filepath.Join(root, "bangerd"),
|
|
filepath.Join(root, "bangerd.exe"),
|
|
} {
|
|
if _, err := os.Stat(candidate); err == nil {
|
|
return candidate, nil
|
|
}
|
|
}
|
|
}
|
|
return "", errors.New("bangerd binary not found next to banger; build ./cmd/bangerd")
|
|
}
|
|
|
|
func getenvDefault(key, fallback string) string {
|
|
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
|
|
return value
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func RuntimeFallbackLabel() string {
|
|
return strconv.Itoa(os.Getuid())
|
|
}
|