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