2026-07-18 23:25:40 +09:00
use std ::ffi ::OsString ;
use std ::path ::PathBuf ;
use clap ::{ Args , Parser , Subcommand } ;
2026-07-19 03:44:30 +09:00
use lazaret_core ::records ::StateResetScopeV1 ;
2026-07-18 23:25:40 +09:00
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 < OsString > ,
}
#[ 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 < PathBuf > ,
/// Lazaret configuration file to load instead of the user default.
#[ arg(long, value_name = " PATH " ) ]
config : Option < PathBuf > ,
}
#[ 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 < String > ,
}
#[ 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 < Item = OsString > ,
current_workspace : PathBuf ,
) -> Result < Command , clap ::Error > {
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 ,
}
}