mecha10-cli 4.2.7

Mecha10 CLI tool
Documentation
//! Mecha10 CLI
//!
//! Command-line interface for managing Mecha10 robot projects.
//!
//! # Commands
//!
//! - `mecha10 init` - Initialize a new robot project
//! - `mecha10 dev` - Start development mode with hot-reload
//! - `mecha10 run` - Run the robot in production mode
//! - `mecha10 build` - Build the project
//! - `mecha10 train` - Submit a training job
//! - `mecha10 logs` - View logs from nodes, framework, and services (watch mode by default)
//! - `mecha10 sim` - Generate robot/environment simulation scenes
//! - `mecha10 target` - Show or update which platform(s) a target builds for

#![allow(
    clippy::print_stdout,
    clippy::print_stderr,
    reason = "this crate IS the CLI's terminal UI -- println!/eprintln! to the user's terminal is its intended output mechanism, not debug leftovers"
)]

mod commands;
mod component_catalog;
mod config_validation;
mod context;
mod dev;
mod handlers;
mod infrastructure;
mod init;
mod paths;
mod run;
mod services;
mod sim;
mod templates;
mod types;
mod ui;

use anyhow::Result;
use clap::Parser;
use context::CliContextBuilder;
use types::{Cli, Commands};

#[tokio::main]
async fn main() -> Result<()> {
    let _ = dotenvy::dotenv();

    let sentry_dsn = std::env::var("SENTRY_DSN_CLI").unwrap_or_else(|_| {
        "https://23908dad56709a668ae2f899e2c19924@o4506107376631808.ingest.us.sentry.io/4512334108098560".to_string()
    });
    let mut sentry_opts = sentry::ClientOptions::default();
    sentry_opts.dsn = sentry_dsn.parse().ok();
    sentry_opts.release = sentry::release_name!();
    let _sentry_guard = sentry::init(sentry_opts);

    let cli = Cli::parse();

    let log_level = match cli.log_level.as_str() {
        "debug" => tracing::Level::DEBUG,
        "info" => tracing::Level::INFO,
        "warn" => tracing::Level::WARN,
        "error" => tracing::Level::ERROR,
        _ => tracing::Level::INFO,
    };

    let _file_log_guard = if matches!(cli.command, Commands::Dev(_)) {
        let working_dir = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
        let logs_dir = working_dir.join(paths::project::LOGS_DIR);
        let _ = std::fs::create_dir_all(&logs_dir);
        let file_appender = tracing_appender::rolling::never(&logs_dir, "mecha10-dev.log");
        let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);

        use tracing_subscriber::prelude::*;
        tracing_subscriber::registry()
            .with(
                tracing_subscriber::fmt::layer()
                    .with_target(false)
                    .with_filter(tracing_subscriber::filter::LevelFilter::from_level(log_level)),
            )
            .with(
                tracing_subscriber::fmt::layer()
                    .with_target(false)
                    .with_ansi(false)
                    .with_writer(non_blocking)
                    .with_filter(tracing_subscriber::filter::LevelFilter::from_level(log_level)),
            )
            // `sentry_tracing::layer()` (the legacy combined layer) is deprecated in favor of
            // individual layers -- see `sentry_tracing::SentryLayer`'s migration docs. This crate
            // doesn't enable sentry-tracing's `logs` feature, so `LogLayer` isn't available; these
            // three layers reproduce the legacy layer's own default (non-`logs`) behavior:
            // `ERROR`/`WARN`/`INFO` spans become Sentry spans/transactions, `WARN`/`INFO` events
            // become breadcrumbs attached to subsequent errors, and `ERROR` events become Sentry
            // error events.
            .with(sentry_tracing::SpanLayer::new().with_filter(tracing_subscriber::filter::LevelFilter::INFO))
            .with(
                sentry_tracing::BreadcrumbLayer::new().with_filter(tracing_subscriber::filter::filter_fn(|metadata| {
                    matches!(*metadata.level(), tracing::Level::WARN | tracing::Level::INFO)
                })),
            )
            .with(sentry_tracing::ErrorLayer::new().with_filter(tracing_subscriber::filter::LevelFilter::ERROR))
            .init();

        Some(guard)
    } else {
        tracing_subscriber::fmt()
            .with_max_level(log_level)
            .with_target(false)
            .init();

        None
    };

    let mut ctx = CliContextBuilder::new()
        .config_path(cli.config.clone())
        .log_level(log_level)
        .build()?;

    match &cli.command {
        Commands::Add { command } => {
            handlers::add::handle_add(&mut ctx, command).await?;
        }

        Commands::Auth { command } => {
            use commands::auth::AuthCommand;
            match command {
                AuthCommand::Login(args) => {
                    handlers::auth::handle_login(args).await?;
                }
                AuthCommand::Logout => {
                    handlers::auth::handle_logout().await?;
                }
                AuthCommand::Whoami(args) => {
                    handlers::auth::handle_whoami(args).await?;
                }
            }
        }

        Commands::Build { command } => {
            handlers::build::handle_build(&mut ctx, command).await?;
        }

        Commands::Cache { command } => {
            use commands::cache::CacheCommand;
            match command {
                CacheCommand::Clean => {
                    handlers::cache::handle_cache_clean().await?;
                }
            }
        }

        Commands::Config { command } => {
            use commands::config::ConfigCommand;
            match command {
                ConfigCommand::Push(args) => {
                    handlers::config::handle_config_push(&mut ctx, args).await?;
                }
                ConfigCommand::Pull(args) => {
                    handlers::config::handle_config_pull(&mut ctx, args).await?;
                }
                ConfigCommand::List(args) => {
                    handlers::config::handle_config_list(&mut ctx, args).await?;
                }
                ConfigCommand::Show => {
                    handlers::config::handle_config_show(&mut ctx).await?;
                }
                ConfigCommand::Validate(args) => {
                    handlers::config::handle_config_validate(&mut ctx, args).await?;
                }
                ConfigCommand::Edit => {
                    handlers::config::handle_config_edit(&mut ctx).await?;
                }
                ConfigCommand::Sync => {
                    handlers::config::handle_config_sync(&mut ctx).await?;
                }
            }
        }

        Commands::Dev(dev_args) => {
            handlers::dev::handle_dev(&mut ctx, dev_args).await?;
        }

        Commands::Diagnostics(args) => {
            handlers::diagnostics::handle_diagnostics(&mut ctx, args).await?;
        }

        Commands::Format(format_args) => {
            use handlers::format::FormatArgs;
            let args = FormatArgs {
                check: format_args.check,
            };
            handlers::format::handle_format(&mut ctx, &args).await?;
        }

        Commands::Init(init_args) => {
            handlers::init::handle_init(&mut ctx, init_args).await?;
        }

        Commands::Lint(lint_args) => {
            use handlers::lint::LintArgs;
            let args = LintArgs { fix: lint_args.fix };
            handlers::lint::handle_lint(&mut ctx, &args).await?;
        }

        Commands::Logs { command } => {
            handlers::logs::handle_logs_command(&mut ctx, command).await?;
        }

        Commands::Model { command } => {
            handlers::model::handle_model(&mut ctx, command).await?;
        }

        Commands::Ps { command } => {
            handlers::ps::handle_ps(&mut ctx, command).await?;
        }

        Commands::Robot { command } => {
            handlers::robot::handle_robot(&mut ctx, command).await?;
        }

        Commands::Run { command } => {
            handlers::run::handle_run(&mut ctx, command).await?;
        }

        Commands::Setup(setup_args) => {
            use handlers::setup::SetupArgs;
            let args = SetupArgs {
                check_only: setup_args.check_only,
                install_deps: !setup_args.non_interactive,
                quiet: false,
            };
            handlers::setup::handle_setup(&mut ctx, &args).await?;
        }

        Commands::Sim { command } => {
            handlers::sim::handle_sim(&mut ctx, command).await?;
        }

        Commands::Stop { command } => {
            handlers::stop::handle_stop(&mut ctx, command).await?;
        }

        Commands::Target { command } => {
            handlers::target::handle_target(&mut ctx, command.as_ref()).await?;
        }

        Commands::Topics { command } => {
            use commands::TopicsCommand;
            use handlers::topics::TopicsListArgs;

            match command {
                TopicsCommand::List {
                    pattern,
                    verbose,
                    grouped,
                } => {
                    let args = TopicsListArgs {
                        pattern: pattern.clone(),
                        verbose: *verbose,
                        grouped: *grouped,
                    };
                    handlers::topics::handle_topics_list(&mut ctx, &args).await?;
                }
            }
        }

        Commands::Topology(topology_args) => {
            handlers::topology::handle_topology(&mut ctx, topology_args).await?;
        }

        Commands::Train(train_args) => {
            handlers::train::handle_train(&mut ctx, train_args).await?;
        }

        Commands::Upload(upload_args) => {
            handlers::upload::handle_upload(&mut ctx, upload_args).await?;
        }

        Commands::Validate(args) => {
            handlers::config::handle_config_validate(&mut ctx, args).await?;
        }
    }

    Ok(())
}

#[cfg(test)]
mod sentry_tracing_layer_tests {
    // Regression test for the `sentry_tracing::layer()` deprecation (migrated to the individual
    // `SpanLayer`/`BreadcrumbLayer`/`ErrorLayer` layers in `main`'s dev-mode tracing setup, since
    // this crate doesn't enable sentry-tracing's `logs` feature for `LogLayer`). Unlike the
    // legacy combined layer, these individual layers are documented as safe to compose together
    // (only mixing them *with* the legacy layer is unsupported/can panic), so this exercises that
    // composition end-to-end -- building the registry, installing it as the default subscriber
    // for the test's scope, and emitting a span/events through it -- to guard against a future
    // edit reintroducing the deprecated call or an incompatible layer combination.
    use tracing_subscriber::prelude::*;

    #[test]
    fn sentry_layers_compose_and_handle_events_without_panicking() {
        let subscriber = tracing_subscriber::registry()
            .with(sentry_tracing::SpanLayer::new().with_filter(tracing_subscriber::filter::LevelFilter::INFO))
            .with(
                sentry_tracing::BreadcrumbLayer::new().with_filter(tracing_subscriber::filter::filter_fn(|metadata| {
                    matches!(*metadata.level(), tracing::Level::WARN | tracing::Level::INFO)
                })),
            )
            .with(sentry_tracing::ErrorLayer::new().with_filter(tracing_subscriber::filter::LevelFilter::ERROR));

        tracing::subscriber::with_default(subscriber, || {
            let _span = tracing::info_span!("test span").entered();
            tracing::warn!("test warning breadcrumb");
            tracing::error!("test error event");
        });
    }
}