Skip to main content

mecha10_cli/services/process/
resolve.rs

1//! Binary path resolution for `ProcessService`
2//!
3//! Handles finding globally installed binaries and resolving paths to the
4//! `mecha10-node-runner` binary and the `mecha10` CLI binary itself.
5
6use super::ProcessService;
7use crate::paths;
8use anyhow::Result;
9
10impl ProcessService {
11    /// Find globally installed binary for a node. Searches `~/.cargo/bin/{node_name}` then
12    /// `~/.mecha10/bin/{node_name}`.
13    pub fn find_global_binary(node_name: &str) -> Option<std::path::PathBuf> {
14        let cargo_bin = paths::user::cargo_bin(node_name);
15        if cargo_bin.exists() && cargo_bin.is_file() {
16            return Some(cargo_bin);
17        }
18
19        let mecha10_bin = paths::user::bin(node_name);
20        if mecha10_bin.exists() && mecha10_bin.is_file() {
21            return Some(mecha10_bin);
22        }
23
24        None
25    }
26
27    /// Resolve path to mecha10-node-runner binary: global installation at
28    /// `~/.cargo/bin/mecha10-node-runner`, else falls back to relying on PATH.
29    pub fn resolve_node_runner_path() -> String {
30        if let Some(global_path) = Self::find_global_binary("mecha10-node-runner") {
31            return global_path.to_string_lossy().to_string();
32        }
33
34        "mecha10-node-runner".to_string()
35    }
36
37    /// Get the version for a node from the project config
38    pub(super) fn get_node_version_from_config(&self, node_identifier: &str) -> Option<String> {
39        use crate::types::ProjectConfig;
40
41        let config_path = std::path::Path::new(paths::PROJECT_CONFIG);
42        if !config_path.exists() {
43            return None;
44        }
45
46        let config_content = std::fs::read_to_string(config_path).ok()?;
47        let config: ProjectConfig = serde_json::from_str(&config_content).ok()?;
48
49        config.nodes.get_version(node_identifier).map(|s| s.to_string())
50    }
51
52    /// Check whether disk-based logging is enabled for the current project's *current*
53    /// environment (LAB-2041). Reads `environments.<current_env>.logging.disk_logging_enabled`
54    /// from `mecha10.json`, defaulting to `true` when absent or unparseable.
55    pub(super) fn disk_logging_enabled(&self) -> bool {
56        use crate::types::ProjectConfig;
57
58        let config_path = std::path::Path::new(paths::PROJECT_CONFIG);
59        if !config_path.exists() {
60            return true;
61        }
62
63        let config_content = match std::fs::read_to_string(config_path) {
64            Ok(c) => c,
65            Err(_) => return true,
66        };
67
68        let config: ProjectConfig = match serde_json::from_str(&config_content) {
69            Ok(c) => c,
70            Err(_) => return true,
71        };
72
73        config.environments.disk_logging_enabled()
74    }
75
76    /// Check if a node is a local project node (declared with `NodeSource::Project` in
77    /// `mecha10.json`).
78    pub(super) fn is_local_project_node(&self, node_name: &str) -> bool {
79        mecha10_cli_core::types::is_local_project_node(std::path::Path::new(paths::PROJECT_CONFIG), node_name)
80    }
81
82    /// Resolve path to mecha10 CLI binary
83    ///
84    /// Tiers: (1) global installation at ~/.cargo/bin or ~/.mecha10/bin, (2) local/cargo bin
85    /// fallback locations, (3) the currently running executable.
86    pub(super) fn resolve_cli_binary() -> Result<String> {
87        mecha10_binary_naming::try_resolution_tiers!("mecha10 CLI binary", {
88            "global binary" => Self::find_global_binary("mecha10")
89                .map(|path| path.to_string_lossy().to_string())
90                .ok_or_else(|| "not found at ~/.cargo/bin or ~/.mecha10/bin".to_string()),
91
92            "local/cargo bin fallback locations" => {
93                let locations = [paths::user::local_bin("mecha10"), paths::user::cargo_bin("mecha10")];
94                locations
95                    .iter()
96                    .find(|path| path.exists())
97                    .map(|path| path.to_string_lossy().to_string())
98                    .ok_or_else(|| "not found at local_bin/cargo_bin fallback locations".to_string())
99            },
100
101            "current executable" => std::env::current_exe()
102                .map(|exe| exe.to_string_lossy().to_string())
103                .map_err(|e| format!("current_exe() failed: {e}")),
104        })
105        .map_err(|e| anyhow::anyhow!("{}. Ensure it's installed and in your PATH.", e))
106    }
107
108    /// Check if mecha10-node-runner is available
109    pub fn is_node_runner_available() -> bool {
110        Self::find_global_binary("mecha10-node-runner").is_some()
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    /// Falls back to the "current executable" tier when no global/local/cargo-bin binary exists.
119    #[test]
120    fn resolve_cli_binary_falls_back_to_current_exe_when_no_global_or_local_binary_exists() {
121        let resolved = ProcessService::resolve_cli_binary().expect("current_exe() tier must succeed");
122        assert!(!resolved.is_empty());
123    }
124}