lazaret/crates/lazaret-client/src/cli.rs

182 lines
5.4 KiB
Rust
Raw Normal View History

2026-07-18 23:25:40 +09:00
use std::ffi::OsString;
use std::path::PathBuf;
use clap::{Args, Parser, Subcommand};
use lazaret_host_coordinator::domain_store::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<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,
}
}