Skip to main content
Glama

microsandbox

by microsandbox
handlers.rs26.6 kB
use clap::{error::ErrorKind, CommandFactory}; use microsandbox_cli::{ AnsiStyles, MicrosandboxArgs, MicrosandboxCliError, MicrosandboxCliResult, SelfAction, }; use microsandbox_core::{ config::START_SCRIPT_NAME, management::{ config::{self, Component, ComponentType}, home, menv, orchestra, sandbox, toolchain, }, oci::Reference, }; use microsandbox_server::MicrosandboxServerResult; use microsandbox_utils::{env, NAMESPACES_SUBDIR}; use std::{collections::HashMap, path::PathBuf}; use typed_path::Utf8UnixPathBuf; //-------------------------------------------------------------------------------------------------- // Constants //-------------------------------------------------------------------------------------------------- const SANDBOX_SCRIPT_SEPARATOR: char = '~'; //-------------------------------------------------------------------------------------------------- // Functions: Handlers //-------------------------------------------------------------------------------------------------- /// Set the log level based on the command line arguments pub fn log_level(args: &MicrosandboxArgs) { let level = if args.trace { Some("trace") } else if args.debug { Some("debug") } else if args.info { Some("info") } else if args.warn { Some("warn") } else if args.error { Some("error") } else { None }; // Set RUST_LOG environment variable only if a level is specified if let Some(level) = level { std::env::set_var("RUST_LOG", format!("microsandbox={},msb={}", level, level)); } } pub async fn add_subcommand( sandbox: bool, build: bool, names: Vec<String>, image: String, memory: Option<u32>, cpus: Option<u32>, volumes: Vec<String>, ports: Vec<String>, envs: Vec<String>, env_file: Option<Utf8UnixPathBuf>, depends_on: Vec<String>, workdir: Option<Utf8UnixPathBuf>, shell: Option<String>, scripts: Vec<(String, String)>, start: Option<String>, imports: Vec<(String, String)>, exports: Vec<(String, String)>, scope: Option<String>, path: Option<PathBuf>, config: Option<String>, ) -> MicrosandboxCliResult<()> { validate_build_sandbox_conflict(build, sandbox, "add", Some("[NAMES]"), None); unsupported_build_error(build, "add", Some("[NAMES]")); let mut scripts = scripts .into_iter() .map(|(k, v)| (k, v.into())) .collect::<HashMap<String, String>>(); if let Some(start) = start { scripts.insert(START_SCRIPT_NAME.to_string(), start.into()); } let component = Component::Sandbox { image, memory, cpus, volumes, ports, envs, env_file, depends_on, workdir, shell, scripts, imports: imports.into_iter().map(|(k, v)| (k, v.into())).collect(), exports: exports.into_iter().map(|(k, v)| (k, v.into())).collect(), scope, }; config::add(&names, &component, path.as_deref(), config.as_deref()).await?; Ok(()) } pub async fn remove_subcommand( sandbox: bool, build: bool, names: Vec<String>, file: Option<PathBuf>, ) -> MicrosandboxCliResult<()> { validate_build_sandbox_conflict(build, sandbox, "remove", Some("[NAMES]"), None); unsupported_build_error(build, "remove", Some("[NAMES]")); let (path, config) = parse_file_path(file); config::remove( ComponentType::Sandbox, &names, path.as_deref(), config.as_deref(), ) .await?; Ok(()) } pub async fn list_subcommand( sandbox: bool, build: bool, file: Option<PathBuf>, ) -> MicrosandboxCliResult<()> { validate_build_sandbox_conflict(build, sandbox, "list", None, None); unsupported_build_error(build, "list", None); let (path, config) = parse_file_path(file); let (config, _, _) = config::load_config(path.as_deref(), config.as_deref()).await?; // Use the new show_list function to display sandboxes menv::show_list(config.get_sandboxes()); Ok(()) } pub async fn init_subcommand(path: Option<PathBuf>) -> MicrosandboxCliResult<()> { menv::initialize(path).await?; Ok(()) } pub async fn run_subcommand( sandbox: bool, build: bool, name: String, file: Option<PathBuf>, detach: bool, exec: Option<String>, args: Vec<String>, ) -> MicrosandboxCliResult<()> { validate_build_sandbox_conflict(build, sandbox, "run", Some("[NAME]"), Some("<ARGS>")); unsupported_build_error(build, "run", Some("[NAME]")); let (sandbox, script) = parse_name_and_script(&name); if matches!((script, &exec), (Some(_), Some(_))) { MicrosandboxArgs::command() .override_usage(usage("run", Some("[NAME[~SCRIPT]]"), Some("<ARGS>"))) .error( ErrorKind::ArgumentConflict, format!( "cannot specify both a script and an `{}` option.", "--exec".placeholder() ), ) .exit(); } let (path, config) = parse_file_path(file); sandbox::run( &sandbox, script, path.as_deref(), config.as_deref(), args, detach, exec.as_deref(), true, ) .await?; Ok(()) } pub async fn script_run_subcommand( sandbox: bool, build: bool, name: String, script: String, file: Option<PathBuf>, detach: bool, args: Vec<String>, ) -> MicrosandboxCliResult<()> { validate_build_sandbox_conflict(build, sandbox, &script, Some("[NAME]"), Some("<ARGS>")); unsupported_build_error(build, &script, Some("[NAME]")); let (path, config) = parse_file_path(file); sandbox::run( &name, Some(&script), path.as_deref(), config.as_deref(), args, detach, None, true, ) .await?; Ok(()) } pub async fn exe_subcommand( name: String, cpus: Option<u8>, memory: Option<u32>, volumes: Vec<String>, ports: Vec<String>, envs: Vec<String>, workdir: Option<Utf8UnixPathBuf>, scope: Option<String>, exec: Option<String>, args: Vec<String>, ) -> MicrosandboxCliResult<()> { let (image, script) = parse_name_and_script(&name); let image = image.parse::<Reference>()?; if matches!((script, &exec), (Some(_), Some(_))) { MicrosandboxArgs::command() .override_usage(usage("exe", Some("[NAME[~SCRIPT]]"), Some("<ARGS>"))) .error( ErrorKind::ArgumentConflict, format!( "cannot specify both a script and an `{}` option.", "--exec".placeholder() ), ) .exit(); } sandbox::run_temp( &image, script, cpus, memory, volumes, ports, envs, workdir, scope, exec.as_deref(), args, true, ) .await?; Ok(()) } pub async fn up_subcommand( sandbox: bool, build: bool, names: Vec<String>, file: Option<PathBuf>, detach: bool, ) -> MicrosandboxCliResult<()> { validate_build_sandbox_conflict(build, sandbox, "up", Some("[NAMES]"), None); unsupported_build_error(build, "up", Some("[NAMES]")); let (path, config) = parse_file_path(file); orchestra::up(names, path.as_deref(), config.as_deref(), detach).await?; Ok(()) } pub async fn down_subcommand( sandbox: bool, build: bool, names: Vec<String>, file: Option<PathBuf>, ) -> MicrosandboxCliResult<()> { validate_build_sandbox_conflict(build, sandbox, "down", Some("[NAMES]"), None); unsupported_build_error(build, "down", Some("[NAMES]")); let (path, config) = parse_file_path(file); orchestra::down(names, path.as_deref(), config.as_deref()).await?; Ok(()) } /// Handle the status subcommand to show resource usage stats for specified sandboxes pub async fn status_subcommand( sandbox: bool, build: bool, names: Vec<String>, file: Option<PathBuf>, ) -> MicrosandboxCliResult<()> { validate_build_sandbox_conflict(build, sandbox, "status", Some("[NAMES]"), None); unsupported_build_error(build, "status", Some("[NAMES]")); let (path, config) = parse_file_path(file); orchestra::show_status(&names, path.as_deref(), config.as_deref()).await?; Ok(()) } /// Handle the `log` subcommand to show logs for a specific sandbox pub async fn log_subcommand( sandbox: bool, build: bool, name: String, file: Option<PathBuf>, follow: bool, tail: Option<usize>, ) -> MicrosandboxCliResult<()> { validate_build_sandbox_conflict(build, sandbox, "log", Some("[NAME]"), None); unsupported_build_error(build, "log", Some("[NAME]")); // Check if tail command exists when follow mode is requested if follow { let tail_exists = which::which("tail").is_ok(); if !tail_exists { MicrosandboxArgs::command() .override_usage(usage("log", Some("[NAME]"), None)) .error( ErrorKind::InvalidValue, "'tail' command not found. Please install it to use the follow (-f) option.", ) .exit(); } } let (project_dir, config_file) = parse_file_path(file); menv::show_log( project_dir.as_ref(), config_file.as_deref(), &name, follow, tail, ) .await?; Ok(()) } /// Handles the clean subcommand, which removes the .menv directory from a project pub async fn clean_subcommand( _sandbox: bool, name: Option<String>, user: bool, all: bool, file: Option<PathBuf>, force: bool, ) -> MicrosandboxCliResult<()> { if user || all { // User-level cleanup - clean the microsandbox home directory home::clean(force).await?; tracing::info!("user microsandbox home directory cleaned"); // User-level cleanup - clean the user scripts (MSB-ALIAS) if force { toolchain::clean().await?; } tracing::info!("user microsandbox scripts cleaned"); } if !user || all { // Local project cleanup if let Some(sandbox_name) = name { // Clean specific sandbox if sandbox name is provided tracing::info!("cleaning sandbox: {}", sandbox_name); let (path, config) = parse_file_path(file); menv::clean(path, config.as_deref(), Some(&sandbox_name), force).await?; } else { // Clean the entire .menv directory if no sandbox is specified tracing::info!("cleaning entire project environment"); let (path, config) = parse_file_path(file); menv::clean(path, config.as_deref(), None, force).await?; } } Ok(()) } pub async fn server_start_subcommand( host: Option<String>, port: Option<u16>, namespace_dir: Option<PathBuf>, dev_mode: bool, key: Option<String>, detach: bool, reset_key: bool, ) -> MicrosandboxCliResult<()> { microsandbox_server::start(key, host, port, namespace_dir, dev_mode, detach, reset_key).await?; Ok(()) } pub async fn server_stop_subcommand() -> MicrosandboxServerResult<()> { microsandbox_server::stop().await?; Ok(()) } pub async fn server_keygen_subcommand( expire: Option<String>, namespace: Option<String>, ) -> MicrosandboxCliResult<()> { // Convert the string duration to chrono::Duration let duration = if let Some(expire_str) = expire { Some(parse_duration_string(&expire_str)?) } else { None }; // If namespace is None, use "*" to represent all namespaces let namespace_value = namespace.unwrap_or_else(|| "*".to_string()); microsandbox_server::keygen(duration, namespace_value).await?; Ok(()) } /// Handles the server ssh subcommand, which spawns a new SSH session into a sandbox pub async fn server_ssh_subcommand( _namespace: String, _sandbox: bool, _name: String, ) -> MicrosandboxCliResult<()> { MicrosandboxArgs::command() .override_usage(usage("ssh", Some("[NAME]"), None)) .error( ErrorKind::InvalidValue, "SSH functionality is not yet implemented", ) .exit(); } /// Handle the self subcommand, which manages microsandbox itself pub async fn self_subcommand(action: SelfAction) -> MicrosandboxCliResult<()> { match action { SelfAction::Upgrade => { println!( "{} upgrade functionality is not yet implemented", "error:".error() ); return Ok(()); } SelfAction::Uninstall => { // Clean the home directory first home::clean(true).await?; // Clean user scripts toolchain::clean().await?; // Then uninstall the binaries and libraries toolchain::uninstall().await?; } } Ok(()) } /// Handles the install subcommand for installing sandbox scripts from images pub async fn install_subcommand( name: String, alias: Option<String>, cpus: Option<u8>, memory: Option<u32>, volumes: Vec<String>, ports: Vec<String>, envs: Vec<String>, workdir: Option<Utf8UnixPathBuf>, scope: Option<String>, exec: Option<String>, args: Vec<String>, ) -> MicrosandboxCliResult<()> { let (image, script) = parse_name_and_script(&name); let image = image.parse::<Reference>()?; if matches!((script, &exec), (Some(_), Some(_))) { MicrosandboxArgs::command() .override_usage(usage( "install", Some("[NAME[~SCRIPT]] [ALIAS]"), Some("<ARGS>"), )) .error( ErrorKind::ArgumentConflict, format!( "cannot specify both a script and an `{}` option.", "--exec".placeholder() ), ) .exit(); } // If extra args are provided, show a warning as they will be ignored during install if !args.is_empty() { tracing::warn!("Extra arguments will be ignored during install. They will be passed to the sandbox when the alias is used."); } home::install( &image, script, alias.as_deref(), cpus, memory, volumes, ports, envs, workdir, scope, exec.as_deref(), args, true, ) .await?; Ok(()) } /// Handles the uninstall subcommand for removing installed script aliases pub async fn uninstall_subcommand(script: Option<String>) -> MicrosandboxCliResult<()> { match script { Some(script_name) => { // Uninstall the specified script home::uninstall(&script_name).await?; tracing::info!("Successfully uninstalled script: {}", script_name); } None => { // No script specified, print error message MicrosandboxArgs::command() .override_usage(usage("uninstall", Some("[SCRIPT]"), None)) .error( ErrorKind::InvalidValue, "Please specify the name of the script to uninstall.", ) .exit(); } } Ok(()) } pub async fn server_log_subcommand( _sandbox: bool, name: String, namespace: String, follow: bool, tail: Option<usize>, ) -> MicrosandboxCliResult<()> { // Ensure microsandbox home directory exists let namespace_path = env::get_microsandbox_home_path() .join(NAMESPACES_SUBDIR) .join(&namespace); if !namespace_path.exists() { return Err(MicrosandboxCliError::NotFound(format!( "Namespace '{}' not found", namespace ))); } // Reuse the same log viewing functionality menv::show_log(Some(namespace_path), None, &name, follow, tail).await?; Ok(()) } pub async fn server_list_subcommand(namespace: Option<String>) -> MicrosandboxCliResult<()> { // Get the microsandbox home path let microsandbox_home_path = env::get_microsandbox_home_path(); let namespaces_path = microsandbox_home_path.join(NAMESPACES_SUBDIR); // Check if we need to show all namespaces or just one if let Some(namespace) = namespace { // Single namespace mode let namespace_path = namespaces_path.join(&namespace); if !namespace_path.exists() { return Err(MicrosandboxCliError::NotFound(format!( "Namespace '{}' not found", namespace ))); } // Load configuration from the namespace directory let config_result = config::load_config(Some(namespace_path.as_path()), None).await; match config_result { Ok((config, _, _)) => { // Use the common show_list function to display sandboxes menv::show_list(config.get_sandboxes()); } Err(err) => { return Err(MicrosandboxCliError::ConfigError(format!( "Failed to load configuration from namespace '{}': {}", namespace, err ))); } } } else { // All namespaces mode - use the dedicated function match menv::show_list_namespaces(namespaces_path.as_path()).await { Ok(_) => (), Err(err) => { return Err(MicrosandboxCliError::NamespaceError(format!( "Failed to list namespaces: {}", err ))); } } } Ok(()) } pub async fn server_status_subcommand( _sandbox: bool, names: Vec<String>, namespace: Option<String>, ) -> MicrosandboxCliResult<()> { // Get the microsandbox home path let microsandbox_home_path = env::get_microsandbox_home_path(); let namespaces_path = microsandbox_home_path.join(NAMESPACES_SUBDIR); // Check if we need to show all namespaces or just one if let Some(namespace) = namespace { // Single namespace mode let namespace_path = namespaces_path.join(&namespace); if !namespace_path.exists() { return Err(MicrosandboxCliError::NotFound(format!( "Namespace '{}' not found", namespace ))); } orchestra::show_status(&names, Some(namespace_path.as_path()), None).await?; } else { // All namespaces mode // First check if namespaces directory exists if !namespaces_path.exists() { return Err(MicrosandboxCliError::NotFound( "No namespaces directory found".to_string(), )); } // Show status for all namespaces, passing the parent directory // instead of a pre-collected list of namespace directories match orchestra::show_status_namespaces(&names, namespaces_path.as_path()).await { Ok(_) => (), Err(err) => { return Err(MicrosandboxCliError::NamespaceError(format!( "Failed to show namespace statuses: {}", err ))); } } } Ok(()) } pub async fn login_subcommand() -> MicrosandboxCliResult<()> { println!( "{} login functionality is not yet implemented", "error:".error() ); Ok(()) } pub async fn push_subcommand(_image: bool, _name: String) -> MicrosandboxCliResult<()> { println!( "{} push functionality is not yet implemented", "error:".error() ); Ok(()) } //-------------------------------------------------------------------------------------------------- // Functions: Common Errors //-------------------------------------------------------------------------------------------------- fn unsupported_build_error(build: bool, command: &str, positional_placeholder: Option<&str>) { if build { MicrosandboxArgs::command() .override_usage(usage(command, positional_placeholder, None)) .error( ErrorKind::ArgumentConflict, format!( "`{}` and `{}` flags are not yet supported.", "--build".literal(), "-b".literal() ), ) .exit(); } } //-------------------------------------------------------------------------------------------------- // Functions: Helpers //-------------------------------------------------------------------------------------------------- fn usage(command: &str, positional_placeholder: Option<&str>, varargs: Option<&str>) -> String { let mut usage = format!( "{} {} {} {}", "msb".literal(), command.literal(), "[OPTIONS]".placeholder(), positional_placeholder.unwrap_or("").placeholder() ); if let Some(varargs) = varargs { usage.push_str(&format!( " {} {} {}", "[--".literal(), format!("{}...", varargs).placeholder(), "]".literal() )); } usage } fn parse_name_and_script(name_and_script: &str) -> (&str, Option<&str>) { let (name, script) = match name_and_script.split_once(SANDBOX_SCRIPT_SEPARATOR) { Some((name, script)) => (name, Some(script)), None => (name_and_script, None), }; (name, script) } /// Parse a file path into project path and config file name. /// /// If the file path is a directory, it is treated as the project path. /// If the file path is a file, its parent directory is treated as the project path /// and its name is treated as the config file. /// If the file has no parent directory (e.g., a simple filename like "config.yaml") /// or its parent is an empty string, the current directory is used as the project path. /// /// # Arguments /// /// * `file` - Optional file path that could be either a directory or a file /// /// # Returns /// /// Tuple of (Option<PathBuf>, Option<String>) for project path and config file name pub fn parse_file_path(file: Option<PathBuf>) -> (Option<PathBuf>, Option<String>) { let (project_path, config_name) = match file { Some(file_path) => { if file_path.is_dir() { tracing::debug!("File path is a directory: {:?}", file_path); // If it's a directory, it's the project path (Some(file_path), None) } else { // Get the config file name let config_name = file_path .file_name() .and_then(|name| name.to_str()) .map(String::from); // Get the parent directory let parent = file_path.parent(); // Handle the cases: // 1. No parent (None) // 2. Empty parent (Some("")) // 3. Valid parent directory let project_path = match parent { Some(p) if p.as_os_str().is_empty() => { // Parent is empty string, use current directory Some(PathBuf::from(".")) } Some(p) => { // Valid parent directory Some(PathBuf::from(p)) } None => { // No parent, use current directory Some(PathBuf::from(".")) } }; (project_path, config_name) } } None => (None, None), }; (project_path, config_name) } /// Parse a duration string like "1s", "1m", "3h", "2d" into a chrono::Duration fn parse_duration_string(duration_str: &str) -> MicrosandboxCliResult<chrono::Duration> { let duration_str = duration_str.trim(); if duration_str.is_empty() { return Err(MicrosandboxCliError::InvalidArgument( "Empty duration string".to_string(), )); } // Extract the numeric value and unit let (value_str, unit) = duration_str.split_at( duration_str .chars() .position(|c| !c.is_ascii_digit()) .unwrap_or(duration_str.len()), ); if value_str.is_empty() { return Err(MicrosandboxCliError::InvalidArgument(format!( "Invalid duration: {}. No numeric value found.", duration_str ))); } let value: i64 = value_str.parse().map_err(|_| { MicrosandboxCliError::InvalidArgument(format!( "Invalid numeric value in duration: {}", value_str )) })?; match unit { "s" => Ok(chrono::Duration::seconds(value)), "m" => Ok(chrono::Duration::minutes(value)), "h" => Ok(chrono::Duration::hours(value)), "d" => Ok(chrono::Duration::days(value)), "w" => Ok(chrono::Duration::weeks(value)), "mo" => Ok(chrono::Duration::days(value * 30)), // Approximate "y" => Ok(chrono::Duration::days(value * 365)), // Approximate "" => Ok(chrono::Duration::hours(value)), // Default to hours if no unit specified _ => Err(MicrosandboxCliError::InvalidArgument(format!( "Invalid duration unit: {}. Expected one of: s, m, h, d, w, mo, y", unit ))), } } /// Validate that both `--build` and `--sandbox` flags are not specified together. /// /// # Arguments /// /// * `build` - Whether the --build flag is set /// * `sandbox` - Whether the --sandbox flag is set /// * `command` - The command name for the error message /// * `positional_placeholder` - Optional positional arguments placeholder for usage /// * `varargs` - Optional varargs placeholder for usage fn validate_build_sandbox_conflict( build: bool, sandbox: bool, command: &str, positional_placeholder: Option<&str>, varargs: Option<&str>, ) { if build && sandbox { MicrosandboxArgs::command() .override_usage(usage(command, positional_placeholder, varargs)) .error( ErrorKind::ArgumentConflict, format!( "cannot specify both `{}` and `{}` flags", "--sandbox".literal(), "--build".literal() ), ) .exit(); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/microsandbox/microsandbox'

If you have feedback or need assistance with the MCP directory API, please join our Discord server