use std::ffi::OsString;
use clap::{CommandFactory, Parser, Subcommand, ValueEnum, error::ErrorKind};
use crate::commands::api_server::ApiServerArgs;
use crate::commands::devices::DevicesArgs;
use crate::commands::doctor::DoctorArgs;
use crate::commands::invoke::InvokeArgs;
use crate::commands::mcp::McpArgs;
use crate::commands::plugin::PluginArgs;
use crate::commands::run::RunArgs;
use crate::commands::runner::RunnerArgs;
use crate::commands::serve::ServeArgs;
#[derive(Debug, Parser)]
#[command(
name = "auv",
version,
about = "Invoke and inspect core computer-use capabilities",
long_about = "AUV turns computer-use operations into command-like, inspectable, and recorded runs.\n\nThe root CLI owns core invoke, doctor, API-server, Device/Run/Runner control, and MCP frontends. Installed auv-* executables extend it with application-owned commands.",
after_long_help = "Examples:\n # Inspect available core invoke commands\n auv invoke --help\n\n # Diagnose local automation readiness\n auv doctor\n\n # Run an installed application plugin\n auv balatro --help\n\nUse `auv plugin list` to inspect external commands visible on PATH."
)]
struct RootArgs {
#[arg(short, long, action = clap::ArgAction::Count, global = true)]
verbose: u8,
#[arg(long, value_enum, hide = true)]
xtask: Option<Xtask>,
#[arg(long, value_name = "NAME")]
device: Option<String>,
#[arg(long, value_name = "ID")]
device_id: Option<String>,
#[arg(long, value_name = "ID")]
run: Option<String>,
#[command(subcommand)]
command: Option<RootCommand>,
}
#[derive(Debug, Subcommand)]
enum RootCommand {
Doctor(DoctorArgs),
Invoke(InvokeArgs),
#[command(hide = true)]
ApiServer(ApiServerArgs),
Serve(ServeArgs),
#[command(
long_about = "Devices are AUV execution targets exposed by a daemon. A local daemon publishes this machine as a Device; pairing adds another daemon as a remotely selectable Device.\n\n`auv devices list` combines the local daemon with saved paired profiles and reports whether each target is online. Pairing credentials stay in the local profile store and are reused automatically by later commands."
)]
Devices(DevicesArgs),
#[command(visible_alias = "runners")]
Runner(RunnerArgs),
Run(RunArgs),
Mcp(McpArgs),
Plugin(PluginArgs),
#[command(external_subcommand)]
External(Vec<OsString>),
}
#[derive(Clone, Copy, Debug, ValueEnum)]
enum Xtask {
GenerateSwiftBridge,
}
pub async fn run_root() -> Result<i32, String> {
let arguments = std::env::args_os().skip(1).collect::<Vec<_>>();
init_diagnostics(verbosity(&arguments));
run_os(arguments).await
}
async fn run_os(arguments: Vec<OsString>) -> Result<i32, String> {
if arguments.is_empty() {
print!("{}", help_text());
return Ok(0);
}
let mut argv = Vec::with_capacity(arguments.len() + 1);
argv.push(OsString::from("auv"));
argv.extend(arguments);
let parsed = match RootArgs::try_parse_from(argv) {
Ok(parsed) => parsed,
Err(error) => {
return match error.kind() {
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => {
print!("{error}");
Ok(0)
}
_ => Err(error.to_string()),
};
}
};
let selection = auv::selection::RootSelection {
device_name: parsed.device,
device_id: parsed.device_id,
run_id: parsed.run,
};
let project_root = std::env::current_dir().map_err(|error| format!("failed to resolve current directory: {error}"))?;
if let Some(Xtask::GenerateSwiftBridge) = parsed.xtask {
let outputs = crate::xtask::generate_swift_bridge_for_ide(&project_root)?;
println!("generated Swift bridge files for IDE indexing");
for output in outputs {
println!("output: {output}");
}
return Ok(0);
}
match parsed.command {
None => {
print!("{}", help_text());
Ok(0)
}
Some(RootCommand::Doctor(args)) => crate::commands::doctor::run(args).await,
Some(RootCommand::Invoke(args)) => crate::commands::invoke::run(args, &selection, &project_root).await,
Some(RootCommand::ApiServer(args)) => crate::commands::api_server::run(args, &project_root).await,
Some(RootCommand::Serve(args)) => crate::commands::serve::run(args, &project_root).await,
Some(RootCommand::Devices(args)) => crate::commands::devices::run(args, &selection).await,
Some(RootCommand::Runner(args)) => crate::commands::runner::run(args, &selection).await,
Some(RootCommand::Run(args)) => crate::commands::run::run(args, &selection).await,
Some(RootCommand::Mcp(args)) => crate::commands::mcp::run(args, &project_root).await,
Some(RootCommand::Plugin(args)) => crate::commands::plugin::run(args).await,
Some(RootCommand::External(mut arguments)) => {
let command_name = arguments.remove(0);
crate::commands::plugin::execute(&command_name, &arguments, &selection, &project_root).await
}
}
}
pub fn exit_status(result: Result<i32, String>) -> i32 {
match result {
Ok(exit_code) => exit_code,
Err(error) => {
eprintln!("error: {error}");
1
}
}
}
fn verbosity(arguments: &[OsString]) -> u8 {
arguments.iter().fold(0_u8, |count, argument| {
let Some(argument) = argument.to_str() else {
return count;
};
if argument == "--verbose" {
count.saturating_add(1)
} else if argument.starts_with('-') && !argument.starts_with("--") && argument[1..].bytes().all(|byte| byte == b'v') {
count.saturating_add(u8::try_from(argument.len() - 1).unwrap_or(u8::MAX))
} else {
count
}
})
}
fn init_diagnostics(verbosity: u8) {
let level = match verbosity {
0 => return,
1 => "info",
2 => "debug",
_ => "trace",
};
let filter = tracing_subscriber::EnvFilter::new(format!("off,auv_cli={level}"));
let _ = tracing_subscriber::fmt().with_env_filter(filter).with_writer(std::io::stderr).with_target(false).try_init();
}
pub fn help_text() -> String {
RootArgs::command().render_long_help().to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn root_selection_remains_unresolved_after_parsing() {
let parsed = RootArgs::try_parse_from([
"auv",
"--device-id",
"abc",
"--run",
"def",
"invoke",
"display.list",
])
.unwrap();
assert_eq!(parsed.device_id.as_deref(), Some("abc"));
assert_eq!(parsed.run.as_deref(), Some("def"));
}
#[test]
fn serve_accepts_repeated_runner_provider_manifests() {
let parsed = RootArgs::try_parse_from([
"auv",
"serve",
"--runner-provider",
"first.json",
"--runner-provider",
"second.json",
])
.unwrap();
let Some(RootCommand::Serve(args)) = parsed.command else {
panic!("serve command")
};
assert_eq!(
args.runner_providers,
[
std::path::PathBuf::from("first.json"),
std::path::PathBuf::from("second.json")
]
);
}
}