use std::path::PathBuf;
use clap::{Parser, Subcommand};
use clap_complete::Shell;
mod access;
#[cfg(feature = "acme-dns")]
mod acme_dns;
mod alias;
mod apply;
mod authcmd;
mod blob;
mod build;
mod bundle;
mod client;
#[cfg(feature = "cluster")]
mod cloudflare;
mod cluster;
#[cfg(all(feature = "cluster", feature = "acme-dns"))]
mod cluster_tls;
mod completions;
mod compute;
pub use boatramp_node::config;
mod config_cmd;
mod dlq;
#[cfg(feature = "acme-dns")]
mod dns;
mod domains;
mod error;
mod function;
mod gateway;
mod handler_validate;
mod workflow;
#[cfg(feature = "cluster")]
mod join;
mod logs;
mod manage;
#[cfg(feature = "mcp")]
mod mcp;
mod migrate;
#[cfg(feature = "operator")]
mod operator;
mod project;
mod security;
mod serve;
mod sync;
mod token;
use error::CliError;
#[derive(Debug, Parser)]
#[command(name = "boatramp", version, about, long_about = None)]
struct Cli {
#[arg(long, global = true)]
config: Option<PathBuf>,
#[arg(long, global = true, env = "BOATRAMP_PROJECT")]
project: Option<String>,
#[command(subcommand)]
command: Command,
}
#[derive(Debug, Subcommand)]
#[allow(clippy::large_enum_variant)]
enum Command {
Serve(serve::ServeArgs),
Sync(sync::SyncArgs),
Apply(apply::ApplyArgs),
Build(build::BuildArgs),
Bundle(bundle::BundleArgs),
Validate(build::ValidateArgs),
Deployments(manage::DeploymentsArgs),
Function(function::FunctionArgs),
Workflow(workflow::WorkflowArgs),
Rollback(manage::RollbackArgs),
Status(manage::StatusArgs),
Domain(domains::DomainArgs),
Alias(alias::AliasArgs),
Access(access::AccessArgs),
Token(token::TokenArgs),
Cluster(cluster::ClusterArgs),
Security(security::SecurityArgs),
Auth(authcmd::AuthArgs),
Gateway(gateway::GatewayArgs),
Compute(compute::ComputeArgs),
Blob(blob::BlobArgs),
Config(config_cmd::ConfigArgs),
Migrate(migrate::MigrateArgs),
Project(project::ProjectArgs),
#[cfg(feature = "acme-dns")]
Dns(dns::DnsArgs),
Logs(logs::LogsArgs),
Stats(logs::StatsArgs),
Dlq(dlq::DlqArgs),
Prune(manage::PruneArgs),
Scrub(manage::ScrubArgs),
CertStatus(manage::CertStatusArgs),
Completions {
#[arg(value_enum)]
shell: Shell,
},
Man,
#[cfg(feature = "cluster")]
Cloudflare(cloudflare::CloudflareArgs),
#[cfg(feature = "operator")]
Operator(operator::OperatorArgs),
#[cfg(feature = "mcp")]
Mcp(mcp::McpArgs),
}
fn main() -> std::process::ExitCode {
match run() {
Ok(()) => std::process::ExitCode::SUCCESS,
Err(err) => {
eprintln!("error: {err}");
let mut source = std::error::Error::source(&err);
while let Some(cause) = source {
eprintln!(" caused by: {cause}");
source = cause.source();
}
std::process::ExitCode::FAILURE
}
}
}
fn run() -> Result<(), CliError> {
#[cfg(target_os = "linux")]
if std::env::args().nth(1).as_deref() == Some("__sandbox") {
return run_sandbox();
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
if std::env::args().nth(1).as_deref()
== Some(boatramp_firecracker::embedded_backend::VMM_RUN_SUBCOMMAND)
{
return run_vmm_worker(std::env::args().nth(2));
}
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.map_err(CliError::Runtime)?
.block_on(async_main())
}
#[cfg(target_os = "linux")]
fn run_sandbox() -> Result<(), CliError> {
use std::io::{BufRead, Write};
let stdin = std::io::stdin();
let mut reader = std::io::BufReader::new(stdin.lock());
let mut plan_line = String::new();
reader
.read_line(&mut plan_line)
.map_err(CliError::SandboxPlanRead)?;
let plan: boatramp_container::SandboxPlan =
serde_json::from_str(plan_line.trim()).map_err(CliError::SandboxPlanParse)?;
boatramp_container::worker::prepare(&plan).map_err(CliError::SandboxPrepare)?;
{
let mut out = std::io::stdout().lock();
out.write_all(b"ready\n")
.and_then(|()| out.flush())
.map_err(CliError::SandboxReady)?;
}
let mut go = String::new();
reader.read_line(&mut go).map_err(CliError::SandboxGo)?;
if go.trim() != "go" {
return Err(CliError::SandboxHandshake { got: go });
}
let code = boatramp_container::worker::jail_and_run(&plan).map_err(CliError::SandboxWorker)?;
std::process::exit(code);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn run_vmm_worker(json: Option<String>) -> Result<(), CliError> {
use boatramp_firecracker::embedded_backend::{run_jailed_worker, WorkerConfig};
let json = json.ok_or(CliError::VmmMissingConfig)?;
let cfg: WorkerConfig = serde_json::from_str(&json).map_err(CliError::VmmConfigParse)?;
run_jailed_worker(cfg).map_err(CliError::VmmWorker)?;
Ok(())
}
async fn async_main() -> Result<(), CliError> {
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("boatramp=info"));
let builder = tracing_subscriber::fmt().with_env_filter(filter);
let json = std::env::var("BOATRAMP_LOG_FORMAT")
.map(|v| v.eq_ignore_ascii_case("json"))
.unwrap_or(false);
if json {
builder.json().init();
} else {
builder.init();
}
let cli = Cli::parse();
match cli.command {
Command::Completions { shell } => {
completions::completions(shell);
return Ok(());
}
Command::Man => {
completions::man().map_err(CliError::Man)?;
return Ok(());
}
_ => {}
}
if matches!(cli.command, Command::Serve(_)) {
let path = cli.config.unwrap_or_else(|| PathBuf::from("boatramp.cfg"));
let config = config::ServerConfig::load(&path)?;
let Command::Serve(args) = cli.command else {
unreachable!("guarded by matches! above")
};
serve::run(args, &config).await?;
return Ok(());
}
if matches!(cli.command, Command::Security(_)) {
let path = cli.config.unwrap_or_else(|| PathBuf::from("boatramp.cfg"));
let config = config::ServerConfig::load(&path)?;
let Command::Security(args) = cli.command else {
unreachable!("guarded by matches! above")
};
security::run(args, &config)?;
return Ok(());
}
if matches!(cli.command, Command::Migrate(_)) {
let path = cli.config.unwrap_or_else(|| PathBuf::from("boatramp.cfg"));
let config = config::ServerConfig::load(&path)?;
let Command::Migrate(args) = cli.command else {
unreachable!("guarded by matches! above")
};
migrate::run(args, &config).await?;
return Ok(());
}
#[cfg(feature = "operator")]
if matches!(cli.command, Command::Operator(_)) {
let Command::Operator(args) = cli.command else {
unreachable!("guarded by matches! above")
};
operator::run(args).await?;
return Ok(());
}
#[cfg(feature = "mcp")]
if matches!(cli.command, Command::Mcp(_)) {
let Command::Mcp(args) = cli.command else {
unreachable!("guarded by matches! above")
};
mcp::run(args).await?;
return Ok(());
}
let path = cli.config.unwrap_or_else(|| PathBuf::from("project.cfg"));
let mut config = config::ProjectConfig::load(&path)?;
if let Some(project) = cli.project.clone() {
config.publish.project = Some(project);
}
match cli.command {
Command::Serve(_) => unreachable!("handled above"),
Command::Security(_) => unreachable!("handled above"),
Command::Migrate(_) => unreachable!("handled above"),
#[cfg(feature = "operator")]
Command::Operator(_) => unreachable!("handled above"),
#[cfg(feature = "mcp")]
Command::Mcp(_) => unreachable!("handled above"),
Command::Completions { .. } | Command::Man => unreachable!("handled above"),
Command::Sync(args) => sync::run(args, &config).await?,
Command::Apply(args) => apply::run(args, &config).await?,
Command::Build(args) => build::run(args, &config).await?,
Command::Bundle(args) => bundle::run(args, &config).await?,
Command::Validate(args) => build::validate(args)?,
Command::Deployments(args) => manage::list(args, &config).await?,
Command::Function(args) => function::run(args, &config).await?,
Command::Workflow(args) => workflow::run(args, &config).await?,
Command::Rollback(args) => manage::rollback(args, &config).await?,
Command::Status(args) => manage::status(args, &config).await?,
Command::Domain(args) => domains::run(args, &config).await?,
Command::Alias(args) => alias::run(args, &config).await?,
Command::Project(args) => project::run(args, &config).await?,
Command::Access(args) => access::run(args, &config).await?,
Command::Token(args) => token::run(args, &config).await?,
Command::Cluster(args) => cluster::run(args, &config).await?,
Command::Auth(args) => authcmd::run(args, &config).await?,
Command::Gateway(args) => gateway::run(args, &config).await?,
Command::Compute(args) => compute::run(args, &config).await?,
Command::Blob(args) => blob::run(args, &config).await?,
Command::Config(args) => config_cmd::run(args, &config).await?,
#[cfg(feature = "acme-dns")]
Command::Dns(args) => dns::run(args, &config).await?,
Command::Logs(args) => logs::run(args, &config).await?,
Command::Stats(args) => logs::stats(args, &config).await?,
Command::Dlq(args) => dlq::run(args, &config).await?,
Command::Prune(args) => manage::prune(args, &config).await?,
Command::Scrub(args) => manage::scrub(args, &config).await?,
Command::CertStatus(args) => manage::cert_status(args, &config).await?,
#[cfg(feature = "cluster")]
Command::Cloudflare(args) => cloudflare::run(args, &config).await?,
}
Ok(())
}