node-app-build 5.25.1

Mini app developer CLI: scaffold, validate, package node-app-* Debian packages
//! Daemon-host mode autodetection (plan 457 phase C+).
//!
//! When `--daemon` and `--monorepo` are both omitted, pick the cheapest
//! mode that's likely to work on this box:
//!
//! 1. Are we inside a monorepo checkout? (parent dir has `Cargo.toml`
//!    referencing `apps/server`) → `Mode::Monorepo`.
//! 2. Is the production socket already there at `/run/node/control.sock`?
//!    → `Mode::Remote`. The user has the daemon running already.
//! 3. Are we on Linux + `apt` present + the user can sudo? → `Mode::Deb`.
//!    (We don't actually probe sudo without prompting; we just suggest it.)
//! 4. Is `docker` on PATH? → `Mode::Docker`.
//! 5. Otherwise: error with a clear message of how to choose explicitly.

use std::path::PathBuf;

use anyhow::{anyhow, Result};

use super::host::Mode;

const PRODUCTION_SOCKET: &str = "/run/node/control.sock";

pub fn detect() -> Result<Mode> {
    // 1. Inside a monorepo?
    if let Some(monorepo) = find_monorepo_above(&std::env::current_dir().unwrap_or_default()) {
        return Ok(Mode::Monorepo { path: monorepo });
    }

    // 2. Existing production socket?
    let prod = PathBuf::from(PRODUCTION_SOCKET);
    if prod.exists() {
        return Ok(Mode::Remote { socket_path: prod });
    }

    // 3. Linux + apt → suggest deb (with sudo prompt baked in).
    if cfg!(target_os = "linux") && which("apt").is_some() {
        return Ok(Mode::Deb);
    }

    // 4. Docker available?
    if which("docker").is_some() {
        return Ok(Mode::Docker);
    }

    // 5. Nothing — surface the explicit options.
    Err(anyhow!(
        "could not autodetect a daemon-host mode.\n\
         \n\
         Pick one explicitly:\n  \
         • --daemon /path/to/socket               (existing daemon you started)\n  \
         • --daemon deb                           (Linux: apt install + systemctl, requires sudo)\n  \
         • --daemon docker                        (cross-platform: docker compose; requires Docker)\n  \
         • --daemon clone                         (in-house: gh clone econ-v1/node + cargo run)\n  \
         • --daemon monorepo --monorepo-path PATH  (you already have a checkout)"
    ))
}

/// Walk up the directory tree looking for a `Cargo.toml` whose contents
/// include `apps/server` (workspace member). Stops at the filesystem
/// root. None if we never find one.
fn find_monorepo_above(start: &std::path::Path) -> Option<PathBuf> {
    let mut current = Some(start.to_path_buf());
    while let Some(dir) = current {
        let cargo_toml = dir.join("Cargo.toml");
        if cargo_toml.exists() {
            if let Ok(contents) = std::fs::read_to_string(&cargo_toml) {
                if contents.contains("\"apps/server\"") || contents.contains("apps/server") {
                    return Some(dir);
                }
            }
        }
        current = dir.parent().map(|p| p.to_path_buf());
    }
    None
}

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
}