Skip to main content

mecha10_cli/services/process/
resolve.rs

1//! Binary path resolution for `ProcessService`
2//!
3//! Handles finding and resolving paths to node binaries, the
4//! `mecha10-node-runner` binary, and the `mecha10` CLI binary itself, across
5//! global installs and local builds.
6
7use super::ProcessService;
8use crate::paths;
9use anyhow::Result;
10
11impl ProcessService {
12    /// Find globally installed binary for a node
13    ///
14    /// Searches in:
15    /// 1. ~/.cargo/bin/{node_name}
16    /// 2. ~/.mecha10/bin/{node_name}
17    ///
18    /// # Arguments
19    ///
20    /// * `node_name` - Name of the node (e.g., "camera")
21    ///
22    /// # Returns
23    ///
24    /// Path to the binary if found, None otherwise
25    pub fn find_global_binary(node_name: &str) -> Option<std::path::PathBuf> {
26        let cargo_bin = paths::user::cargo_bin(node_name);
27        if cargo_bin.exists() && cargo_bin.is_file() {
28            return Some(cargo_bin);
29        }
30
31        let mecha10_bin = paths::user::bin(node_name);
32        if mecha10_bin.exists() && mecha10_bin.is_file() {
33            return Some(mecha10_bin);
34        }
35
36        None
37    }
38
39    /// Resolve binary path for a node with smart resolution
40    ///
41    /// Resolution strategy:
42    /// 1. If global binary exists: use global binary
43    /// 2. Fallback: use local build path
44    ///
45    /// # Arguments
46    ///
47    /// * `node_name` - Name of the node
48    /// * `is_monorepo_node` - Whether this is a framework node
49    /// * `project_name` - Name of the project
50    ///
51    /// # Returns
52    ///
53    /// Path to the binary to execute
54    pub fn resolve_node_binary(node_name: &str, is_monorepo_node: bool, project_name: &str) -> String {
55        if is_monorepo_node {
56            if let Some(global_path) = Self::find_global_binary(node_name) {
57                return global_path.to_string_lossy().to_string();
58            }
59        }
60
61        Self::get_local_binary_path(node_name, is_monorepo_node, project_name)
62    }
63
64    /// Get local binary path (in target/ directory)
65    fn get_local_binary_path(node_name: &str, is_monorepo_node: bool, project_name: &str) -> String {
66        if is_monorepo_node {
67            paths::target_path("release", project_name)
68        } else {
69            paths::target_path("release", node_name)
70        }
71    }
72
73    /// Resolve path to mecha10-node-runner binary
74    ///
75    /// Resolution strategy:
76    /// 1. Global installation: ~/.cargo/bin/mecha10-node-runner
77    /// 2. Fallback: "mecha10-node-runner" (rely on PATH)
78    ///
79    /// # Returns
80    ///
81    /// Path to the mecha10-node-runner binary
82    pub fn resolve_node_runner_path() -> String {
83        if let Some(global_path) = Self::find_global_binary("mecha10-node-runner") {
84            return global_path.to_string_lossy().to_string();
85        }
86
87        "mecha10-node-runner".to_string()
88    }
89
90    /// Get the version for a node from the project config
91    pub(super) fn get_node_version_from_config(&self, node_identifier: &str) -> Option<String> {
92        use crate::types::ProjectConfig;
93
94        let config_path = std::path::Path::new(paths::PROJECT_CONFIG);
95        if !config_path.exists() {
96            return None;
97        }
98
99        let config_content = std::fs::read_to_string(config_path).ok()?;
100        let config: ProjectConfig = serde_json::from_str(&config_content).ok()?;
101
102        config.nodes.get_version(node_identifier).map(|s| s.to_string())
103    }
104
105    /// Check whether disk-based logging is enabled for the current project's *current*
106    /// environment (LAB-2041).
107    ///
108    /// Reads `environments.<current_env>.logging.disk_logging_enabled` from the
109    /// project's `mecha10.json`, where `<current_env>` is whatever
110    /// `EnvironmentsConfig::current_env_name` resolves to (`MECHA10_ENV`, defaulting to
111    /// "dev"). Defaults to `true` (existing behavior) when no project config is
112    /// present, it can't be parsed, or the current environment has no `logging`
113    /// section, so projects without an explicit override are unaffected.
114    ///
115    /// Edge/robot deployments that already ship logs to a remote server can set this to
116    /// `false` on their `prod`/`staging` environment to avoid unbounded local disk
117    /// growth and repeated writes wearing out SD/USB storage, without disabling disk
118    /// logging in `dev` too. This has no effect on remote/network log shipping.
119    pub(super) fn disk_logging_enabled(&self) -> bool {
120        use crate::types::ProjectConfig;
121
122        let config_path = std::path::Path::new(paths::PROJECT_CONFIG);
123        if !config_path.exists() {
124            return true;
125        }
126
127        let config_content = match std::fs::read_to_string(config_path) {
128            Ok(c) => c,
129            Err(_) => return true,
130        };
131
132        let config: ProjectConfig = match serde_json::from_str(&config_content) {
133            Ok(c) => c,
134            Err(_) => return true,
135        };
136
137        config.environments.disk_logging_enabled()
138    }
139
140    /// Check if a node is a local project node (defined in nodes/ directory)
141    pub(super) fn is_local_project_node(&self, node_name: &str) -> bool {
142        use crate::types::{NodeSource, ProjectConfig};
143
144        let config_path = std::path::Path::new(paths::PROJECT_CONFIG);
145        if !config_path.exists() {
146            return false;
147        }
148
149        let config_content = match std::fs::read_to_string(config_path) {
150            Ok(c) => c,
151            Err(_) => return false,
152        };
153
154        let config: ProjectConfig = match serde_json::from_str(&config_content) {
155            Ok(c) => c,
156            Err(_) => return false,
157        };
158
159        config
160            .nodes
161            .find_by_name(node_name)
162            .map(|spec| spec.source == NodeSource::Project)
163            .unwrap_or(false)
164    }
165
166    /// Resolve path to mecha10 CLI binary
167    pub(super) fn resolve_cli_binary() -> Result<String> {
168        if let Some(path) = Self::find_global_binary("mecha10") {
169            return Ok(path.to_string_lossy().to_string());
170        }
171
172        let locations = [paths::user::local_bin("mecha10"), paths::user::cargo_bin("mecha10")];
173
174        for path in &locations {
175            if path.exists() {
176                return Ok(path.to_string_lossy().to_string());
177            }
178        }
179
180        if let Ok(exe) = std::env::current_exe() {
181            return Ok(exe.to_string_lossy().to_string());
182        }
183
184        anyhow::bail!("Could not find mecha10 CLI binary. Ensure it's installed and in your PATH.")
185    }
186
187    /// Check if mecha10-node-runner is available
188    pub fn is_node_runner_available() -> bool {
189        Self::find_global_binary("mecha10-node-runner").is_some()
190    }
191}