node-app-build 5.25.1

Mini app developer CLI: scaffold, validate, package node-app-* Debian packages
//! `--daemon clone` mode: clone `econ-v1/node` to the cache dir, then
//! delegate to MonorepoHost. For in-house contributors who want the
//! latest unreleased platform code without manually managing a checkout.
//!
//! Subsequent runs do `git pull` to track upstream. The cargo build
//! cache is preserved across runs (still under the cache dir), so only
//! the first run pays the ~5min cold-build cost.

use std::path::{Path, PathBuf};
use std::process::Command;

use anyhow::{anyhow, bail, Context, Result};

use super::{gh, monorepo::MonorepoHost, DaemonHandle, DaemonHost};
use crate::tui::{self, LogTx};

const CLONE_REPO: &str = "econ-v1/node";

pub struct CloneHost {
    socket_override: Option<PathBuf>,
    dev_dir_override: Option<PathBuf>,
    log_tx: Option<LogTx>,
}

impl CloneHost {
    pub fn new(
        socket_override: Option<&Path>,
        dev_dir_override: Option<&Path>,
        log_tx: Option<LogTx>,
    ) -> Self {
        Self {
            socket_override: socket_override.map(PathBuf::from),
            dev_dir_override: dev_dir_override.map(PathBuf::from),
            log_tx,
        }
    }

    fn syslog(&self, msg: impl Into<String>) {
        tui::sys_log(self.log_tx.as_ref(), msg);
    }
}

impl DaemonHost for CloneHost {
    fn pre_start_dev_dir(&self) -> Option<PathBuf> {
        let monorepo_path = cache_root().ok()?.join("monorepo");
        MonorepoHost::new(
            monorepo_path,
            super::InstanceProfile::alice(),
            self.socket_override.clone(),
            self.dev_dir_override.clone(),
            None, // log_tx not needed for pre-start path computation
        )
        .pre_start_dev_dir()
    }

    fn ensure_running(&self) -> Result<DaemonHandle> {
        // 1. Verify gh CLI is installed, authenticated, and has access.
        gh::ensure_gh_auth_and_access(CLONE_REPO).map_err(|e| {
            anyhow!(
                "{e}\n\
                 Otherwise use `--daemon docker` (no private repo access required)."
            )
        })?;

        // 2. Clone (or update) the repo into the cache dir.
        let cache_root = cache_root()?;
        std::fs::create_dir_all(&cache_root).ok();
        let monorepo_path = cache_root.join("monorepo");

        if monorepo_path.exists() && monorepo_path.join(".git").exists() {
            self.syslog(format!(
                "→ updating cached monorepo at {}",
                monorepo_path.display()
            ));
            let status = Command::new("git")
                .args(["pull", "--ff-only"])
                .current_dir(&monorepo_path)
                .status()
                .with_context(|| format!("git pull in {}", monorepo_path.display()))?;
            if !status.success() {
                self.syslog(format!(
                    "⚠  git pull failed — proceeding with the cached version. \
                     You may want to delete {} for a fresh clone.",
                    monorepo_path.display()
                ));
            }
        } else {
            self.syslog(format!(
                "→ cloning {} to {} (first run, ~30s)…",
                CLONE_REPO,
                monorepo_path.display()
            ));
            let status = Command::new("gh")
                .args(["repo", "clone", CLONE_REPO])
                .arg(&monorepo_path)
                .status()
                .with_context(|| format!("gh repo clone {}", CLONE_REPO))?;
            if !status.success() {
                bail!(
                    "gh repo clone failed (exit {}). Cache state at {} may be partial.",
                    status.code().unwrap_or(-1),
                    monorepo_path.display()
                );
            }
        }

        // 3. Delegate to MonorepoHost — pass our log_tx so the TUI receives daemon output.
        let monorepo_host = MonorepoHost::new(
            monorepo_path,
            super::InstanceProfile::alice(),
            self.socket_override.clone(),
            self.dev_dir_override.clone(),
            self.log_tx.clone(),
        );
        monorepo_host.ensure_running()
    }
}

fn cache_root() -> Result<PathBuf> {
    if let Ok(c) = std::env::var("XDG_CACHE_HOME") {
        if !c.is_empty() {
            return Ok(PathBuf::from(c).join("node-app"));
        }
    }
    let home = std::env::var_os("HOME").ok_or_else(|| anyhow!("$HOME not set"))?;
    Ok(PathBuf::from(home).join(".cache/node-app"))
}