use clap::{ArgAction, Args, Parser, Subcommand};
use std::path::PathBuf;
use std::process::ExitCode;
use tracing_subscriber::EnvFilter;
use crate::cli::build::{self, BuildArgs};
use crate::cli::clean::{self, CleanArgs};
use crate::cli::design_prompt::{self, DesignArgs};
use crate::cli::discard::{self, DiscardArgs};
use crate::cli::logs::{self, LogsArgs};
use crate::cli::ls::{self, LsArgs};
use crate::cli::mcp::{self, McpArgs};
use crate::cli::mcp_self as mcp_self_cli;
use crate::cli::run::{self, RunArgs};
use crate::error::Result;
use crate::paths::{global_config_path, resolve_repo_config, resolve_repo_config_optional};
use crate::{config_init, image_setup, init};
#[derive(Debug, Parser)]
#[command(
name = "outrig",
version,
about = "Run LLM agents with podman-isolated MCP servers."
)]
struct Cli {
#[arg(long, global = true, value_name = "PATH")]
config: Option<PathBuf>,
#[arg(long = "global-config", global = true, value_name = "PATH")]
global_config: Option<PathBuf>,
#[arg(long = "session-root", global = true, value_name = "PATH")]
session_root: Option<PathBuf>,
#[arg(short = 'v', long = "verbose", global = true, action = ArgAction::Count)]
verbose: u8,
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Debug, Subcommand)]
enum Cmd {
Run(RunArgs),
Mcp(McpArgs),
Design(DesignArgs),
Build(BuildArgs),
Config(ConfigArgs),
Init {
#[arg(long)]
force: bool,
},
Image(ImageArgs),
Ls(LsArgs),
Logs(LogsArgs),
Discard(DiscardArgs),
Clean(CleanArgs),
}
#[derive(Debug, Args)]
struct ConfigArgs {
#[command(subcommand)]
cmd: ConfigCmd,
}
#[derive(Debug, Subcommand)]
enum ConfigCmd {
Init {
#[arg(long)]
force: bool,
},
}
#[derive(Debug, Args)]
struct ImageArgs {
#[command(subcommand)]
cmd: ImageCmd,
}
#[derive(Debug, Subcommand)]
enum ImageCmd {
Add {
name: Option<String>,
#[arg(long)]
force: bool,
},
Init {
dir: Option<PathBuf>,
#[arg(long)]
force: bool,
},
Build {
dir: Option<PathBuf>,
#[arg(long, value_name = "REF")]
tag: Option<String>,
#[arg(long = "no-test")]
no_test: bool,
#[arg(long = "no-cache")]
no_cache: bool,
},
Inspect {
#[arg(long)]
remote: bool,
#[arg(value_name = "REF")]
image_ref: String,
},
}
pub fn run() -> ExitCode {
let cli = Cli::parse();
init_tracing(cli.verbose);
outrig::container::install_panic_hook();
tracing::debug!("outrig starting");
match dispatch(&cli) {
Ok(0) => ExitCode::SUCCESS,
Ok(code) => ExitCode::from(code.clamp(0, 255) as u8),
Err(e) => {
eprintln!("error: {e}");
ExitCode::from(1)
}
}
}
fn dispatch(cli: &Cli) -> Result<i32> {
match &cli.cmd {
Cmd::Run(args) => {
let (repo_config, global_config, runtime) = repo_cmd_ctx(cli, false)?;
runtime.block_on(run::execute(
&repo_config,
&global_config,
cli.session_root.as_deref(),
args,
cli.verbose,
))
}
Cmd::Mcp(args) => {
if args.is_self_description() {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
return runtime.block_on(mcp_self_cli::execute(args));
}
let (repo_config, global_config, runtime) = repo_cmd_ctx(cli, false)?;
runtime.block_on(mcp::execute(
&repo_config,
&global_config,
cli.session_root.as_deref(),
args,
cli.verbose,
))
}
Cmd::Design(args) => design_prompt::execute(args),
Cmd::Build(args) => {
let (repo_config, global_config, runtime) = repo_cmd_ctx(cli, true)?;
runtime.block_on(build::execute(&repo_config, &global_config, args))
}
Cmd::Config(args) => match &args.cmd {
ConfigCmd::Init { force } => {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
runtime.block_on(config_init::run(*force, cli.global_config.as_deref()))?;
Ok(0)
}
},
Cmd::Init { force } => {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
runtime.block_on(init::run(*force, cli.global_config.as_deref()))?;
Ok(0)
}
Cmd::Image(args) => match &args.cmd {
ImageCmd::Add { name, force } => {
let cwd = std::env::current_dir()?;
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
runtime.block_on(image_setup::add::run(
&cwd,
cli.global_config.as_deref(),
name.clone(),
*force,
))?;
Ok(0)
}
ImageCmd::Init { dir, force } => {
let cwd = std::env::current_dir()?;
image_setup::init::run(&cwd, dir.as_deref(), *force)?;
Ok(0)
}
ImageCmd::Build {
dir,
tag,
no_test,
no_cache,
} => {
let cwd = std::env::current_dir()?;
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
runtime.block_on(image_setup::build::run(
&cwd,
dir.as_deref(),
tag.as_deref(),
*no_test,
*no_cache,
))?;
Ok(0)
}
ImageCmd::Inspect { remote, image_ref } => {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
runtime.block_on(image_setup::inspect::run(image_ref, *remote))?;
Ok(0)
}
},
Cmd::Ls(args) => {
let (cwd, global, runtime) = session_cmd_ctx(cli)?;
let session_root = cli.session_root.as_deref();
let repo_cfg = cli.config.as_deref();
runtime.block_on(ls::execute(args, session_root, repo_cfg, &global, &cwd))
}
Cmd::Logs(args) => {
let (cwd, global, runtime) = session_cmd_ctx(cli)?;
let session_root = cli.session_root.as_deref();
let repo_cfg = cli.config.as_deref();
runtime.block_on(logs::execute(args, session_root, repo_cfg, &global, &cwd))
}
Cmd::Discard(args) => {
let (cwd, global, runtime) = session_cmd_ctx(cli)?;
let session_root = cli.session_root.as_deref();
let repo_cfg = cli.config.as_deref();
runtime.block_on(discard::execute(
args,
session_root,
repo_cfg,
&global,
&cwd,
))
}
Cmd::Clean(args) => {
let (cwd, global, runtime) = session_cmd_ctx(cli)?;
let session_root = cli.session_root.as_deref();
let repo_cfg = cli.config.as_deref();
runtime.block_on(clean::execute(args, session_root, repo_cfg, &global, &cwd))
}
}
}
fn init_tracing(verbose: u8) {
let outrig_log = std::env::var("OUTRIG_LOG").ok();
let rust_log = std::env::var("RUST_LOG").ok();
let mut filter =
EnvFilter::try_new(log_filter_spec(outrig_log.as_deref(), rust_log.as_deref()))
.unwrap_or_else(|_| EnvFilter::new("info"));
if verbose >= 2 {
filter = filter.add_directive(
"outrig=trace"
.parse()
.expect("hard-coded outrig trace directive must parse"),
);
}
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(std::io::stderr)
.init();
if verbose >= 2 {
tracing::trace!(target: "outrig", "verbose tracing enabled");
}
}
fn log_filter_spec<'a>(outrig_log: Option<&'a str>, rust_log: Option<&'a str>) -> &'a str {
outrig_log.or(rust_log).unwrap_or("info")
}
fn session_cmd_ctx(cli: &Cli) -> Result<(PathBuf, PathBuf, tokio::runtime::Runtime)> {
let cwd = std::env::current_dir()?;
let global = global_config_path(cli.global_config.as_deref());
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
Ok((cwd, global, runtime))
}
fn repo_cmd_ctx(
cli: &Cli,
require_config: bool,
) -> Result<(PathBuf, PathBuf, tokio::runtime::Runtime)> {
let cwd = std::env::current_dir()?;
let repo_config = if require_config {
resolve_repo_config(cli.config.as_deref(), &cwd)?
} else {
resolve_repo_config_optional(cli.config.as_deref(), &cwd)
};
let global_config = global_config_path(cli.global_config.as_deref());
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
Ok((repo_config, global_config, runtime))
}
#[cfg(test)]
mod tests {
use clap::Parser;
use super::{Cli, Cmd, ImageCmd, log_filter_spec};
#[test]
fn outrig_log_wins_over_rust_log() {
assert_eq!(
log_filter_spec(Some("outrig=trace"), Some("debug")),
"outrig=trace"
);
}
#[test]
fn rust_log_is_used_when_outrig_log_is_unset() {
assert_eq!(log_filter_spec(None, Some("debug")), "debug");
}
#[test]
fn log_filter_defaults_to_info() {
assert_eq!(log_filter_spec(None, None), "info");
}
#[test]
fn image_inspect_defaults_to_local() {
let cli =
Cli::try_parse_from(["outrig", "image", "inspect", "rust-dev"]).expect("arg parses");
let Cmd::Image(args) = cli.cmd else {
panic!("expected image command");
};
let ImageCmd::Inspect { remote, image_ref } = args.cmd else {
panic!("expected image inspect command");
};
assert!(!remote);
assert_eq!(image_ref, "rust-dev");
}
#[test]
fn image_inspect_remote_arg_parses() {
let cli = Cli::try_parse_from([
"outrig",
"image",
"inspect",
"--remote",
"quay.io/acme/rust-dev:latest",
])
.expect("arg parses");
let Cmd::Image(args) = cli.cmd else {
panic!("expected image command");
};
let ImageCmd::Inspect { remote, image_ref } = args.cmd else {
panic!("expected image inspect command");
};
assert!(remote);
assert_eq!(image_ref, "quay.io/acme/rust-dev:latest");
}
}