Skip to main content

mecha10_cli_core/
paths.rs

1//! Centralized path definitions for the CLI
2//!
3//! All hardcoded paths should be defined here to ensure consistency across the codebase.
4
5#![allow(dead_code)]
6
7/// Project configuration file
8pub const PROJECT_CONFIG: &str = "mecha10.json";
9
10/// Project directories
11pub mod project {
12    /// Node source code directory
13    pub const NODES_DIR: &str = "nodes";
14
15    /// Framework node (`@mecha10` scope) schema directory, under the project's `nodes/`
16    /// directory. This is where a framework node's `<name>.schema.json` lives.
17    pub const NODES_MECHA10_DIR: &str = "nodes/@mecha10";
18
19    /// The framework node's schema *directory* (no `<name>.schema.json` suffix) under
20    /// `nodes/@mecha10/<node_name>`.
21    pub fn framework_node_schema_dir(node_name: &str) -> String {
22        format!("{}/{}", NODES_MECHA10_DIR, node_name)
23    }
24
25    /// Driver source code directory
26    pub const DRIVERS_DIR: &str = "drivers";
27
28    /// Types directory
29    pub const TYPES_DIR: &str = "types";
30
31    /// Behavior trees directory - nested under `configs/` (LAB-2072) so `mecha10 config
32    /// push`/`pull`/`list` genuinely sync it like any other config, instead of living as a
33    /// sibling top-level directory `collect_configs_from_dir` never walked.
34    pub const BEHAVIORS_DIR: &str = "configs/behaviors";
35
36    /// Models directory (ONNX models, etc.)
37    pub const MODELS_DIR: &str = "models";
38
39    /// Model catalog subdirectory, bind-mounted into the `ollama` Compose service as
40    /// `/root/.ollama/models` (see `docker-compose.yml.template`).
41    pub const MODELS_CATALOG_DIR: &str = "models/catalog";
42
43    /// Logs directory
44    pub const LOGS_DIR: &str = "logs";
45
46    /// Simulation directory - nested under `configs/` (moved from a top-level `simulation/`
47    /// sibling) so `mecha10 config push`/`pull` syncs it like any other config, the same reason
48    /// `BEHAVIORS_DIR` moved under `configs/` in LAB-2072.
49    pub const SIMULATION_DIR: &str = "configs/simulation";
50
51    /// Simulation models directory
52    pub const SIMULATION_MODELS_DIR: &str = "configs/simulation/models";
53
54    /// Simulation environments directory
55    pub const SIMULATION_ENVIRONMENTS_DIR: &str = "configs/simulation/environments";
56
57    /// Simulation billboard-image assets directory (`type: "image"` `environment.json`/
58    /// `task.json` objects' `source` fields point here) - flat, no per-environment nesting.
59    pub const SIMULATION_IMAGES_DIR: &str = "configs/simulation/images";
60
61    /// RL task-curriculum configs directory, scaffolded from the templates package's bundled
62    /// catalog at `mecha10 init` time. `mecha10-rl`'s `AssetResolver` resolves task configs
63    /// (e.g. `task1`) from here for training (`mecha10 train`).
64    pub const RL_TASKS_DIR: &str = "configs/rl/tasks";
65
66    /// Simulation output directory (generated robot/scene files)
67    pub const SIMULATION_OUTPUT_DIR: &str = "configs/simulation/output";
68
69    /// Cargo build target directory
70    pub const TARGET_DIR: &str = "target";
71
72    /// Source directory
73    pub const SRC_DIR: &str = "src";
74
75    /// Local (gitignored) project-scoped state directory - mirrors the `~/.mecha10`
76    /// user-home convention (`user::MECHA10_DIR`) but scoped to this project checkout.
77    pub const STATE_DIR: &str = ".mecha10";
78
79    /// Cache file for `mecha10 dev`'s project-level Python dependency preflight gate.
80    pub const DEV_PREFLIGHT_CACHE_FILE: &str = ".mecha10/dev-preflight-cache.json";
81}
82
83/// Configuration paths within project
84pub mod config {
85    use super::project;
86
87    /// Base configs directory
88    pub const DIR: &str = "configs";
89
90    /// Node configs base directory
91    pub const NODES_DIR: &str = "configs/nodes";
92
93    /// Framework node configs (@mecha10 scope)
94    pub const NODES_MECHA10_DIR: &str = "configs/nodes/@mecha10";
95
96    /// Framework-tier (`@mecha10`) `config.json` path for `name` under `category_dir`.
97    fn framework_config(category_dir: &str, name: &str) -> String {
98        format!("{category_dir}/@mecha10/{name}/config.json")
99    }
100
101    /// Project-tier (plain name) `config.json` path for `name` under `category_dir`.
102    fn project_config(category_dir: &str, name: &str) -> String {
103        format!("{category_dir}/{name}/config.json")
104    }
105
106    /// The 2-tier `config.json` lookup candidates for `name` under `category_dir`, in canonical
107    /// resolution order: framework (`@mecha10`) -> project (plain name).
108    fn config_candidates(category_dir: &str, name: &str) -> [String; 2] {
109        [framework_config(category_dir, name), project_config(category_dir, name)]
110    }
111
112    /// Get config path for a framework node
113    pub fn framework_node(node_name: &str) -> String {
114        framework_config(NODES_DIR, node_name)
115    }
116
117    /// Get config path for a project node (plain name)
118    pub fn project_node(node_name: &str) -> String {
119        project_config(NODES_DIR, node_name)
120    }
121
122    /// Get the framework node's config *directory* (no `config.json` suffix) - the `@mecha10`
123    /// counterpart to [`framework_node`], for callers that need the directory itself (e.g. to
124    /// copy a whole config template tree into it) rather than the `config.json` file path.
125    pub fn framework_node_dir(node_name: &str) -> String {
126        format!("{}/{}", NODES_MECHA10_DIR, node_name)
127    }
128
129    /// The 2-tier `config.json` lookup candidates for a node, in canonical resolution order:
130    /// framework (`@mecha10`) -> project (plain name).
131    pub fn node_config_candidates(node_name: &str) -> [String; 2] {
132        config_candidates(NODES_DIR, node_name)
133    }
134
135    /// Get config path for a framework-default behavior (bundled into the project at scaffold
136    /// time under `configs/behaviors/@mecha10/<name>/config.json`).
137    pub fn framework_behavior(behavior_name: &str) -> String {
138        framework_config(project::BEHAVIORS_DIR, behavior_name)
139    }
140
141    /// The framework behavior's directory (no `config.json` suffix) - the `@mecha10`
142    /// counterpart to [`framework_behavior`].
143    pub fn framework_behavior_dir(behavior_name: &str) -> String {
144        format!("{}/@mecha10/{}", project::BEHAVIORS_DIR, behavior_name)
145    }
146
147    /// Get config path for a project behavior (plain name:
148    /// `configs/behaviors/<name>/config.json`).
149    pub fn project_behavior(behavior_name: &str) -> String {
150        project_config(project::BEHAVIORS_DIR, behavior_name)
151    }
152
153    /// The 2-tier `config.json` lookup candidates for a behavior, in override-precedence order
154    /// (highest first): project (plain name) -> framework (`@mecha10`).
155    pub fn behavior_config_candidates(behavior_name: &str) -> [String; 2] {
156        [project_behavior(behavior_name), framework_behavior(behavior_name)]
157    }
158}
159
160/// Docker-related paths within project
161pub mod docker {
162    /// Docker directory
163    pub const DIR: &str = "docker";
164
165    /// Main docker-compose file
166    pub const COMPOSE_FILE: &str = "docker/docker-compose.yml";
167
168    /// Remote nodes docker-compose file
169    pub const COMPOSE_REMOTE_FILE: &str = "docker/docker-compose.remote.yml";
170
171    /// Remote nodes Dockerfile
172    pub const DOCKERFILE_REMOTE: &str = "docker/Dockerfile.remote";
173
174    /// Remote container entrypoint script (execs mecha10-launcher)
175    pub const ENTRYPOINT_REMOTE: &str = "docker/entrypoint.sh";
176
177    /// rl target docker-compose file - runs a trained RL policy in production
178    pub const COMPOSE_RL_FILE: &str = "docker/docker-compose.rl.yml";
179
180    /// rl target Dockerfile
181    pub const DOCKERFILE_RL: &str = "docker/Dockerfile.rl";
182
183    /// rl target container entrypoint script (execs mecha10-launcher --target rl) - a distinct
184    /// filename from `ENTRYPOINT_REMOTE` since both `docker/entrypoint.sh` and this file coexist
185    /// in the same project (remote and rl are independent, both-scaffolded targets).
186    pub const ENTRYPOINT_RL: &str = "docker/entrypoint-rl.sh";
187}
188
189/// Environment files
190pub mod env {
191    /// Environment example file
192    pub const EXAMPLE: &str = ".env.example";
193
194    /// Environment file
195    pub const FILE: &str = ".env";
196}
197
198/// Meta files (README, gitignore, etc.)
199pub mod meta {
200    /// README file
201    pub const README: &str = "README.md";
202
203    /// Git ignore file
204    pub const GITIGNORE: &str = ".gitignore";
205
206    /// Package.json for Node.js tooling
207    pub const PACKAGE_JSON: &str = "package.json";
208
209    /// Python requirements
210    pub const REQUIREMENTS_TXT: &str = "requirements.txt";
211
212    /// Pinned Python version for `mecha10 setup` to target (see
213    /// `find_python_executable` in `packages/cli/src/handlers/setup.rs`).
214    pub const PYTHON_VERSION: &str = ".python-version";
215}
216
217/// Rust project files
218pub mod rust {
219    /// Cargo workspace manifest
220    pub const CARGO_TOML: &str = "Cargo.toml";
221
222    /// Cargo lock file
223    pub const CARGO_LOCK: &str = "Cargo.lock";
224
225    /// Main entry point
226    pub const MAIN_RS: &str = "src/main.rs";
227
228    /// Library entry point
229    pub const LIB_RS: &str = "src/lib.rs";
230
231    /// Build script
232    pub const BUILD_RS: &str = "build.rs";
233
234    /// Rustfmt config
235    pub const RUSTFMT_TOML: &str = "rustfmt.toml";
236}
237
238/// Model files within a model directory
239pub mod model {
240    /// ONNX model file
241    pub const ONNX_FILE: &str = "model.onnx";
242
243    /// Labels file
244    pub const LABELS_FILE: &str = "labels.txt";
245
246    /// Model config file
247    pub const CONFIG_FILE: &str = "config.json";
248}
249
250/// User-level paths in home directory
251pub mod user {
252    use std::path::PathBuf;
253
254    /// Base mecha10 directory in user home.
255    ///
256    /// Re-exports `mecha10_core::fs_utils::MECHA10_DIR`, shared with `launcher-service`.
257    pub const MECHA10_DIR: &str = mecha10_core::fs_utils::MECHA10_DIR;
258
259    /// Credentials file, relative to the user home directory.
260    ///
261    /// Documentation-only - `credentials_file()` below is the actual source of truth.
262    pub const CREDENTIALS_FILE: &str = ".mecha10/credentials.json";
263
264    /// Templates cache directory
265    pub const TEMPLATES_DIR: &str = ".mecha10/templates";
266
267    /// Binary installation directory
268    pub const BIN_DIR: &str = ".mecha10/bin";
269
270    /// Get the base mecha10 directory path
271    pub fn mecha10_dir() -> PathBuf {
272        dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")).join(MECHA10_DIR)
273    }
274
275    /// Get the credentials file path.
276    ///
277    /// Delegates to `mecha10-auth`'s `credentials::default_credentials_path()`.
278    pub fn credentials_file() -> PathBuf {
279        mecha10_auth::default_credentials_path()
280    }
281
282    /// Get the templates cache directory
283    pub fn templates_dir() -> PathBuf {
284        mecha10_dir().join("templates")
285    }
286
287    /// Get the binary directory
288    pub fn bin_dir() -> PathBuf {
289        mecha10_dir().join("bin")
290    }
291
292    /// Get path to a binary in the mecha10 bin directory
293    pub fn bin(name: &str) -> PathBuf {
294        bin_dir().join(name)
295    }
296
297    /// Get path to a binary in cargo bin directory
298    pub fn cargo_bin(name: &str) -> PathBuf {
299        home_bin_dir(".cargo", name)
300    }
301
302    /// Get path to a binary in local bin directory
303    pub fn local_bin(name: &str) -> PathBuf {
304        home_bin_dir(".local", name)
305    }
306
307    /// Get path to a binary in a `bin` subdirectory of the given home-relative directory
308    /// (e.g. `~/.cargo/bin/{name}`, `~/.local/bin/{name}`).
309    fn home_bin_dir(home_subdir: &str, name: &str) -> PathBuf {
310        mecha10_core::fs_utils::home_subpath(&[home_subdir, "bin", name])
311    }
312}
313
314/// Paths used inside Docker containers
315pub mod container {
316    /// Project mount point in container
317    pub const PROJECT_ROOT: &str = "/project";
318
319    /// Framework mount point in container
320    pub const FRAMEWORK_ROOT: &str = "/mecha10";
321
322    /// App directory (for mecha10-remote)
323    pub const APP_ROOT: &str = "/app";
324
325    /// Config file in mecha10-remote container
326    pub const REMOTE_CONFIG: &str = "/app/mecha10.json";
327
328    /// Get project-relative path inside container
329    pub fn project_path(relative: &str) -> String {
330        format!("{}/{}", PROJECT_ROOT, relative)
331    }
332
333    /// Get framework-relative path inside container
334    pub fn framework_path(relative: &str) -> String {
335        format!("{}/{}", FRAMEWORK_ROOT, relative)
336    }
337}
338
339/// Paths within the mecha10 framework source
340pub mod framework {
341    /// Packages directory
342    pub const PACKAGES_DIR: &str = "packages";
343
344    /// Nodes package directory
345    pub const NODES_DIR: &str = "packages/nodes";
346
347    /// Drivers package directory
348    pub const DRIVERS_DIR: &str = "packages/drivers";
349
350    /// Marker directory used to detect the mecha10 framework root
351    pub const ROOT_MARKER_DIR: &str = "packages/core";
352
353    /// Release binary
354    pub const RELEASE_BINARY: &str = "target/release/mecha10";
355
356    /// Debug binary
357    pub const DEBUG_BINARY: &str = "target/debug/mecha10";
358
359    /// Get node config path within framework
360    pub fn node_config(node_name: &str) -> String {
361        format!("packages/nodes/{}/configs/config.json", node_name)
362    }
363}
364
365/// External service URLs
366pub mod urls {
367    /// Default base URL for the public `mecha10-api` service. All framework/runtime artifact
368    /// downloads are proxied through its `/api/downloads/*` routes.
369    pub const MECHA10_API_URL: &str = "https://mecha.industries/api";
370
371    /// Build the `mecha10-api` base URL (e.g. `https://mecha.industries/api`).
372    ///
373    /// Override at runtime with the `MECHA10_API_URL` env var.
374    pub fn mecha10_api_base_url() -> String {
375        std::env::var("MECHA10_API_URL")
376            .unwrap_or_else(|_| MECHA10_API_URL.to_string())
377            .trim_end_matches('/')
378            .to_string()
379    }
380
381    /// Join `path` onto [`mecha10_api_base_url`]. `path` must not start with a leading
382    /// `/api` of its own, or the request doubles up to `.../api/api/...` and 404s.
383    pub fn mecha10_api_path(path: &str) -> String {
384        format!("{}/{}", mecha10_api_base_url(), path.trim_start_matches('/'))
385    }
386
387    /// Default authentication URL
388    pub const AUTH_URL: &str = "https://mecha.industries/api/auth";
389}
390
391/// Build a target output path
392pub fn target_path(profile: &str, binary: &str) -> String {
393    format!("target/{}/{}", profile, binary)
394}
395
396/// Build a target output path with optional target triple
397pub fn target_path_with_triple(target: Option<&str>, profile: &str) -> String {
398    match target {
399        Some(t) => format!("target/{}/{}", t, profile),
400        None => format!("target/{}", profile),
401    }
402}
403
404#[cfg(test)]
405mod tests {
406    use super::*;
407
408    #[test]
409    fn credentials_file_matches_mecha10_auth_source_of_truth() {
410        assert_eq!(user::credentials_file(), mecha10_auth::default_credentials_path());
411    }
412
413    /// `cargo_bin`/`local_bin` share a `home_bin_dir` helper that only differs on which
414    /// home-relative directory it joins.
415    #[test]
416    fn cargo_bin_and_local_bin_join_name_under_their_own_home_subdir() {
417        let home = dirs::home_dir().expect("home dir must resolve in test environment");
418        assert_eq!(
419            user::cargo_bin("mecha10"),
420            home.join(".cargo").join("bin").join("mecha10")
421        );
422        assert_eq!(
423            user::local_bin("mecha10"),
424            home.join(".local").join("bin").join("mecha10")
425        );
426    }
427
428    #[test]
429    fn simulation_project_dirs_share_the_simulation_root() {
430        assert_eq!(project::SIMULATION_MODELS_DIR, "configs/simulation/models");
431        assert_eq!(project::SIMULATION_ENVIRONMENTS_DIR, "configs/simulation/environments");
432        for dir in [project::SIMULATION_MODELS_DIR, project::SIMULATION_ENVIRONMENTS_DIR] {
433            assert!(dir.starts_with(&format!("{}/", project::SIMULATION_DIR)));
434        }
435    }
436
437    /// Task-curriculum configs live under `configs/rl/`, NOT `SIMULATION_DIR` - they're a
438    /// project's RL configuration, not a physics/rendering asset.
439    #[test]
440    fn rl_tasks_dir_lives_under_configs_rl_not_simulation() {
441        assert_eq!(project::RL_TASKS_DIR, "configs/rl/tasks");
442        assert!(!project::RL_TASKS_DIR.starts_with(&format!("{}/", project::SIMULATION_DIR)));
443    }
444
445    #[test]
446    fn test_config_paths() {
447        assert_eq!(
448            config::framework_node("object-detector"),
449            "configs/nodes/@mecha10/object-detector/config.json"
450        );
451        assert_eq!(config::project_node("camera"), "configs/nodes/camera/config.json");
452    }
453
454    #[test]
455    fn test_framework_node_dir() {
456        assert_eq!(config::framework_node_dir("speaker"), "configs/nodes/@mecha10/speaker");
457    }
458
459    #[test]
460    fn test_framework_node_schema_dir_lives_under_nodes_not_configs_nodes() {
461        assert_eq!(project::framework_node_schema_dir("speaker"), "nodes/@mecha10/speaker");
462        assert_ne!(
463            project::framework_node_schema_dir("speaker"),
464            config::framework_node_dir("speaker"),
465            "schema dir (wave 10) must not collide with the config.json dir"
466        );
467    }
468
469    #[test]
470    fn test_node_config_candidates_order() {
471        assert_eq!(
472            config::node_config_candidates("teleop"),
473            [
474                "configs/nodes/@mecha10/teleop/config.json".to_string(),
475                "configs/nodes/teleop/config.json".to_string(),
476            ]
477        );
478    }
479
480    #[test]
481    fn test_behavior_config_paths() {
482        assert_eq!(
483            config::framework_behavior("idle_wander"),
484            "configs/behaviors/@mecha10/idle_wander/config.json"
485        );
486        assert_eq!(
487            config::project_behavior("idle_wander"),
488            "configs/behaviors/idle_wander/config.json"
489        );
490    }
491
492    #[test]
493    fn test_framework_behavior_dir() {
494        assert_eq!(
495            config::framework_behavior_dir("idle-wander"),
496            "configs/behaviors/@mecha10/idle-wander"
497        );
498        assert_eq!(
499            config::framework_behavior("idle-wander"),
500            format!("{}/config.json", config::framework_behavior_dir("idle-wander"))
501        );
502    }
503
504    #[test]
505    fn test_behavior_config_candidates_order() {
506        assert_eq!(
507            config::behavior_config_candidates("idle_wander"),
508            [
509                "configs/behaviors/idle_wander/config.json".to_string(),
510                "configs/behaviors/@mecha10/idle_wander/config.json".to_string(),
511            ]
512        );
513    }
514
515    #[test]
516    fn test_target_path() {
517        assert_eq!(target_path("release", "my-node"), "target/release/my-node");
518        assert_eq!(
519            target_path_with_triple(Some("aarch64-unknown-linux-gnu"), "release"),
520            "target/aarch64-unknown-linux-gnu/release"
521        );
522        assert_eq!(target_path_with_triple(None, "debug"), "target/debug");
523    }
524
525    #[test]
526    fn test_container_paths() {
527        assert_eq!(
528            container::project_path("configs/test.json"),
529            "/project/configs/test.json"
530        );
531        assert_eq!(container::framework_path("packages/core"), "/mecha10/packages/core");
532    }
533
534    #[test]
535    fn test_mecha10_api_base_url_default() {
536        let _guard = ENV_MUTEX.lock().unwrap();
537        std::env::remove_var("MECHA10_API_URL");
538        assert_eq!(urls::mecha10_api_base_url(), "https://mecha.industries/api");
539    }
540
541    #[test]
542    fn test_mecha10_api_base_url_env_override() {
543        let _guard = ENV_MUTEX.lock().unwrap();
544        std::env::set_var("MECHA10_API_URL", "http://localhost:4000/api/");
545        assert_eq!(urls::mecha10_api_base_url(), "http://localhost:4000/api");
546        std::env::remove_var("MECHA10_API_URL");
547    }
548
549    #[test]
550    fn test_mecha10_api_path_does_not_double_the_api_segment() {
551        let _guard = ENV_MUTEX.lock().unwrap();
552        std::env::remove_var("MECHA10_API_URL");
553        assert_eq!(
554            urls::mecha10_api_path("/models/catalog"),
555            "https://mecha.industries/api/models/catalog"
556        );
557    }
558
559    #[test]
560    fn test_mecha10_api_path_works_without_a_leading_slash() {
561        let _guard = ENV_MUTEX.lock().unwrap();
562        std::env::remove_var("MECHA10_API_URL");
563        assert_eq!(
564            urls::mecha10_api_path("downloads/model/deepghs/yolos/yolov8n/model.onnx"),
565            "https://mecha.industries/api/downloads/model/deepghs/yolos/yolov8n/model.onnx"
566        );
567    }
568
569    /// Serializes tests that mutate process-wide env vars, since `cargo test` runs
570    /// tests in this module concurrently by default.
571    static ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
572}