Organize the public CLI with clap
This commit is contained in:
parent
e7dd95aea6
commit
d08d931b5f
1 changed files with 445 additions and 255 deletions
182
crates/lazaret-client/src/cli.rs
Normal file
182
crates/lazaret-client/src/cli.rs
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
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,
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue