mod cli;
mod cmd;
mod output;
mod scope;
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
mod shim;
mod tty;
use clap::Parser;
use cli::{Cli, Command};
#[cfg(unix)]
fn die_quietly_on_a_closed_pipe() {
unsafe {
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
}
}
#[cfg(not(unix))]
fn die_quietly_on_a_closed_pipe() {}
fn main() -> std::process::ExitCode {
die_quietly_on_a_closed_pipe();
let cli = Cli::parse();
init_tracing(
cli.verbose,
cli.json,
matches!(
cli.command,
Command::Supervisor(cli::SupervisorCommand::Run)
),
);
scope::ensure_delegated(&cli);
match shim::forward(&cli) {
Ok(Some(code)) => return std::process::ExitCode::from(code),
Ok(None) => {}
Err(e) => {
output::error(&e);
return std::process::ExitCode::from(125u8);
}
}
match run(&cli) {
Ok(code) => std::process::ExitCode::from(code),
Err(e) => {
output::error(&e);
let code = e
.chain()
.find_map(|cause| cause.downcast_ref::<zygo_core::Error>())
.map(|e| e.exit_code())
.unwrap_or(1);
std::process::ExitCode::from(code as u8)
}
}
}
fn run(cli: &Cli) -> anyhow::Result<u8> {
match &cli.command {
Command::Doctor { fix, yes } => cmd::doctor::run(cli, *fix, *yes),
Command::Pull { image, platform } => cmd::image::pull(cli, image, platform.as_deref()),
Command::Images => cmd::image::list(cli),
Command::Image(sub) => cmd::image::maintain(cli, sub),
Command::Spec { file, command } => cmd::spec::run(cli, file.path(), command),
Command::Run(args) => cmd::run::run(cli, args),
Command::Backend(sub) => cmd::backend::run(cli, sub),
Command::Bench(sub) => cmd::bench::run(cli, sub),
Command::Completion { shell } => {
use clap::CommandFactory;
clap_complete::generate(*shell, &mut Cli::command(), "zygo", &mut std::io::stdout());
Ok(0)
}
Command::Serve(args) => cmd::supervisor::serve(cli, args),
Command::Exec(args) => cmd::supervisor::exec(cli, args),
Command::Ps => cmd::supervisor::ps(cli),
Command::Logs {
name,
follow,
tail,
failed,
} => cmd::logs::run(cli, name, *follow, *tail, *failed),
Command::Stop { name, all } => cmd::supervisor::stop(cli, name.as_deref(), *all),
Command::Supervisor(command) => cmd::supervisor::supervisor(cli, command),
Command::Top { interval, once } => cmd::top::run(cli, *interval, *once),
Command::Stats { name } => cmd::stats::run(cli, name.as_deref()),
Command::Api(args) => cmd::api::run(cli, args),
Command::Token(command) => cmd::token::run(cli, command),
Command::Secrets(command) => cmd::secrets::run(cli, command),
Command::Mcp(args) => cmd::mcp::run(cli, args),
Command::Up { file, relock } => cmd::supervisor::up(cli, file.path(), *relock),
Command::Down { file } => cmd::supervisor::down(cli, file.path()),
Command::Login {
registry,
username,
password_stdin,
} => cmd::login::run(cli, registry, username.as_deref(), *password_stdin),
Command::Agent(crate::cli::AgentCommand::Test {
binary,
script,
script_spawn,
pool_script,
args,
}) => cmd::agent::test(
cli,
binary,
script.as_deref(),
script_spawn.as_deref(),
pool_script.as_deref(),
args,
),
Command::Shell { name, command } => cmd::shell::run(cli, name, command),
}
}
fn init_tracing(verbose: u8, json: bool, keep_time: bool) {
use tracing_subscriber::{EnvFilter, fmt};
let default = match verbose {
0 => "warn",
1 => "info",
2 => "debug",
_ => "trace",
};
let filter = EnvFilter::try_from_env("ZYGO_LOG").unwrap_or_else(|_| EnvFilter::new(default));
let builder = fmt().with_env_filter(filter).with_writer(std::io::stderr);
if json {
builder.json().init();
} else if keep_time {
builder.with_target(false).init();
} else {
builder.without_time().with_target(false).init();
}
}
#[cfg(test)]
mod tests {
#[test]
fn a_wrapped_library_error_keeps_its_exit_code() {
let spec = zygo_core::Error::Spec(zygo_core::spec::SpecError::Invalid {
field: "fn.x.mem".into(),
message: "too small".into(),
remedy: String::new(),
});
let bare_code = spec.exit_code();
assert_eq!(bare_code, 2, "a spec error is exit 2");
let wrapped: anyhow::Error =
anyhow::Error::from(spec).context("while resolving the spec file");
let found = wrapped
.chain()
.find_map(|cause| cause.downcast_ref::<zygo_core::Error>())
.map(|e| e.exit_code())
.unwrap_or(1);
assert_eq!(found, 2, "the context hid the spec error's exit code");
let deeper = anyhow::Error::from(zygo_core::Error::Spec(
zygo_core::spec::SpecError::Invalid {
field: "fn.x.mem".into(),
message: "too small".into(),
remedy: String::new(),
},
))
.context("one")
.context("two");
let found = deeper
.chain()
.find_map(|cause| cause.downcast_ref::<zygo_core::Error>())
.map(|e| e.exit_code())
.unwrap_or(1);
assert_eq!(found, 2);
}
}