Restore profile-first interactive launches

This commit is contained in:
ms2k 2026-07-19 13:59:43 +09:00
commit b9dbd78087
2 changed files with 67 additions and 15 deletions

View file

@ -12,7 +12,9 @@ use crate::state::{StateCommand, StateOperation};
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."
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.",
override_usage = "lazaret <PROFILE> [OPTIONS] [-- <COMMAND>...]\n lazaret <COMMAND>",
after_help = "Launch a profile directly with `lazaret PROFILE`. Add `-- COMMAND...` to override the default /bin/bash environment entry."
)]
struct Cli {
#[command(subcommand)]
@ -34,8 +36,8 @@ enum PublicCommand {
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 and arguments to run instead of the default `/bin/bash`.
#[arg(last = true, num_args = 1.., value_name = "COMMAND")]
command: Vec<OsString>,
}
@ -112,17 +114,14 @@ pub fn parse(
arguments: impl IntoIterator<Item = OsString>,
current_workspace: PathBuf,
) -> Result<Command, clap::Error> {
let arguments = expand_profile_launch(arguments);
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(),
argv: command_argv(arguments.command),
}),
PublicCommand::State { operation } => {
Command::State(state_command(operation, current_workspace))
@ -130,6 +129,29 @@ pub fn parse(
})
}
fn expand_profile_launch(arguments: impl IntoIterator<Item = OsString>) -> Vec<OsString> {
let mut arguments = arguments.into_iter().collect::<Vec<_>>();
if arguments.first().is_some_and(|first| {
first != "run"
&& first != "state"
&& first.to_str().is_some_and(|first| !first.starts_with('-'))
}) {
arguments.insert(0, OsString::from("--profile"));
arguments.insert(0, OsString::from("run"));
}
arguments
}
fn command_argv(command: Vec<OsString>) -> Vec<Vec<u8>> {
if command.is_empty() {
return vec![b"/bin/bash".to_vec()];
}
command
.into_iter()
.map(std::os::unix::ffi::OsStringExt::into_vec)
.collect()
}
fn state_command(operation: StateArguments, current_workspace: PathBuf) -> StateCommand {
let (operation, domain, machine, yes, scope) = match operation {
StateArguments::List(arguments) => (

View file

@ -233,11 +233,31 @@ fn unsupported_host_and_configuration_errors_keep_their_cause() {
}
#[test]
fn command_line_requires_an_explicit_profile_and_preserves_command_bytes() {
fn command_line_accepts_profile_entry_and_preserves_explicit_command_bytes() {
let parsed =
lazaret_client::cli::parse(["agent"].map(OsString::from), PathBuf::from("/current"))
.unwrap();
let lazaret_client::cli::Command::Run(parsed) = parsed else {
panic!("expected run command");
};
assert_eq!(parsed.profile, "agent");
assert_eq!(parsed.workspace, PathBuf::from("/current"));
assert_eq!(parsed.configuration, None);
assert_eq!(parsed.argv, [b"/bin/bash".to_vec()]);
let parsed = lazaret_client::cli::parse(
["run", "--profile", "state"].map(OsString::from),
PathBuf::from("/current"),
)
.unwrap();
let lazaret_client::cli::Command::Run(parsed) = parsed else {
panic!("expected run command");
};
assert_eq!(parsed.profile, "state");
assert_eq!(parsed.argv, [b"/bin/bash".to_vec()]);
let parsed = lazaret_client::cli::parse(
[
"run",
"--profile",
"agent",
"--workspace",
"/project",
@ -305,6 +325,8 @@ fn public_help_explains_commands_and_options() {
&["--help"][..],
&[
"Run development tools in an isolated gVisor sandbox",
"Usage: lazaret <PROFILE>",
"lazaret PROFILE",
"run",
"state",
][..],
@ -314,7 +336,7 @@ fn public_help_explains_commands_and_options() {
&[
"Agent profile to use",
"Project directory to share",
"Command and arguments to run",
"default `/bin/bash`",
],
),
(
@ -347,11 +369,10 @@ fn public_help_explains_commands_and_options() {
}
#[test]
fn clap_rejects_missing_profile_command_and_ambiguous_reset_scope() {
fn clap_rejects_missing_profile_and_ambiguous_reset_scope() {
let workspace = PathBuf::from("/current");
for arguments in [
vec!["run", "--", "tool"],
vec!["run", "--profile", "agent"],
vec!["state", "reset", "--profile", "agent"],
vec![
"state",