//! Command arguments and presentation. Raw execution archives never depend on the inline budget. use crate::*; use clap::{Parser, Subcommand}; #[derive(Parser, Debug)] #[command(name = "box", about = "Disposable build and test boxes")] pub struct Args { #[arg(long, global = true, help = "Containers service hostname[:port]")] pub host: Option, #[command(subcommand)] pub command: Operation, } #[derive(Subcommand, Debug)] pub enum Operation { Create { #[arg(long)] flake: Option, #[arg(long, value_parser = clap::value_parser!(u32).range(1..))] ttl: Option, }, Exec { id: String, #[arg(long, default_value_t = 60, value_parser = clap::value_parser!(u32).range(1..=1800))] timeout: u32, #[arg(long, default_value_t = excerpt::DEFAULT_BYTES)] inline_bytes: u32, #[arg(last = true, required = true)] cmd: Vec, }, Put { id: String, world_path: String, box_path: String, }, Get { id: String, box_path: String, world_path: String, }, Logs { id: String, }, Ls, Rm { id: String, }, } fn notices(result: &Executed) { for (name, path) in [ ("stdout", &result.stdout_path), ("stderr", &result.stderr_path), ("result", &result.output_log), ] { if let Some(path) = path { eprintln!("box {name} archive: {path}"); } } if let Some(notice) = &result.output_notice { eprintln!("box: {notice}"); } if result.timed_out { eprintln!("box: command timed out"); } } pub fn run(args: Args) -> Result { let h = args.host; let service = BoxPlugin; match args.command { Operation::Create { flake, ttl } => { let config = Config { nixos: flake.is_none().then(|| Nixos::Text(NODE_MODULE.into())), flake, system: None, }; let c = service.create(Create { config, ttl_secs: ttl, owner: Some("box CLI".into()), host: h, })?; println!("{}", c.id); if c.state != "running" { if let Some(tail) = c.build_log_tail { eprintln!("{tail}"); } if let Some(path) = c.build_log_path { eprintln!("box build archive: {path}"); } return Err(error(format!( "box {} failed: {}; remove with box rm", c.id, c.error.unwrap_or_default() ))); } } Operation::Exec { id, timeout, inline_bytes, cmd, } => { let result = service.exec(Exec { id, cmd: Command::Argv(cmd), stdin: None, timeout_secs: Some(timeout), inline_bytes: Some(inline_bytes), host: h, })?; std::io::stdout().write_all(result.stdout.as_bytes())?; std::io::stderr().write_all(result.stderr.as_bytes())?; notices(&result); return Ok(result.exit); } Operation::Put { id, world_path, box_path, } => { if Path::new(&world_path).is_dir() { service.put_tree(PutTree { id, from_vfs_dir: world_path, to_dir: box_path, host: h, })?; } else { service.put_file(PutFile { id, path: box_path, text: None, from_vfs: Some(world_path), host: h, })?; } } Operation::Get { id, box_path, world_path, } => { let host = resolve(&id, h.as_deref())?; let kind = checked( &host, &id, json!([ "sh", "-c", "if [ -d \"$1\" ]; then printf directory; elif [ -f \"$1\" ]; then printf file; else exit 1; fi", "sh", box_path ]), )?; if kind == "directory" { service.get_tree(GetTree { id, from_dir: box_path, to_vfs_dir: world_path, host: h, })?; } else { service.get_file(GetFile { id, path: box_path, to_vfs: Some(world_path), host: h, })?; } } Operation::Logs { id } => { let log = service.log(Id { id, host: h })?; print!("{}", log.text); eprintln!("box build archive: {} ({} bytes)", log.path, log.bytes); if let Some(notice) = log.notice { eprintln!("box: {notice}"); } } Operation::Ls => { let list = service.list(List { host: h })?; for c in list.containers { println!("{}\t{}\t{}", c.id, c.state, c.expires_at); } } Operation::Rm { id } => { service.delete(Id { id, host: h })?; } } Ok(0) } #[cfg(test)] mod tests { use super::*; #[test] fn argument_forms_and_validation() { for args in [ vec!["box", "create"], vec![ "box", "create", "--flake", "github:example/project#container", "--ttl", "600", ], vec![ "box", "exec", "tc-abc123", "--timeout", "5", "--", "sh", "-c", "exit 7", ], vec!["box", "put", "tc-abc123", "dir", "/work"], vec!["box", "get", "tc-abc123", "/work", "out"], vec!["box", "logs", "tc-abc123"], vec!["box", "ls"], vec!["box", "rm", "tc-abc123"], vec!["box", "--host", "localhost:8093", "ls"], ] { assert!(Args::try_parse_from(args).is_ok()); } for args in [ vec!["box", "create", "--ttl", "0"], vec!["box", "exec", "tc-abc123", "--"], vec![ "box", "exec", "tc-abc123", "--timeout", "1801", "--", "true", ], vec!["box", "exec", "tc-abc123", "true"], vec!["box", "put", "tc-abc123", "a"], vec!["box", "unknown"], ] { assert!(Args::try_parse_from(args).is_err()); } let args = Args::try_parse_from([ "box", "exec", "tc-abc123", "--inline-bytes", "0", "--", "echo", "--flag", "space here", ]) .unwrap(); match args.command { Operation::Exec { cmd, inline_bytes, .. } => { assert_eq!(cmd, ["echo", "--flag", "space here"]); assert_eq!(inline_bytes, 0); } _ => panic!("wrong operation"), } } }