use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{anyhow, bail, Context, Result};
use super::{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,
self.socket_override.clone(),
self.dev_dir_override.clone(),
None, )
.pre_start_dev_dir()
}
fn ensure_running(&self) -> Result<DaemonHandle> {
if which("gh").is_none() {
bail!(
"--daemon clone requires the GitHub CLI (`gh`).\n\
Install from https://cli.github.com, then run `gh auth login`."
);
}
let auth_ok = Command::new("gh")
.args(["auth", "status"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false);
if !auth_ok {
bail!(
"`gh auth status` failed — run `gh auth login` first.\n\
(Cloning {} requires read access to the private repo.)",
CLONE_REPO
);
}
let access = Command::new("gh")
.args(["api", &format!("repos/{}", CLONE_REPO)])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false);
if !access {
bail!(
"your GitHub account doesn't have read access to {}.\n\
If you're an in-house contributor, ask the team to add you to the org.\n\
Otherwise use `--daemon docker` (no private repo access required).",
CLONE_REPO
);
}
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()
);
}
}
let monorepo_host = MonorepoHost::new(
monorepo_path,
self.socket_override.clone(),
self.dev_dir_override.clone(),
self.log_tx.clone(),
);
monorepo_host.ensure_running()
}
}
fn which(bin: &str) -> Option<PathBuf> {
let path = std::env::var_os("PATH")?;
for dir in std::env::split_paths(&path) {
let candidate = dir.join(bin);
if candidate.is_file() {
return Some(candidate);
}
}
None
}
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"))
}