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}