mecha10-cli 0.6.3

Mecha10 CLI tool
Documentation
//! Binary path resolution for `ProcessService`
//!
//! Handles finding and resolving paths to node binaries, the
//! `mecha10-node-runner` binary, and the `mecha10` CLI binary itself, across
//! global installs and local builds.

use super::ProcessService;
use crate::paths;
use anyhow::Result;

impl ProcessService {
    /// Find globally installed binary for a node
    ///
    /// Searches in:
    /// 1. ~/.cargo/bin/{node_name}
    /// 2. ~/.mecha10/bin/{node_name}
    ///
    /// # Arguments
    ///
    /// * `node_name` - Name of the node (e.g., "camera")
    ///
    /// # Returns
    ///
    /// Path to the binary if found, None otherwise
    pub fn find_global_binary(node_name: &str) -> Option<std::path::PathBuf> {
        // Try ~/.cargo/bin/ first
        let cargo_bin = paths::user::cargo_bin(node_name);
        if cargo_bin.exists() && cargo_bin.is_file() {
            return Some(cargo_bin);
        }

        // Try ~/.mecha10/bin/
        let mecha10_bin = paths::user::bin(node_name);
        if mecha10_bin.exists() && mecha10_bin.is_file() {
            return Some(mecha10_bin);
        }

        None
    }

    /// Resolve binary path for a node with smart resolution
    ///
    /// Resolution strategy:
    /// 1. If global binary exists: use global binary
    /// 2. Fallback: use local build path
    ///
    /// # Arguments
    ///
    /// * `node_name` - Name of the node
    /// * `is_monorepo_node` - Whether this is a framework node
    /// * `project_name` - Name of the project
    ///
    /// # Returns
    ///
    /// Path to the binary to execute
    pub fn resolve_node_binary(node_name: &str, is_monorepo_node: bool, project_name: &str) -> String {
        // For monorepo (framework) nodes, check for global installation
        if is_monorepo_node {
            if let Some(global_path) = Self::find_global_binary(node_name) {
                return global_path.to_string_lossy().to_string();
            }
        }

        // Fallback to local build
        Self::get_local_binary_path(node_name, is_monorepo_node, project_name)
    }

    /// Get local binary path (in target/ directory)
    fn get_local_binary_path(node_name: &str, is_monorepo_node: bool, project_name: &str) -> String {
        if is_monorepo_node {
            // Monorepo nodes run via the project binary with 'node' subcommand
            paths::target_path("release", project_name)
        } else {
            // Local nodes have their own binary
            paths::target_path("release", node_name)
        }
    }

    /// Resolve path to mecha10-node-runner binary
    ///
    /// Resolution strategy:
    /// 1. Global installation: ~/.cargo/bin/mecha10-node-runner
    /// 2. Fallback: "mecha10-node-runner" (rely on PATH)
    ///
    /// # Returns
    ///
    /// Path to the mecha10-node-runner binary
    pub fn resolve_node_runner_path() -> String {
        // Try global installation
        if let Some(global_path) = Self::find_global_binary("mecha10-node-runner") {
            return global_path.to_string_lossy().to_string();
        }

        // Fallback: rely on PATH
        "mecha10-node-runner".to_string()
    }

    /// Get the version for a node from the project config
    pub(super) fn get_node_version_from_config(&self, node_identifier: &str) -> Option<String> {
        use crate::types::ProjectConfig;

        let config_path = std::path::Path::new(paths::PROJECT_CONFIG);
        if !config_path.exists() {
            return None;
        }

        let config_content = std::fs::read_to_string(config_path).ok()?;
        let config: ProjectConfig = serde_json::from_str(&config_content).ok()?;

        config.nodes.get_version(node_identifier).map(|s| s.to_string())
    }

    /// Check whether disk-based logging is enabled for the current project's *current*
    /// environment (LAB-2041).
    ///
    /// Reads `environments.<current_env>.logging.disk_logging_enabled` from the
    /// project's `mecha10.json`, where `<current_env>` is whatever
    /// `EnvironmentsConfig::current_env_name` resolves to (`MECHA10_ENV`, defaulting to
    /// "dev"). Defaults to `true` (existing behavior) when no project config is
    /// present, it can't be parsed, or the current environment has no `logging`
    /// section, so projects without an explicit override are unaffected.
    ///
    /// Edge/robot deployments that already ship logs to a remote server can set this to
    /// `false` on their `prod`/`staging` environment to avoid unbounded local disk
    /// growth and repeated writes wearing out SD/USB storage, without disabling disk
    /// logging in `dev` too. This has no effect on remote/network log shipping.
    pub(super) fn disk_logging_enabled(&self) -> bool {
        use crate::types::ProjectConfig;

        let config_path = std::path::Path::new(paths::PROJECT_CONFIG);
        if !config_path.exists() {
            return true;
        }

        let config_content = match std::fs::read_to_string(config_path) {
            Ok(c) => c,
            Err(_) => return true,
        };

        let config: ProjectConfig = match serde_json::from_str(&config_content) {
            Ok(c) => c,
            Err(_) => return true,
        };

        config.environments.disk_logging_enabled()
    }

    /// Check if a node is a local project node (defined in nodes/ directory)
    pub(super) fn is_local_project_node(&self, node_name: &str) -> bool {
        use crate::types::{NodeSource, ProjectConfig};

        let config_path = std::path::Path::new(paths::PROJECT_CONFIG);
        if !config_path.exists() {
            return false;
        }

        let config_content = match std::fs::read_to_string(config_path) {
            Ok(c) => c,
            Err(_) => return false,
        };

        let config: ProjectConfig = match serde_json::from_str(&config_content) {
            Ok(c) => c,
            Err(_) => return false,
        };

        // Check if node exists as a project node (nodes/<name>)
        config
            .nodes
            .find_by_name(node_name)
            .map(|spec| spec.source == NodeSource::Project)
            .unwrap_or(false)
    }

    /// Resolve path to mecha10 CLI binary
    pub(super) fn resolve_cli_binary() -> Result<String> {
        // First try to find mecha10 in PATH
        if let Some(path) = Self::find_global_binary("mecha10") {
            return Ok(path.to_string_lossy().to_string());
        }

        // Try common install locations
        let locations = [paths::user::local_bin("mecha10"), paths::user::cargo_bin("mecha10")];

        for path in &locations {
            if path.exists() {
                return Ok(path.to_string_lossy().to_string());
            }
        }

        // Fall back to current executable (we are mecha10!)
        if let Ok(exe) = std::env::current_exe() {
            return Ok(exe.to_string_lossy().to_string());
        }

        anyhow::bail!("Could not find mecha10 CLI binary. Ensure it's installed and in your PATH.")
    }

    /// Check if mecha10-node-runner is available
    pub fn is_node_runner_available() -> bool {
        Self::find_global_binary("mecha10-node-runner").is_some()
    }
}