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
4//! and make it easy to update paths across the codebase.
5//!
6//! # Organization
7//!
8//! - **Project paths**: Paths relative to project root (mecha10.json location)
9//! - **User paths**: Paths in user's home directory (~/.mecha10)
10//! - **Docker paths**: Paths used inside Docker containers
11//! - **Framework paths**: Paths within the mecha10 framework source
12
13// Allow dead code during migration - paths will be used incrementally
14#![allow(dead_code)]
15
16// =============================================================================
17// Project Structure - Paths relative to project root
18// =============================================================================
19
20/// Project configuration file
21pub const PROJECT_CONFIG: &str = "mecha10.json";
22
23/// Project directories
24pub mod project {
25    /// Node source code directory
26    pub const NODES_DIR: &str = "nodes";
27
28    /// Driver source code directory
29    pub const DRIVERS_DIR: &str = "drivers";
30
31    /// Types directory
32    pub const TYPES_DIR: &str = "types";
33
34    /// Behavior trees directory - nested under `configs/` (LAB-2072) so `mecha10 config
35    /// push`/`pull`/`list` genuinely sync it like any other config, instead of living as a
36    /// sibling top-level directory `collect_configs_from_dir` never walked.
37    pub const BEHAVIORS_DIR: &str = "configs/behaviors";
38
39    /// Assets directory
40    pub const ASSETS_DIR: &str = "assets";
41
42    /// Assets images subdirectory
43    pub const ASSETS_IMAGES_DIR: &str = "assets/images";
44
45    /// Models directory (ONNX models, etc.)
46    pub const MODELS_DIR: &str = "models";
47
48    /// Logs directory
49    pub const LOGS_DIR: &str = "logs";
50
51    /// Simulation directory
52    pub const SIMULATION_DIR: &str = "simulation";
53
54    /// Simulation models directory
55    pub const SIMULATION_MODELS_DIR: &str = "simulation/models";
56
57    /// Simulation environments directory
58    pub const SIMULATION_ENVIRONMENTS_DIR: &str = "simulation/environments";
59
60    /// Simulation task-curriculum configs directory (LAB-2019) - scaffolded from the templates
61    /// package's bundled catalog at `mecha10 init` time, alongside `SIMULATION_MODELS_DIR`/
62    /// `SIMULATION_ENVIRONMENTS_DIR`. `mecha10_rl.mujoco.cli serve --assets-path` (supervised by
63    /// `packages/nodes/simulator`) resolves task configs (e.g. `task1`) from here.
64    pub const SIMULATION_TASKS_DIR: &str = "simulation/tasks";
65
66    /// Get path to a model's config file
67    pub fn model_config(model_name: &str) -> String {
68        format!("simulation/models/{}/model.json", model_name)
69    }
70
71    /// Get path to an environment's config file
72    pub fn environment_config(env_name: &str) -> String {
73        format!("simulation/environments/{}/environment.json", env_name)
74    }
75
76    /// Simulation output directory (generated robot/scene files)
77    pub const SIMULATION_OUTPUT_DIR: &str = "simulation/output";
78
79    /// Cargo build target directory
80    pub const TARGET_DIR: &str = "target";
81
82    /// Source directory
83    pub const SRC_DIR: &str = "src";
84
85    /// Local (gitignored) project-scoped state directory - mirrors the `~/.mecha10`
86    /// user-home convention (`user::MECHA10_DIR`) but scoped to this project checkout, for
87    /// caches/state that are specific to one clone and shouldn't be committed to git (e.g.
88    /// `mecha10 dev`'s Python dependency preflight cache - see
89    /// `DEV_PREFLIGHT_CACHE_FILE`/`handlers/dev/ops/preflight.rs` in the `mecha10-cli` crate).
90    pub const STATE_DIR: &str = ".mecha10";
91
92    /// Cache file for `mecha10 dev`'s project-level Python dependency preflight gate - see
93    /// `handlers/dev/ops/preflight.rs` in the `mecha10-cli` crate. Purely a speed
94    /// optimization: a missing or stale-and-discarded cache always falls back to a real
95    /// scan, so nothing about correctness depends on this file existing or being fresh.
96    pub const DEV_PREFLIGHT_CACHE_FILE: &str = ".mecha10/dev-preflight-cache.json";
97}
98
99/// Configuration paths within project
100pub mod config {
101    use super::project;
102
103    /// Base configs directory
104    pub const DIR: &str = "configs";
105
106    /// Node configs base directory
107    pub const NODES_DIR: &str = "configs/nodes";
108
109    /// Framework node configs (@mecha10 scope)
110    pub const NODES_MECHA10_DIR: &str = "configs/nodes/@mecha10";
111
112    /// Simulation configs directory
113    pub const SIMULATION_DIR: &str = "configs/simulation";
114
115    /// Simulation config file
116    pub const SIMULATION_CONFIG: &str = "configs/simulation/config.json";
117
118    // =========================================================================================
119    // Generic `config.json` resolution
120    // =========================================================================================
121    //
122    // Node configs (`configs/nodes/...`) and behavior configs (`configs/behaviors/...`, moved
123    // under `configs/` in LAB-2072 so they're actually reachable by `mecha10 config
124    // push`/`pull`/`list` - see `mecha10_robot_config::sync`) both resolve across exactly two
125    // scopes under their category root: `@mecha10/<name>/config.json` (bundled framework
126    // default) and `<name>/config.json` (plain, project-committed override). There is no
127    // `@local` tier for either category (LAB-1994 removed it for nodes, where it was legacy and
128    // had no coherent, implemented meaning; LAB-1990 removed an earlier, differently-motivated
129    // attempt at one for behaviors, for the same reason - nothing gitignores it and, as of
130    // LAB-2072, `mecha10 config push` genuinely does sync it like any other config).
131    // `framework_config`/`project_config`/`config_candidates` below are shared by both
132    // categories.
133
134    /// Framework-tier (`@mecha10`) `config.json` path for `name` under `category_dir`.
135    fn framework_config(category_dir: &str, name: &str) -> String {
136        format!("{category_dir}/@mecha10/{name}/config.json")
137    }
138
139    /// Project-tier (plain name) `config.json` path for `name` under `category_dir`.
140    fn project_config(category_dir: &str, name: &str) -> String {
141        format!("{category_dir}/{name}/config.json")
142    }
143
144    /// The 2-tier `config.json` lookup candidates for `name` under `category_dir`, in canonical
145    /// resolution order: framework (`@mecha10`) -> project (plain name).
146    fn config_candidates(category_dir: &str, name: &str) -> [String; 2] {
147        [framework_config(category_dir, name), project_config(category_dir, name)]
148    }
149
150    /// Get config path for a framework node
151    pub fn framework_node(node_name: &str) -> String {
152        framework_config(NODES_DIR, node_name)
153    }
154
155    /// Get config path for a project node (plain name)
156    pub fn project_node(node_name: &str) -> String {
157        project_config(NODES_DIR, node_name)
158    }
159
160    /// Get the framework node's config *directory* (no `config.json` suffix) - the `@mecha10`
161    /// counterpart to [`framework_node`], for callers that need the directory itself (e.g. to
162    /// copy a whole config template tree into it) rather than the `config.json` file path.
163    pub fn framework_node_dir(node_name: &str) -> String {
164        format!("{}/{}", NODES_MECHA10_DIR, node_name)
165    }
166
167    /// The 2-tier `config.json` lookup candidates for a node, in canonical resolution order:
168    /// framework (`@mecha10`) -> project (plain name). This is the ONE place that knows the tier
169    /// order/naming convention for "does this node have a config.json, and if so which scope is
170    /// it in" - callers that need to check both tiers (e.g. "use the first one that exists on
171    /// disk") should use this rather than hand-rolling the same `format!()`/literal strings
172    /// themselves, which is exactly what caused LAB-1982.
173    pub fn node_config_candidates(node_name: &str) -> [String; 2] {
174        config_candidates(NODES_DIR, node_name)
175    }
176
177    /// Get config path for a framework-default behavior (bundled into the project at scaffold
178    /// time under `configs/behaviors/@mecha10/<name>/config.json`).
179    pub fn framework_behavior(behavior_name: &str) -> String {
180        framework_config(project::BEHAVIORS_DIR, behavior_name)
181    }
182
183    /// Get config path for a project behavior (plain name:
184    /// `configs/behaviors/<name>/config.json`).
185    pub fn project_behavior(behavior_name: &str) -> String {
186        project_config(project::BEHAVIORS_DIR, behavior_name)
187    }
188
189    /// The 2-tier `config.json` lookup candidates for a behavior, in override-precedence order
190    /// (highest first): project (plain name) -> framework (`@mecha10`). A project-committed
191    /// override beats the bundled framework default. There is no `@local` tier for behaviors
192    /// (LAB-1990). See [`node_config_candidates`] for the sibling helper for nodes.
193    pub fn behavior_config_candidates(behavior_name: &str) -> [String; 2] {
194        [project_behavior(behavior_name), framework_behavior(behavior_name)]
195    }
196}
197
198/// Docker-related paths within project
199pub mod docker {
200    /// Docker directory
201    pub const DIR: &str = "docker";
202
203    /// Main docker-compose file
204    pub const COMPOSE_FILE: &str = "docker/docker-compose.yml";
205
206    /// Remote nodes docker-compose file
207    pub const COMPOSE_REMOTE_FILE: &str = "docker/docker-compose.remote.yml";
208
209    /// Remote nodes Dockerfile
210    pub const DOCKERFILE_REMOTE: &str = "docker/Dockerfile.remote";
211}
212
213/// Environment files
214pub mod env {
215    /// Environment example file
216    pub const EXAMPLE: &str = ".env.example";
217
218    /// Environment file
219    pub const FILE: &str = ".env";
220}
221
222/// Meta files (README, gitignore, etc.)
223pub mod meta {
224    /// README file
225    pub const README: &str = "README.md";
226
227    /// Git ignore file
228    pub const GITIGNORE: &str = ".gitignore";
229
230    /// Package.json for Node.js tooling
231    pub const PACKAGE_JSON: &str = "package.json";
232
233    /// Python requirements
234    pub const REQUIREMENTS_TXT: &str = "requirements.txt";
235
236    /// Lint config
237    pub const LS_LINT_CONFIG: &str = ".ls-lint.yml";
238}
239
240/// Rust project files
241pub mod rust {
242    /// Cargo workspace manifest
243    pub const CARGO_TOML: &str = "Cargo.toml";
244
245    /// Cargo lock file
246    pub const CARGO_LOCK: &str = "Cargo.lock";
247
248    /// Main entry point
249    pub const MAIN_RS: &str = "src/main.rs";
250
251    /// Library entry point
252    pub const LIB_RS: &str = "src/lib.rs";
253
254    /// Build script
255    pub const BUILD_RS: &str = "build.rs";
256
257    /// Rustfmt config
258    pub const RUSTFMT_TOML: &str = "rustfmt.toml";
259}
260
261/// Model files within a model directory
262pub mod model {
263    /// ONNX model file
264    pub const ONNX_FILE: &str = "model.onnx";
265
266    /// Labels file
267    pub const LABELS_FILE: &str = "labels.txt";
268
269    /// Model config file
270    pub const CONFIG_FILE: &str = "config.json";
271}
272
273// =============================================================================
274// User Home Paths - Paths in ~/.mecha10
275// =============================================================================
276
277/// User-level paths in home directory
278pub mod user {
279    use std::path::PathBuf;
280
281    /// Base mecha10 directory in user home.
282    ///
283    /// Re-exports `mecha10_core::fs_utils::MECHA10_DIR`, the single source of truth for this
284    /// literal shared with `launcher-service` (which can't depend on this crate - see that
285    /// constant's doc comment) - kept as a `mecha10-cli-core`-local constant (rather than
286    /// requiring callers to reach into `mecha10-core` directly) so existing callers of
287    /// `paths::user::MECHA10_DIR` don't need to change (LAB-1999).
288    pub const MECHA10_DIR: &str = mecha10_core::fs_utils::MECHA10_DIR;
289
290    /// Credentials file, relative to the user home directory.
291    ///
292    /// Documentation-only - `mecha10-auth`'s `credentials::default_credentials_path()` is the
293    /// actual source of truth for this path (see `credentials_file()` below), since
294    /// `mecha10-auth` can't depend back on this crate (`mecha10-cli-core` already depends on
295    /// `mecha10-auth`, so the reverse edge would be a dependency cycle).
296    pub const CREDENTIALS_FILE: &str = ".mecha10/credentials.json";
297
298    /// Templates cache directory
299    pub const TEMPLATES_DIR: &str = ".mecha10/templates";
300
301    /// Binary installation directory
302    pub const BIN_DIR: &str = ".mecha10/bin";
303
304    /// Get the base mecha10 directory path
305    pub fn mecha10_dir() -> PathBuf {
306        dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")).join(MECHA10_DIR)
307    }
308
309    /// Get the credentials file path.
310    ///
311    /// Delegates to `mecha10-auth`'s `credentials::default_credentials_path()`, which is the
312    /// single implementation of this resolution logic (see the `CREDENTIALS_FILE` doc comment
313    /// for why the dependency runs in this direction).
314    pub fn credentials_file() -> PathBuf {
315        mecha10_auth::default_credentials_path()
316    }
317
318    /// Get the templates cache directory
319    pub fn templates_dir() -> PathBuf {
320        mecha10_dir().join("templates")
321    }
322
323    /// Get the binary directory
324    pub fn bin_dir() -> PathBuf {
325        mecha10_dir().join("bin")
326    }
327
328    /// Get path to a binary in the mecha10 bin directory
329    pub fn bin(name: &str) -> PathBuf {
330        bin_dir().join(name)
331    }
332
333    /// Get path to a binary in cargo bin directory
334    pub fn cargo_bin(name: &str) -> PathBuf {
335        mecha10_core::fs_utils::home_subpath(&[".cargo", "bin", name])
336    }
337
338    /// Get path to a binary in local bin directory
339    pub fn local_bin(name: &str) -> PathBuf {
340        mecha10_core::fs_utils::home_subpath(&[".local", "bin", name])
341    }
342}
343
344// =============================================================================
345// Docker Container Paths - Paths used inside containers
346// =============================================================================
347
348/// Paths used inside Docker containers
349pub mod container {
350    /// Project mount point in container
351    pub const PROJECT_ROOT: &str = "/project";
352
353    /// Framework mount point in container
354    pub const FRAMEWORK_ROOT: &str = "/mecha10";
355
356    /// App directory (for mecha10-remote)
357    pub const APP_ROOT: &str = "/app";
358
359    /// Config file in mecha10-remote container
360    pub const REMOTE_CONFIG: &str = "/app/mecha10.json";
361
362    /// Get project-relative path inside container
363    pub fn project_path(relative: &str) -> String {
364        format!("{}/{}", PROJECT_ROOT, relative)
365    }
366
367    /// Get framework-relative path inside container
368    pub fn framework_path(relative: &str) -> String {
369        format!("{}/{}", FRAMEWORK_ROOT, relative)
370    }
371}
372
373// =============================================================================
374// Framework Paths - Paths within mecha10 framework source
375// =============================================================================
376
377/// Paths within the mecha10 framework source
378pub mod framework {
379    /// Packages directory
380    pub const PACKAGES_DIR: &str = "packages";
381
382    /// Nodes package directory
383    pub const NODES_DIR: &str = "packages/nodes";
384
385    /// Drivers package directory
386    pub const DRIVERS_DIR: &str = "packages/drivers";
387
388    /// Simulation package directory
389    pub const SIMULATION_DIR: &str = "packages/simulation";
390
391    /// Simulation models directory
392    pub const SIMULATION_MODELS_DIR: &str = "packages/simulation/models";
393
394    /// Simulation environments directory
395    pub const SIMULATION_ENVIRONMENTS_DIR: &str = "packages/simulation/environments";
396
397    /// Robot tasks environments directory (for environment matching)
398    pub const ROBOT_TASKS_DIR: &str = "packages/simulation/environments/robot-tasks";
399
400    /// Robot tasks catalog file
401    pub const ROBOT_TASKS_CATALOG: &str = "packages/simulation/environments/robot-tasks/catalog.json";
402
403    /// Python taskrunner package
404    pub const TASKRUNNER_DIR: &str = "packages/taskrunner";
405
406    // Note: `packages/rl-trainer` (hosted the MuJoCo simulation backend, `mecha10_rl.mujoco`)
407    // was extracted out of this repo into the standalone `mecha-industries/mecha10-rl` repo
408    // (LAB-1830, see `docs/architecture/SIMULATION_ARCHITECTURE.md`). There is no in-repo path
409    // to it anymore - `mecha10_rl` is now an externally pip-installed package, resolved via the
410    // interpreter `mecha10 sim`/`packages/nodes/simulator` invoke (see
411    // `packages/cli/src/sim/mod.rs::run_mujoco_cli` and
412    // `packages/nodes/simulator/src/python_env.rs::check_mecha10_rl_importable`), so no
413    // `RL_TRAINER_DIR`-style project-relative constant belongs here anymore.
414
415    /// Marker directory used to detect the mecha10 framework root
416    pub const ROOT_MARKER_DIR: &str = "packages/core";
417
418    /// Release binary
419    pub const RELEASE_BINARY: &str = "target/release/mecha10";
420
421    /// Debug binary
422    pub const DEBUG_BINARY: &str = "target/debug/mecha10";
423
424    /// Get node config path within framework
425    pub fn node_config(node_name: &str) -> String {
426        format!("packages/nodes/{}/configs/config.json", node_name)
427    }
428}
429
430// =============================================================================
431// External URLs
432// =============================================================================
433
434/// External service URLs
435pub mod urls {
436    /// Default base URL for the public `mecha10-api` service.
437    ///
438    /// The CLI never talks to the private homelab Minio instance directly - all
439    /// framework/runtime artifact downloads (CLI/launcher/node binaries, simulation
440    /// assets, templates, remote-image manifests, ML models) are proxied through
441    /// `mecha10-api`'s `/api/downloads/*` routes (see `packages/mecha10-api/src/catalog/downloads`),
442    /// which run *inside* the private network and stream bytes back out.
443    pub const MECHA10_API_URL: &str = "https://mecha.industries/api";
444
445    /// Build the `mecha10-api` base URL (e.g. `https://mecha.industries/api`).
446    ///
447    /// Override at runtime with the `MECHA10_API_URL` env var (e.g. to point at a
448    /// local `mecha10-api` instance during development).
449    pub fn mecha10_api_base_url() -> String {
450        std::env::var("MECHA10_API_URL")
451            .unwrap_or_else(|_| MECHA10_API_URL.to_string())
452            .trim_end_matches('/')
453            .to_string()
454    }
455
456    /// Default authentication URL
457    pub const AUTH_URL: &str = "https://mecha.industries/api/auth";
458}
459
460// =============================================================================
461// Helper Functions
462// =============================================================================
463
464/// Build a target output path
465pub fn target_path(profile: &str, binary: &str) -> String {
466    format!("target/{}/{}", profile, binary)
467}
468
469/// Build a target output path with optional target triple
470pub fn target_path_with_triple(target: Option<&str>, profile: &str) -> String {
471    match target {
472        Some(t) => format!("target/{}/{}", t, profile),
473        None => format!("target/{}", profile),
474    }
475}
476
477#[cfg(test)]
478mod tests {
479    use super::*;
480
481    #[test]
482    fn credentials_file_matches_mecha10_auth_source_of_truth() {
483        // `user::credentials_file()` must stay byte-identical to
484        // `mecha10_auth::default_credentials_path()`, which is the actual implementation it
485        // delegates to (see the doc comment on `user::CREDENTIALS_FILE` for why this crate
486        // can't own the literal itself - it already depends on `mecha10-auth`) (LAB-1981).
487        assert_eq!(user::credentials_file(), mecha10_auth::default_credentials_path());
488    }
489
490    #[test]
491    fn simulation_project_dirs_share_the_simulation_root() {
492        // The three simulation asset catalog subdirectories scaffolded at `mecha10 init` time
493        // (LAB-2019) must all live under the same `simulation/` root `project::SIMULATION_DIR`
494        // names - `mecha10_rl.mujoco.asset_resolver.AssetResolver`'s project-root fallback tier
495        // expects exactly this layout.
496        assert_eq!(project::SIMULATION_MODELS_DIR, "simulation/models");
497        assert_eq!(project::SIMULATION_ENVIRONMENTS_DIR, "simulation/environments");
498        assert_eq!(project::SIMULATION_TASKS_DIR, "simulation/tasks");
499        for dir in [
500            project::SIMULATION_MODELS_DIR,
501            project::SIMULATION_ENVIRONMENTS_DIR,
502            project::SIMULATION_TASKS_DIR,
503        ] {
504            assert!(dir.starts_with(&format!("{}/", project::SIMULATION_DIR)));
505        }
506    }
507
508    #[test]
509    fn test_config_paths() {
510        assert_eq!(
511            config::framework_node("object-detector"),
512            "configs/nodes/@mecha10/object-detector/config.json"
513        );
514        assert_eq!(config::project_node("camera"), "configs/nodes/camera/config.json");
515    }
516
517    #[test]
518    fn test_framework_node_dir() {
519        assert_eq!(config::framework_node_dir("speaker"), "configs/nodes/@mecha10/speaker");
520    }
521
522    #[test]
523    fn test_node_config_candidates_order() {
524        // Canonical 2-tier order: framework (@mecha10) -> project (plain). No `@local` tier for
525        // nodes (LAB-1994). Every hand-rolled call site consolidated onto this helper (preflight/
526        // teleop/models/topology::nodes) relied on exactly this order - see LAB-1982.
527        assert_eq!(
528            config::node_config_candidates("teleop"),
529            [
530                "configs/nodes/@mecha10/teleop/config.json".to_string(),
531                "configs/nodes/teleop/config.json".to_string(),
532            ]
533        );
534    }
535
536    #[test]
537    fn test_behavior_config_paths() {
538        assert_eq!(
539            config::framework_behavior("idle_wander"),
540            "configs/behaviors/@mecha10/idle_wander/config.json"
541        );
542        assert_eq!(
543            config::project_behavior("idle_wander"),
544            "configs/behaviors/idle_wander/config.json"
545        );
546    }
547
548    #[test]
549    fn test_behavior_config_candidates_order() {
550        // Highest precedence first: project override -> framework default. No `@local` tier for
551        // behaviors (LAB-1990). Nested under `configs/` since LAB-2072.
552        assert_eq!(
553            config::behavior_config_candidates("idle_wander"),
554            [
555                "configs/behaviors/idle_wander/config.json".to_string(),
556                "configs/behaviors/@mecha10/idle_wander/config.json".to_string(),
557            ]
558        );
559    }
560
561    #[test]
562    fn test_target_path() {
563        assert_eq!(target_path("release", "my-node"), "target/release/my-node");
564        assert_eq!(
565            target_path_with_triple(Some("aarch64-unknown-linux-gnu"), "release"),
566            "target/aarch64-unknown-linux-gnu/release"
567        );
568        assert_eq!(target_path_with_triple(None, "debug"), "target/debug");
569    }
570
571    #[test]
572    fn test_container_paths() {
573        assert_eq!(
574            container::project_path("configs/test.json"),
575            "/project/configs/test.json"
576        );
577        assert_eq!(container::framework_path("packages/core"), "/mecha10/packages/core");
578    }
579
580    #[test]
581    fn test_mecha10_api_base_url_default() {
582        // Guard against interference from other tests running in the same process.
583        let _guard = ENV_MUTEX.lock().unwrap();
584        std::env::remove_var("MECHA10_API_URL");
585        assert_eq!(urls::mecha10_api_base_url(), "https://mecha.industries/api");
586    }
587
588    #[test]
589    fn test_mecha10_api_base_url_env_override() {
590        let _guard = ENV_MUTEX.lock().unwrap();
591        std::env::set_var("MECHA10_API_URL", "http://localhost:4000/api/");
592        assert_eq!(urls::mecha10_api_base_url(), "http://localhost:4000/api");
593        std::env::remove_var("MECHA10_API_URL");
594    }
595
596    /// Serializes tests that mutate process-wide env vars, since `cargo test` runs
597    /// tests in this module concurrently by default.
598    static ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
599}