417 lines
12 KiB
Go
Raw Permalink Normal View History

2026-03-12 20:51:38 -07:00
package tui
import (
"fmt"
"os/exec"
"strings"
tea "github.com/charmbracelet/bubbletea"
)
// ── Dependency Categories ───────────────────────────────────────────
type depCheck struct {
Name string
Cmd string // command to check existence
Pkg string // apt package name
Kind string // "system", "python", "npm"
Desc string
}
var systemDeps = []depCheck{
// Core runtime
{"python3", "python3", "python3", "system", "Python 3.10+ interpreter"},
{"pip", "pip3", "python3-pip", "system", "Python package manager"},
{"python3-venv", "python3 -m venv --help", "python3-venv", "system", "Python virtual environments"},
{"python3-dev", "python3-config --includes", "python3-dev", "system", "Python C headers (for native extensions)"},
// Build tools
{"gcc", "gcc", "build-essential", "system", "C/C++ compiler toolchain"},
{"cmake", "cmake", "cmake", "system", "CMake build system (for llama-cpp)"},
{"pkg-config", "pkg-config", "pkg-config", "system", "Package config helper"},
// Core system utilities
{"git", "git", "git", "system", "Version control"},
{"curl", "curl", "curl", "system", "HTTP client"},
{"wget", "wget", "wget", "system", "File downloader"},
{"openssl", "openssl", "openssl", "system", "TLS/crypto toolkit"},
// C libraries for Python packages
{"libffi-dev", "pkg-config --exists libffi", "libffi-dev", "system", "FFI library (for cffi/cryptography)"},
{"libssl-dev", "pkg-config --exists openssl", "libssl-dev", "system", "OpenSSL headers (for cryptography)"},
{"libpcap-dev", "pkg-config --exists libpcap", "libpcap-dev", "system", "Packet capture headers (for scapy)"},
{"libxml2-dev", "pkg-config --exists libxml-2.0", "libxml2-dev", "system", "XML parser headers (for lxml)"},
{"libxslt1-dev", "pkg-config --exists libxslt", "libxslt1-dev", "system", "XSLT headers (for lxml)"},
// Security tools
{"nmap", "nmap", "nmap", "system", "Network scanner"},
{"tshark", "tshark", "tshark", "system", "Packet analysis (Wireshark CLI)"},
{"whois", "whois", "whois", "system", "WHOIS lookup"},
{"dnsutils", "dig", "dnsutils", "system", "DNS utilities (dig, nslookup)"},
// Android tools
{"adb", "adb", "adb", "system", "Android Debug Bridge"},
{"fastboot", "fastboot", "fastboot", "system", "Android Fastboot"},
// Network tools
{"wg", "wg", "wireguard-tools", "system", "WireGuard VPN tools"},
{"upnpc", "upnpc", "miniupnpc", "system", "UPnP port mapping client"},
{"net-tools", "ifconfig", "net-tools", "system", "Network utilities (ifconfig)"},
// Node.js
{"node", "node", "nodejs", "system", "Node.js (for hardware WebUSB libs)"},
{"npm", "npm", "npm", "system", "Node package manager"},
// Go
{"go", "go", "golang", "system", "Go compiler (for DNS server build)"},
// Media / misc
{"ffmpeg", "ffmpeg", "ffmpeg", "system", "Media processing"},
}
// ── Rendering ───────────────────────────────────────────────────────
func (a App) renderDepsMenu() string {
var b strings.Builder
b.WriteString(styleTitle.Render("DEPENDENCIES"))
b.WriteString("\n")
b.WriteString(a.renderHR())
b.WriteString("\n")
if len(a.listItems) == 0 {
b.WriteString(styleDim.Render(" Loading..."))
b.WriteString("\n")
return b.String()
}
// Count installed vs total
installed, total := 0, 0
for _, item := range a.listItems {
if item.Extra == "system" {
total++
if item.Enabled {
installed++
}
}
}
// System packages
b.WriteString(styleKey.Render(fmt.Sprintf(" System Packages (%d/%d installed)", installed, total)))
b.WriteString("\n\n")
for _, item := range a.listItems {
if item.Extra != "system" {
continue
}
status := styleStatusOK.Render("✔ installed")
if !item.Enabled {
status = styleStatusBad.Render("✘ missing ")
}
b.WriteString(fmt.Sprintf(" %s %-14s %s\n", status, item.Name, styleDim.Render(item.Status)))
}
// Python venv
b.WriteString("\n")
b.WriteString(styleKey.Render(" Python Virtual Environment"))
b.WriteString("\n\n")
for _, item := range a.listItems {
if item.Extra != "venv" {
continue
}
status := styleStatusOK.Render("✔ ready ")
if !item.Enabled {
status = styleStatusBad.Render("✘ missing")
}
b.WriteString(fmt.Sprintf(" %s %s\n", status, item.Name))
}
// Actions
b.WriteString("\n")
b.WriteString(a.renderHR())
b.WriteString("\n")
b.WriteString(styleKey.Render(" [a]") + " Install all missing system packages\n")
b.WriteString(styleKey.Render(" [v]") + " Create/recreate Python venv + install pip packages\n")
b.WriteString(styleKey.Render(" [n]") + " Install npm packages + build hardware JS bundles\n")
b.WriteString(styleKey.Render(" [f]") + " Full install (system + venv + pip + npm)\n")
b.WriteString(styleKey.Render(" [g]") + " Install Go compiler (for DNS server)\n")
b.WriteString(styleKey.Render(" [r]") + " Refresh status\n")
b.WriteString("\n")
b.WriteString(styleDim.Render(" esc back"))
b.WriteString("\n")
return b.String()
}
// ── Key Handling ────────────────────────────────────────────────────
func (a App) handleDepsMenu(key string) (tea.Model, tea.Cmd) {
switch key {
case "a":
return a.startDepsInstall("system")
case "v":
return a.startDepsInstall("venv")
case "n":
return a.startDepsInstall("npm")
case "f":
return a.startDepsInstall("full")
case "g":
return a.startDepsInstall("go")
case "r":
return a, a.loadDepsStatus()
}
return a, nil
}
// ── Commands ────────────────────────────────────────────────────────
func (a App) loadDepsStatus() tea.Cmd {
return func() tea.Msg {
var items []ListItem
// Check system deps
for _, d := range systemDeps {
installed := false
parts := strings.Fields(d.Cmd)
if len(parts) == 1 {
_, err := exec.LookPath(d.Cmd)
installed = err == nil
} else {
cmd := exec.Command(parts[0], parts[1:]...)
installed = cmd.Run() == nil
}
items = append(items, ListItem{
Name: d.Name,
Status: d.Desc,
Enabled: installed,
Extra: "system",
})
}
// Check venv
venvPath := fmt.Sprintf("%s/venv", findAutarchDir())
_, err := exec.LookPath(venvPath + "/bin/python3")
items = append(items, ListItem{
Name: "venv (" + venvPath + ")",
Enabled: err == nil,
Extra: "venv",
})
// Check pip packages in venv
venvPip := venvPath + "/bin/pip3"
if _, err := exec.LookPath(venvPip); err == nil {
out, _ := exec.Command(venvPip, "list", "--format=columns").Output()
count := strings.Count(string(out), "\n") - 2
if count < 0 {
count = 0
}
items = append(items, ListItem{
Name: fmt.Sprintf("pip packages (%d installed)", count),
Enabled: count > 5,
Extra: "venv",
})
} else {
items = append(items, ListItem{
Name: "pip packages (venv not found)",
Enabled: false,
Extra: "venv",
})
}
return depsLoadedMsg{items: items}
}
}
type depsLoadedMsg struct{ items []ListItem }
func (a App) startDepsInstall(mode string) (App, tea.Cmd) {
a.pushView(ViewDepsInstall)
a.outputLines = nil
a.outputDone = false
a.progressStep = 0
a.progressTotal = 0
a.progressLabel = ""
ch := make(chan tea.Msg, 256)
a.outputCh = ch
autarchDir := findAutarchDir()
go func() {
var steps []CmdStep
switch mode {
case "system":
steps = buildSystemInstallSteps()
case "venv":
steps = buildVenvSteps(autarchDir)
case "npm":
steps = buildNpmSteps(autarchDir)
case "go":
steps = []CmdStep{
{Label: "Update package lists", Args: []string{"apt-get", "update", "-qq"}},
{Label: "Install Go compiler", Args: []string{"apt-get", "install", "-y", "golang"}},
}
case "full":
steps = buildSystemInstallSteps()
steps = append(steps, buildVenvSteps(autarchDir)...)
steps = append(steps, buildNpmSteps(autarchDir)...)
}
if len(steps) == 0 {
ch <- OutputLineMsg(styleSuccess.Render("Nothing to install — all dependencies are present."))
close(ch)
return
}
streamSteps(ch, steps)
}()
return a, a.waitForOutput()
}
// ── Step Builders ───────────────────────────────────────────────────
func buildSystemInstallSteps() []CmdStep {
// Collect missing packages
var pkgs []string
for _, d := range systemDeps {
parts := strings.Fields(d.Cmd)
if len(parts) == 1 {
if _, err := exec.LookPath(d.Cmd); err != nil {
pkgs = append(pkgs, d.Pkg)
}
} else {
cmd := exec.Command(parts[0], parts[1:]...)
if cmd.Run() != nil {
pkgs = append(pkgs, d.Pkg)
}
}
}
// Deduplicate packages (some deps share packages like build-essential)
seen := make(map[string]bool)
var uniquePkgs []string
for _, p := range pkgs {
if !seen[p] {
seen[p] = true
uniquePkgs = append(uniquePkgs, p)
}
}
if len(uniquePkgs) == 0 {
return nil
}
steps := []CmdStep{
{Label: "Update package lists", Args: []string{"apt-get", "update", "-qq"}},
}
// Install in batches to show progress per category
// Group: core runtime
corePackages := filterPackages(uniquePkgs, []string{
"python3", "python3-pip", "python3-venv", "python3-dev",
"build-essential", "cmake", "pkg-config",
"git", "curl", "wget", "openssl",
})
if len(corePackages) > 0 {
steps = append(steps, CmdStep{
Label: fmt.Sprintf("Install core packages (%s)", strings.Join(corePackages, ", ")),
Args: append([]string{"apt-get", "install", "-y"}, corePackages...),
})
}
// Group: C library headers
libPackages := filterPackages(uniquePkgs, []string{
"libffi-dev", "libssl-dev", "libpcap-dev", "libxml2-dev", "libxslt1-dev",
})
if len(libPackages) > 0 {
steps = append(steps, CmdStep{
Label: fmt.Sprintf("Install C library headers (%s)", strings.Join(libPackages, ", ")),
Args: append([]string{"apt-get", "install", "-y"}, libPackages...),
})
}
// Group: security & network tools
toolPackages := filterPackages(uniquePkgs, []string{
"nmap", "tshark", "whois", "dnsutils",
"adb", "fastboot",
"wireguard-tools", "miniupnpc", "net-tools",
"ffmpeg",
})
if len(toolPackages) > 0 {
steps = append(steps, CmdStep{
Label: fmt.Sprintf("Install security/network tools (%s)", strings.Join(toolPackages, ", ")),
Args: append([]string{"apt-get", "install", "-y"}, toolPackages...),
})
}
// Group: node + go
devPackages := filterPackages(uniquePkgs, []string{
"nodejs", "npm", "golang",
})
if len(devPackages) > 0 {
steps = append(steps, CmdStep{
Label: fmt.Sprintf("Install dev tools (%s)", strings.Join(devPackages, ", ")),
Args: append([]string{"apt-get", "install", "-y"}, devPackages...),
})
}
return steps
}
func buildVenvSteps(autarchDir string) []CmdStep {
venv := autarchDir + "/venv"
pip := venv + "/bin/pip3"
reqFile := autarchDir + "/requirements.txt"
steps := []CmdStep{
{Label: "Create Python virtual environment", Args: []string{"python3", "-m", "venv", venv}},
{Label: "Upgrade pip, setuptools, wheel", Args: []string{pip, "install", "--upgrade", "pip", "setuptools", "wheel"}},
}
if fileExists(reqFile) {
steps = append(steps, CmdStep{
Label: "Install Python packages from requirements.txt",
Args: []string{pip, "install", "-r", reqFile},
})
}
return steps
}
func buildNpmSteps(autarchDir string) []CmdStep {
steps := []CmdStep{
{Label: "Install npm packages", Args: []string{"npm", "install"}, Dir: autarchDir},
}
if fileExists(autarchDir + "/scripts/build-hw-libs.sh") {
steps = append(steps, CmdStep{
Label: "Build hardware JS bundles",
Args: []string{"bash", "scripts/build-hw-libs.sh"},
Dir: autarchDir,
})
}
return steps
}
// filterPackages returns only packages from wanted that exist in available.
func filterPackages(available, wanted []string) []string {
avail := make(map[string]bool)
for _, p := range available {
avail[p] = true
}
var result []string
for _, p := range wanted {
if avail[p] {
result = append(result, p)
}
}
return result
}