proto_cli 0.58.2

A multi-language version manager, a unified toolchain.
mod app;
mod commands;
mod components;
mod error;
mod helpers;
mod mcp;
mod session;
mod shell;
mod systems;
mod telemetry;
mod utils;
mod workflows;

use app::{App as CLI, Commands, DebugCommands, PluginCommands};
use clap::Parser;
use proto_core::reporter::ReporterFormat;
use session::ProtoSession;
use starbase::{
    App, MainResult,
    tracing::{LogLevel, OtelOptions, TracingOptions},
};
use starbase_utils::{envx, string_vec};
use std::env;
use std::process::ExitCode;
use tracing::debug;

fn get_tracing_modules() -> Vec<String> {
    let mut modules = string_vec!["proto", "schematic", "starbase", "warpgate"];

    if envx::bool_var("PROTO_DEBUG_WASM") || envx::bool_var("PROTO_WASM_LOG") {
        modules.push("extism".into());
    } else {
        modules.push("extism::pdk".into());
    }

    modules
}

#[tokio::main]
async fn main() -> MainResult {
    sigpipe::reset();

    let mut cli = CLI::parse();
    cli.setup_env_vars();

    let app = App::default();
    app.setup_diagnostics();

    let is_exec_command = matches!(
        cli.command,
        Commands::Exec { .. } | Commands::Run { .. } | Commands::Shell { .. }
    );

    let _guard = app.setup_tracing(TracingOptions {
        default_level: if is_exec_command || matches!(cli.command, Commands::Bin { .. }) {
            LogLevel::Warn
        } else if matches!(cli.command, Commands::Completions { .. }) {
            LogLevel::Off
        } else {
            LogLevel::Info
        },
        dump_trace: cli.dump && !is_exec_command,
        filter_modules: get_tracing_modules(),
        log_env: "PROTO_APP_LOG".into(),
        log_file: cli.log_file.clone(),
        ndjson: cli.reporter == ReporterFormat::Ndjson,
        otel: OtelOptions {
            enabled: cli.otel,
            logs_enabled: cli.otel_logs,
            service_name: cli.otel_service_name.clone(),
        },
        show_spans: cli.log.is_verbose(),
        // test_env: "PROTO_TEST".into(),
        ..TracingOptions::default()
    })?;

    let session = ProtoSession::new(cli);
    let mut args = env::args_os().collect::<Vec<_>>();

    debug!(
        exe = ?args.remove(0),
        args = ?args,
        shim = env::var("PROTO_SHIM_NAME").ok(),
        shim_exe = env::var("PROTO_SHIM_PATH").ok(),
        pid = std::process::id(),
        "Running proto v{}",
        session.cli_version
    );

    let mut outcome = app
        .run(session.clone(), |session: ProtoSession| async {
            match session.cli.command.clone() {
                Commands::Activate(args) => commands::activate(session, args).await,
                Commands::Alias(args) => commands::alias(session, args).await,
                Commands::Bin(args) => commands::bin(session, args).await,
                Commands::Clean(args) => commands::clean(session, args).await,
                Commands::Completions(args) => commands::completions(session, args).await,
                Commands::Debug { command } => match command {
                    DebugCommands::Config(args) => commands::debug::config(session, args).await,
                    DebugCommands::Env(args) => commands::debug::env(session, args).await,
                },
                Commands::Diagnose(args) => commands::diagnose(session, args).await,
                Commands::Exec(args) => commands::exec(session, args).await,
                Commands::Install(args) => commands::install(session, args).await,
                Commands::Mcp(args) => commands::mcp(session, args).await,
                Commands::Migrate(args) => commands::migrate(session, args).await,
                Commands::Outdated(args) => commands::outdated(session, args).await,
                Commands::Pin(args) => commands::pin(session, args).await,
                Commands::Plugin { command } => match command {
                    PluginCommands::Add(args) => commands::plugin::add(session, args).await,
                    PluginCommands::Info(args) => commands::plugin::info(session, args).await,
                    PluginCommands::List(args) => commands::plugin::list(session, args).await,
                    PluginCommands::Remove(args) => commands::plugin::remove(session, args).await,
                    PluginCommands::Search(args) => commands::plugin::search(session, args).await,
                },
                Commands::Regen(args) => commands::regen(session, args).await,
                Commands::Run(args) => commands::run(session, args).await,
                Commands::Setup(args) => commands::setup(session, args).await,
                Commands::Shell(args) => commands::shell(session, args).await,
                Commands::Status(args) => commands::status(session, args).await,
                Commands::Unalias(args) => commands::unalias(session, args).await,
                Commands::Uninstall(args) => commands::uninstall(session, args).await,
                Commands::Unpin(args) => commands::unpin(session, args).await,
                Commands::Upgrade(args) => commands::upgrade(session, args).await,
                Commands::Versions(args) => commands::versions(session, args).await,
            }
        })
        .await;

    if let Some(error) = outcome.error {
        // If NDJSON format, we must print the error as JSON so
        // that it parses correctly by the consumer!
        if session.cli.reporter == ReporterFormat::Ndjson {
            session.console.main_error(error.to_string())?;
            session.console.out.flush()?;

            if outcome.exit_code == 0 {
                outcome.exit_code = 1;
            }
        }
        // Otherwise bubble up the error so that miette renders
        // it nicely for the user using the fancy output!
        else {
            return Err(error);
        }
    }

    Ok(ExitCode::from(outcome.exit_code))
}