Updated installation process simplified it

This commit is contained in:
anonpenguin23 2026-02-14 14:06:14 +02:00
parent 749d5ed5e7
commit ba4e2688e4
10 changed files with 124 additions and 298 deletions

View File

@ -94,6 +94,8 @@ build-linux: deps
GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS_LINUX)" -trimpath -o bin-linux/orama-cli ./cmd/cli
@echo "Building Olric for linux/amd64..."
GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -trimpath -o bin-linux/olric-server github.com/olric-data/olric/cmd/olric-server
@echo "Building IPFS Cluster Service for linux/amd64..."
GOOS=linux GOARCH=amd64 GOBIN=$(CURDIR)/bin-linux go install -ldflags "-s -w" -trimpath github.com/ipfs-cluster/ipfs-cluster/cmd/ipfs-cluster-service@latest
@echo "✓ All Linux binaries built in bin-linux/"
@echo ""
@echo "Next steps:"

View File

@ -14,7 +14,26 @@ func Handle(args []string) {
os.Exit(1)
}
// Create orchestrator
// Resolve base domain interactively if not provided (before local/VPS branch)
if flags.BaseDomain == "" {
flags.BaseDomain = promptForBaseDomain()
}
// Local mode: not running as root → orchestrate install via SSH
if os.Geteuid() != 0 {
remote, err := NewRemoteOrchestrator(flags)
if err != nil {
fmt.Fprintf(os.Stderr, "❌ %v\n", err)
os.Exit(1)
}
if err := remote.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "❌ %v\n", err)
os.Exit(1)
}
return
}
// VPS mode: running as root on the VPS — existing behavior
orchestrator, err := NewOrchestrator(flags)
if err != nil {
fmt.Fprintf(os.Stderr, "❌ %v\n", err)
@ -27,12 +46,6 @@ func Handle(args []string) {
os.Exit(1)
}
// Check root privileges
if err := orchestrator.validator.ValidateRootPrivileges(); err != nil {
fmt.Fprintf(os.Stderr, "❌ %v\n", err)
os.Exit(1)
}
// Check port availability before proceeding
if err := orchestrator.validator.ValidatePorts(); err != nil {
fmt.Fprintf(os.Stderr, "❌ %v\n", err)

View File

@ -11,12 +11,9 @@ type Flags struct {
VpsIP string
Domain string
BaseDomain string // Base domain for deployment routing (e.g., "dbrs.space")
Branch string
NoPull bool
Force bool
DryRun bool
SkipChecks bool
PreBuilt bool // Skip building binaries, use pre-built binaries already on disk
Nameserver bool // Make this node a nameserver (runs CoreDNS + Caddy)
JoinAddress string // HTTPS URL of existing node (e.g., https://node1.dbrs.space)
Token string // Invite token for joining (from orama invite)
@ -57,12 +54,9 @@ func ParseFlags(args []string) (*Flags, error) {
fs.StringVar(&flags.VpsIP, "vps-ip", "", "Public IP of this VPS (required)")
fs.StringVar(&flags.Domain, "domain", "", "Domain name for HTTPS (optional, e.g. gateway.example.com)")
fs.StringVar(&flags.BaseDomain, "base-domain", "", "Base domain for deployment routing (e.g., dbrs.space)")
fs.StringVar(&flags.Branch, "branch", "main", "Git branch to use (main or nightly)")
fs.BoolVar(&flags.NoPull, "no-pull", false, "Skip git clone/pull, use existing repository in /home/debros/src")
fs.BoolVar(&flags.Force, "force", false, "Force reconfiguration even if already installed")
fs.BoolVar(&flags.DryRun, "dry-run", false, "Show what would be done without making changes")
fs.BoolVar(&flags.SkipChecks, "skip-checks", false, "Skip minimum resource checks (RAM/CPU)")
fs.BoolVar(&flags.PreBuilt, "pre-built", false, "Skip building binaries on VPS, use pre-built binaries already in /home/debros/bin and /usr/local/bin")
fs.BoolVar(&flags.Nameserver, "nameserver", false, "Make this node a nameserver (runs CoreDNS + Caddy)")
// Cluster join flags

View File

@ -33,18 +33,13 @@ func NewOrchestrator(flags *Flags) (*Orchestrator, error) {
oramaHome := "/home/debros"
oramaDir := oramaHome + "/.orama"
// Prompt for base domain if not provided via flag
if flags.BaseDomain == "" {
flags.BaseDomain = promptForBaseDomain()
}
// Normalize peers
peers, err := utils.NormalizePeers(flags.PeersStr)
if err != nil {
return nil, fmt.Errorf("invalid peers: %w", err)
}
setup := production.NewProductionSetup(oramaHome, os.Stdout, flags.Force, flags.Branch, flags.NoPull, flags.SkipChecks, flags.PreBuilt)
setup := production.NewProductionSetup(oramaHome, os.Stdout, flags.Force, flags.SkipChecks)
setup.SetNameserver(flags.Nameserver)
// Configure Anyone mode
@ -84,18 +79,6 @@ func NewOrchestrator(flags *Flags) (*Orchestrator, error) {
func (o *Orchestrator) Execute() error {
fmt.Printf("🚀 Starting production installation...\n\n")
// Inform user if skipping git pull
if o.flags.NoPull {
fmt.Printf(" ⚠️ --no-pull flag enabled: Skipping git clone/pull\n")
fmt.Printf(" Using existing repository at /home/debros/src\n")
}
// Inform user if using pre-built binaries
if o.flags.PreBuilt {
fmt.Printf(" ⚠️ --pre-built flag enabled: Skipping all Go compilation\n")
fmt.Printf(" Using pre-built binaries from /home/debros/bin and /usr/local/bin\n")
}
// Validate DNS if domain is provided
o.validator.ValidateDNS()
@ -112,7 +95,7 @@ func (o *Orchestrator) Execute() error {
ORPort: o.flags.AnyoneORPort,
}
}
utils.ShowDryRunSummaryWithRelay(o.flags.VpsIP, o.flags.Domain, o.flags.Branch, o.peers, o.flags.JoinAddress, o.validator.IsFirstNode(), o.oramaDir, relayInfo)
utils.ShowDryRunSummaryWithRelay(o.flags.VpsIP, o.flags.Domain, "main", o.peers, o.flags.JoinAddress, o.validator.IsFirstNode(), o.oramaDir, relayInfo)
return nil
}
@ -131,7 +114,7 @@ func (o *Orchestrator) Execute() error {
anyoneORPort = 9001
}
prefs := &production.NodePreferences{
Branch: o.flags.Branch,
Branch: "main",
Nameserver: o.flags.Nameserver,
AnyoneClient: o.flags.AnyoneClient,
AnyoneRelay: o.flags.AnyoneRelay,

View File

@ -10,10 +10,7 @@ import (
type Flags struct {
Force bool
RestartServices bool
NoPull bool
PreBuilt bool
SkipChecks bool
Branch string
Nameserver *bool // Pointer so we can detect if explicitly set vs default
// Anyone flags
@ -39,10 +36,7 @@ func ParseFlags(args []string) (*Flags, error) {
fs.BoolVar(&flags.Force, "force", false, "Reconfigure all settings")
fs.BoolVar(&flags.RestartServices, "restart", false, "Automatically restart services after upgrade")
fs.BoolVar(&flags.NoPull, "no-pull", false, "Skip source download, use existing /home/debros/src")
fs.BoolVar(&flags.PreBuilt, "pre-built", false, "Skip building binaries on VPS, use pre-built binaries already in /home/debros/bin and /usr/local/bin")
fs.BoolVar(&flags.SkipChecks, "skip-checks", false, "Skip minimum resource checks (RAM/CPU)")
fs.StringVar(&flags.Branch, "branch", "", "Git branch to use (uses saved preference if not specified)")
// Nameserver flag - use pointer to detect if explicitly set
nameserver := fs.Bool("nameserver", false, "Make this node a nameserver (uses saved preference if not specified)")

View File

@ -32,19 +32,13 @@ func NewOrchestrator(flags *Flags) *Orchestrator {
// Load existing preferences
prefs := production.LoadPreferences(oramaDir)
// Use saved branch if not specified
branch := flags.Branch
if branch == "" {
branch = prefs.Branch
}
// Use saved nameserver preference if not explicitly specified
isNameserver := prefs.Nameserver
if flags.Nameserver != nil {
isNameserver = *flags.Nameserver
}
setup := production.NewProductionSetup(oramaHome, os.Stdout, flags.Force, branch, flags.NoPull, flags.SkipChecks, flags.PreBuilt)
setup := production.NewProductionSetup(oramaHome, os.Stdout, flags.Force, flags.SkipChecks)
setup.SetNameserver(isNameserver)
// Configure Anyone mode (flag > saved preference > auto-detect)
@ -103,18 +97,6 @@ func (o *Orchestrator) Execute() error {
fmt.Printf(" This will preserve existing configurations and data\n")
fmt.Printf(" Configurations will be updated to latest format\n\n")
// Log if --no-pull is enabled
if o.flags.NoPull {
fmt.Printf(" ⚠️ --no-pull flag enabled: Skipping git clone/pull\n")
fmt.Printf(" Using existing repository at %s/src\n", o.oramaHome)
}
// Log if --pre-built is enabled
if o.flags.PreBuilt {
fmt.Printf(" ⚠️ --pre-built flag enabled: Skipping all Go compilation\n")
fmt.Printf(" Using pre-built binaries from %s/bin and /usr/local/bin\n", o.oramaHome)
}
// Handle branch preferences
if err := o.handleBranchPreferences(); err != nil {
return err
@ -216,15 +198,6 @@ func (o *Orchestrator) handleBranchPreferences() error {
prefs := production.LoadPreferences(o.oramaDir)
prefsChanged := false
// If branch was explicitly provided, update it
if o.flags.Branch != "" {
prefs.Branch = o.flags.Branch
prefsChanged = true
fmt.Printf(" Using branch: %s (saved for future upgrades)\n", o.flags.Branch)
} else {
fmt.Printf(" Using branch: %s (from saved preference)\n", prefs.Branch)
}
// If nameserver was explicitly provided, update it
if o.flags.Nameserver != nil {
prefs.Nameserver = *o.flags.Nameserver

View File

@ -116,66 +116,84 @@ func (ad *ArchitectureDetector) Detect() (string, error) {
}
}
// DependencyChecker validates external tool availability
type DependencyChecker struct {
skipOptional bool
}
// DependencyChecker validates external tool availability and auto-installs missing ones
type DependencyChecker struct{}
// NewDependencyChecker creates a new checker
func NewDependencyChecker(skipOptional bool) *DependencyChecker {
return &DependencyChecker{
skipOptional: skipOptional,
}
func NewDependencyChecker(_ bool) *DependencyChecker {
return &DependencyChecker{}
}
// Dependency represents an external binary dependency
type Dependency struct {
Name string
Command string
Optional bool
InstallHint string
AptPkg string // apt package name to install
}
// CheckAll validates all required dependencies
// CheckAll validates all required dependencies, auto-installing any that are missing.
func (dc *DependencyChecker) CheckAll() ([]Dependency, error) {
dependencies := []Dependency{
{
Name: "curl",
Command: "curl",
Optional: false,
InstallHint: "Usually pre-installed; if missing: apt-get install curl",
},
{
Name: "git",
Command: "git",
Optional: false,
InstallHint: "Install with: apt-get install git",
},
{
Name: "make",
Command: "make",
Optional: false,
InstallHint: "Install with: apt-get install make",
},
{Name: "curl", Command: "curl", AptPkg: "curl"},
{Name: "git", Command: "git", AptPkg: "git"},
{Name: "make", Command: "make", AptPkg: "make"},
{Name: "jq", Command: "jq", AptPkg: "jq"},
{Name: "speedtest", Command: "speedtest-cli", AptPkg: "speedtest-cli"},
}
var missing []Dependency
for _, dep := range dependencies {
if _, err := exec.LookPath(dep.Command); err != nil {
if !dep.Optional || !dc.skipOptional {
missing = append(missing, dep)
}
}
if len(missing) == 0 {
return nil, nil
}
if len(missing) > 0 {
errMsg := "missing required dependencies:\n"
// Auto-install missing dependencies
var pkgs []string
var names []string
for _, dep := range missing {
errMsg += fmt.Sprintf(" - %s (%s): %s\n", dep.Name, dep.Command, dep.InstallHint)
}
return missing, fmt.Errorf("%s", errMsg)
pkgs = append(pkgs, dep.AptPkg)
names = append(names, dep.Name)
}
fmt.Fprintf(os.Stderr, " Installing missing dependencies: %s\n", strings.Join(names, ", "))
// apt-get update first
update := exec.Command("apt-get", "update", "-qq")
update.Stdout = os.Stdout
update.Stderr = os.Stderr
update.Run() // best-effort, don't fail on update
// apt-get install
args := append([]string{"install", "-y", "-qq"}, pkgs...)
install := exec.Command("apt-get", args...)
install.Stdout = os.Stdout
install.Stderr = os.Stderr
if err := install.Run(); err != nil {
return missing, fmt.Errorf("failed to install dependencies (%s): %w", strings.Join(names, ", "), err)
}
// Verify after install
var stillMissing []Dependency
for _, dep := range missing {
if _, err := exec.LookPath(dep.Command); err != nil {
stillMissing = append(stillMissing, dep)
}
}
if len(stillMissing) > 0 {
errMsg := "dependencies still missing after install attempt:\n"
for _, dep := range stillMissing {
errMsg += fmt.Sprintf(" - %s\n", dep.Name)
}
return stillMissing, fmt.Errorf("%s", errMsg)
}
fmt.Fprintf(os.Stderr, " ✓ Dependencies installed successfully\n")
return nil, nil
}

View File

@ -72,9 +72,9 @@ func (bi *BinaryInstaller) ResolveBinaryPath(binary string, extraPaths ...string
return installers.ResolveBinaryPath(binary, extraPaths...)
}
// InstallDeBrosBinaries clones and builds DeBros binaries
func (bi *BinaryInstaller) InstallDeBrosBinaries(branch string, oramaHome string, skipRepoUpdate bool) error {
return bi.gateway.InstallDeBrosBinaries(branch, oramaHome, skipRepoUpdate)
// InstallDeBrosBinaries builds DeBros binaries from source
func (bi *BinaryInstaller) InstallDeBrosBinaries(oramaHome string) error {
return bi.gateway.InstallDeBrosBinaries(oramaHome)
}
// InstallSystemDependencies installs system-level dependencies via apt

View File

@ -39,78 +39,9 @@ func (gi *GatewayInstaller) Configure() error {
return nil
}
// downloadSourceZIP downloads source code as ZIP from GitHub
// This is simpler and more reliable than git clone with shallow clones
func (gi *GatewayInstaller) downloadSourceZIP(branch string, srcDir string) error {
// GitHub archive URL format
zipURL := fmt.Sprintf("https://github.com/DeBrosOfficial/network/archive/refs/heads/%s.zip", branch)
zipPath := "/tmp/network-source.zip"
extractDir := "/tmp/network-extract"
// Clean up any previous download artifacts
os.RemoveAll(zipPath)
os.RemoveAll(extractDir)
// Download ZIP
fmt.Fprintf(gi.logWriter, " Downloading source (branch: %s)...\n", branch)
if err := DownloadFile(zipURL, zipPath); err != nil {
return fmt.Errorf("failed to download source from %s: %w", zipURL, err)
}
// Create extraction directory
if err := os.MkdirAll(extractDir, 0755); err != nil {
return fmt.Errorf("failed to create extraction directory: %w", err)
}
// Extract ZIP
fmt.Fprintf(gi.logWriter, " Extracting source...\n")
extractCmd := exec.Command("unzip", "-q", "-o", zipPath, "-d", extractDir)
if output, err := extractCmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to extract source: %w\n%s", err, string(output))
}
// GitHub extracts to network-{branch}/ directory
extractedDir := filepath.Join(extractDir, fmt.Sprintf("network-%s", branch))
// Verify extracted directory exists
if _, err := os.Stat(extractedDir); os.IsNotExist(err) {
// Try alternative naming (GitHub may sanitize branch names)
entries, _ := os.ReadDir(extractDir)
if len(entries) == 1 && entries[0].IsDir() {
extractedDir = filepath.Join(extractDir, entries[0].Name())
} else {
return fmt.Errorf("extracted directory not found at %s", extractedDir)
}
}
// Remove existing source directory
os.RemoveAll(srcDir)
// Move extracted content to source directory
if err := os.Rename(extractedDir, srcDir); err != nil {
// Cross-filesystem fallback: copy instead of rename
fmt.Fprintf(gi.logWriter, " Moving source (cross-filesystem copy)...\n")
copyCmd := exec.Command("cp", "-r", extractedDir, srcDir)
if output, err := copyCmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to move source: %w\n%s", err, string(output))
}
}
// Cleanup temp files
os.RemoveAll(zipPath)
os.RemoveAll(extractDir)
// Fix ownership
if err := exec.Command("chown", "-R", "debros:debros", srcDir).Run(); err != nil {
fmt.Fprintf(gi.logWriter, " ⚠️ Warning: failed to chown source directory: %v\n", err)
}
fmt.Fprintf(gi.logWriter, " ✓ Source downloaded\n")
return nil
}
// InstallDeBrosBinaries downloads and builds DeBros binaries
func (gi *GatewayInstaller) InstallDeBrosBinaries(branch string, oramaHome string, skipRepoUpdate bool) error {
// InstallDeBrosBinaries builds DeBros binaries from source at /home/debros/src.
// Source must already be present (uploaded via SCP archive).
func (gi *GatewayInstaller) InstallDeBrosBinaries(oramaHome string) error {
fmt.Fprintf(gi.logWriter, " Building DeBros binaries...\n")
srcDir := filepath.Join(oramaHome, "src")
@ -124,24 +55,9 @@ func (gi *GatewayInstaller) InstallDeBrosBinaries(branch string, oramaHome strin
return fmt.Errorf("failed to create bin directory %s: %w", binDir, err)
}
// Check if source directory has content
hasSourceContent := false
if entries, err := os.ReadDir(srcDir); err == nil && len(entries) > 0 {
hasSourceContent = true
}
// Handle repository update/download based on skipRepoUpdate flag
if skipRepoUpdate {
fmt.Fprintf(gi.logWriter, " Skipping source download (--no-pull flag)\n")
if !hasSourceContent {
return fmt.Errorf("cannot skip download: source directory is empty at %s (need to populate it first)", srcDir)
}
fmt.Fprintf(gi.logWriter, " Using existing source at %s\n", srcDir)
} else {
// Download source as ZIP from GitHub (simpler than git, no shallow clone issues)
if err := gi.downloadSourceZIP(branch, srcDir); err != nil {
return err
}
// Verify source exists
if entries, err := os.ReadDir(srcDir); err != nil || len(entries) == 0 {
return fmt.Errorf("source directory is empty at %s (upload source archive first)", srcDir)
}
// Build binaries

View File

@ -52,9 +52,6 @@ type ProductionSetup struct {
serviceGenerator *SystemdServiceGenerator
serviceController *SystemdController
binaryInstaller *BinaryInstaller
branch string
skipRepoUpdate bool
skipBuild bool // Skip all Go compilation (use pre-built binaries)
NodePeerID string // Captured during Phase3 for later display
}
@ -86,24 +83,16 @@ func SaveBranchPreference(oramaDir, branch string) error {
}
// NewProductionSetup creates a new production setup orchestrator
func NewProductionSetup(oramaHome string, logWriter io.Writer, forceReconfigure bool, branch string, skipRepoUpdate bool, skipResourceChecks bool, skipBuild bool) *ProductionSetup {
func NewProductionSetup(oramaHome string, logWriter io.Writer, forceReconfigure bool, skipResourceChecks bool) *ProductionSetup {
oramaDir := filepath.Join(oramaHome, ".orama")
arch, _ := (&ArchitectureDetector{}).Detect()
// If branch is empty, try to read from stored preference, otherwise default to main
if branch == "" {
branch = ReadBranchPreference(oramaDir)
}
return &ProductionSetup{
oramaHome: oramaHome,
oramaDir: oramaDir,
logWriter: logWriter,
forceReconfigure: forceReconfigure,
arch: arch,
branch: branch,
skipRepoUpdate: skipRepoUpdate,
skipBuild: skipBuild,
skipResourceChecks: skipResourceChecks,
privChecker: &PrivilegeChecker{},
osDetector: &OSDetector{},
@ -201,12 +190,12 @@ func (ps *ProductionSetup) Phase1CheckPrerequisites() error {
ps.arch = arch
ps.logf(" ✓ Detected architecture: %s", arch)
// Check basic dependencies
// Check basic dependencies (auto-installs missing ones)
depChecker := NewDependencyChecker(ps.skipOptionalDeps)
if missing, err := depChecker.CheckAll(); err != nil {
ps.logf(" ❌ Missing dependencies:")
ps.logf(" ❌ Failed to install dependencies:")
for _, dep := range missing {
ps.logf(" - %s: %s", dep.Name, dep.InstallHint)
ps.logf(" - %s", dep.Name)
}
return err
}
@ -307,62 +296,7 @@ func (ps *ProductionSetup) Phase2bInstallBinaries() error {
ps.logf(" ⚠️ System dependencies warning: %v", err)
}
if ps.skipBuild {
// --pre-built mode: skip all Go compilation, verify binaries exist
ps.logf(" --pre-built mode: skipping Go installation and all compilation")
// Verify required DeBros binaries exist
binDir := filepath.Join(ps.oramaHome, "bin")
requiredBins := []string{"orama-node", "gateway", "orama", "identity"}
for _, bin := range requiredBins {
binPath := filepath.Join(binDir, bin)
if _, err := os.Stat(binPath); os.IsNotExist(err) {
return fmt.Errorf("--pre-built: required binary not found at %s (run 'make build-linux' locally and copy to VPS)", binPath)
}
ps.logf(" ✓ Found %s", binPath)
}
// Grant CAP_NET_BIND_SERVICE to orama-node
nodeBinary := filepath.Join(binDir, "orama-node")
if err := exec.Command("setcap", "cap_net_bind_service=+ep", nodeBinary).Run(); err != nil {
ps.logf(" ⚠️ Warning: failed to setcap on orama-node: %v", err)
}
// Verify Olric
if _, err := exec.LookPath("olric-server"); err != nil {
// Check if it's in the bin dir
olricPath := filepath.Join(binDir, "olric-server")
if _, err := os.Stat(olricPath); os.IsNotExist(err) {
return fmt.Errorf("--pre-built: olric-server not found in PATH or %s", binDir)
}
// Copy to /usr/local/bin
if data, err := os.ReadFile(olricPath); err == nil {
os.WriteFile("/usr/local/bin/olric-server", data, 0755)
}
ps.logf(" ✓ Found %s", olricPath)
} else {
ps.logf(" ✓ olric-server already in PATH")
}
// Verify CoreDNS and Caddy if nameserver
if ps.isNameserver {
if _, err := os.Stat("/usr/local/bin/coredns"); os.IsNotExist(err) {
return fmt.Errorf("--pre-built: coredns not found at /usr/local/bin/coredns")
}
ps.logf(" ✓ Found /usr/local/bin/coredns")
if _, err := os.Stat("/usr/bin/caddy"); os.IsNotExist(err) {
return fmt.Errorf("--pre-built: caddy not found at /usr/bin/caddy")
}
ps.logf(" ✓ Found /usr/bin/caddy")
// Grant CAP_NET_BIND_SERVICE to caddy
if err := exec.Command("setcap", "cap_net_bind_service=+ep", "/usr/bin/caddy").Run(); err != nil {
ps.logf(" ⚠️ Warning: failed to setcap on caddy: %v", err)
}
}
} else {
// Normal mode: install Go and build everything
// Install Go toolchain (downloads from go.dev if needed)
if err := ps.binaryInstaller.InstallGo(); err != nil {
return fmt.Errorf("failed to install Go: %w", err)
}
@ -371,8 +305,8 @@ func (ps *ProductionSetup) Phase2bInstallBinaries() error {
ps.logf(" ⚠️ Olric install warning: %v", err)
}
// Install DeBros binaries (must be done before CoreDNS since we need the RQLite plugin source)
if err := ps.binaryInstaller.InstallDeBrosBinaries(ps.branch, ps.oramaHome, ps.skipRepoUpdate); err != nil {
// Install DeBros binaries (source must be at /home/debros/src via SCP)
if err := ps.binaryInstaller.InstallDeBrosBinaries(ps.oramaHome); err != nil {
return fmt.Errorf("failed to install DeBros binaries: %w", err)
}
@ -387,7 +321,6 @@ func (ps *ProductionSetup) Phase2bInstallBinaries() error {
if err := ps.binaryInstaller.InstallCaddy(); err != nil {
ps.logf(" ⚠️ Caddy install warning: %v", err)
}
}
// These are pre-built binary downloads (not Go compilation), always run them
if err := ps.binaryInstaller.InstallRQLite(); err != nil {