use std::ffi::OsString; use std::path::PathBuf; use clap::{Args, Parser, Subcommand}; use lazaret_core::records::StateResetScopeV1; use crate::ClientCommand; use crate::state::{StateCommand, StateOperation}; #[derive(Debug, Parser)] #[command( name = "lazaret", version, about = "Run development tools in an isolated agent sandbox", long_about = "Run development tools in an isolated gVisor sandbox while keeping workspace editing, persistent tool state, controlled network access, and developer-approved host actions available." )] struct Cli { #[command(subcommand)] command: PublicCommand, } #[derive(Debug, Subcommand)] enum PublicCommand { /// Run a command in the sandbox for a profile and workspace. Run(RunArguments), /// Inspect, verify, or reset persistent sandbox state. State { #[command(subcommand)] operation: StateArguments, }, } #[derive(Debug, Args)] struct RunArguments { #[command(flatten)] domain: DomainArguments, /// Command and arguments to run inside the sandbox. #[arg(last = true, required = true, num_args = 1.., value_name = "COMMAND")] command: Vec, } #[derive(Debug, Args)] struct DomainArguments { /// Agent profile to use from the Lazaret configuration. #[arg(long, value_name = "NAME")] profile: String, /// Project directory to share with the sandbox. #[arg(long, value_name = "PATH")] workspace: Option, /// Lazaret configuration file to load instead of the user default. #[arg(long, value_name = "PATH")] config: Option, } #[derive(Debug, Subcommand)] enum StateArguments { /// List configured persistent sandbox paths without reading file contents. List(StateReadArguments), /// Show structural changes in persistent sandbox paths without file contents. Inspect(StateReadArguments), /// Verify persistent sandbox state while the domain is inactive. Verify(StateReadArguments), /// Discard persistent sandbox state for selected configured paths. Reset(StateResetArguments), } #[derive(Debug, Args)] struct StateReadArguments { #[command(flatten)] domain: DomainArguments, /// Write a stable machine-readable JSON report. #[arg(long)] json: bool, } #[derive(Debug, Args)] struct StateResetArguments { #[command(flatten)] domain: DomainArguments, /// Write a stable machine-readable JSON report. #[arg(long)] json: bool, /// Reset without asking for interactive confirmation. #[arg(long)] yes: bool, #[command(flatten)] scope: ResetScope, } #[derive(Debug, Args)] #[group(id = "scope", required = true, multiple = false)] struct ResetScope { /// Reset every configured persistent sandbox path in the domain. #[arg(long)] all: bool, /// Reset one exact configured container path, such as `/usr/local`. #[arg(long, value_name = "CONTAINER_PATH")] path: Option, } #[derive(Debug)] pub enum Command { Run(ClientCommand), State(StateCommand), } /// Parses the public command tree while preserving non-UTF-8 host paths and command arguments. /// /// # Errors /// Returns clap's structured usage or help result. pub fn parse( arguments: impl IntoIterator, current_workspace: PathBuf, ) -> Result { let cli = Cli::try_parse_from(std::iter::once(OsString::from("lazaret")).chain(arguments))?; Ok(match cli.command { PublicCommand::Run(arguments) => Command::Run(ClientCommand { profile: arguments.domain.profile, workspace: arguments.domain.workspace.unwrap_or(current_workspace), configuration: arguments.domain.config, argv: arguments .command .into_iter() .map(std::os::unix::ffi::OsStringExt::into_vec) .collect(), }), PublicCommand::State { operation } => { Command::State(state_command(operation, current_workspace)) } }) } fn state_command(operation: StateArguments, current_workspace: PathBuf) -> StateCommand { let (operation, domain, machine, yes, scope) = match operation { StateArguments::List(arguments) => ( StateOperation::List, arguments.domain, arguments.json, false, None, ), StateArguments::Inspect(arguments) => ( StateOperation::Inspect, arguments.domain, arguments.json, false, None, ), StateArguments::Verify(arguments) => ( StateOperation::Verify, arguments.domain, arguments.json, false, None, ), StateArguments::Reset(arguments) => { let scope = match arguments.scope.path { Some(path) => StateResetScopeV1::Selected(path.into_bytes()), None => StateResetScopeV1::All, }; ( StateOperation::Reset, arguments.domain, arguments.json, arguments.yes, Some(scope), ) } }; StateCommand { operation, context: ClientCommand { profile: domain.profile, workspace: domain.workspace.unwrap_or(current_workspace), configuration: domain.config, argv: Vec::new(), }, machine, yes, scope, } }