#![deny(warnings)]
#![cfg_attr(not(test), deny(clippy::unwrap_used))]
mod build_info;
mod cli;
mod commands;
mod error;
mod filesystem;
mod global_lock;
mod logging;
mod machine;
mod middlewares;
mod openapi;
mod paths;
mod restart;
mod routes;
mod routine_storage;
mod routines;
mod service;
mod sync;
mod utils;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
match cli::parse(std::env::args().skip(1)) {
cli::Command::Help => {
cli::print_help();
Ok(())
}
cli::Command::Version => {
cli::print_version();
Ok(())
}
cli::Command::Usage(arg) => {
cli::print_usage_error(&arg);
std::process::exit(cli::EXIT_USAGE);
}
cli::Command::Status { json, wait_secs } => {
std::process::exit(cli::status(json, wait_secs)?)
}
cli::Command::Cleanup { json } => std::process::exit(cli::cleanup(json)?),
cli::Command::Stop { json, quiet } => std::process::exit(cli::stop(json, quiet)?),
cli::Command::Trigger { id } => std::process::exit(cli::trigger(&id)?),
cli::Command::Logs { id } => std::process::exit(cli::logs(&id)?),
cli::Command::Background => cli::run_background(),
cli::Command::Restart {
json,
quiet,
interactive: false,
} => cli::restart(json, quiet),
cli::Command::Restart {
interactive: true, ..
} => {
cli::stop_existing_for_restart(false)?;
run_server().await
}
cli::Command::Install => service::install(),
cli::Command::Uninstall => uninstall(),
cli::Command::Completions(shell) => std::process::exit(cli::completions(shell.as_deref())),
cli::Command::Data(args) => std::process::exit(commands::run(args)),
cli::Command::Machine(args) => std::process::exit(machine::run(&args)),
cli::Command::Foreground => {
cli::ensure_not_running_for_foreground()?;
run_server().await
}
}
}
fn uninstall() -> anyhow::Result<()> {
if let Err(err) = service::uninstall() {
eprintln!("moadim: service uninstall step failed: {err}");
}
match sync::clear_managed_crontab_blocks() {
Ok(0) => println!("moadim: no managed crontab entries to remove"),
Ok(1) => println!("moadim: removed 1 managed crontab entry"),
Ok(n) => println!("moadim: removed {n} managed crontab entries"),
Err(err) => eprintln!("moadim: crontab cleanup failed: {err}"),
}
Ok(())
}
async fn run_server() -> anyhow::Result<()> {
logging::init();
if !routines::tmux_available() {
log::warn!(
"tmux not found on PATH; scheduled routine runs will silently fail to launch their \
agent. Install tmux (e.g. `brew install tmux` or `apt install tmux`)."
);
}
if !routines::agent_command_available("python3") {
log::warn!(
"python3 not found on PATH; the built-in `claude` agent's setup step requires it to \
pre-seed workspace-trust state, so routines using that agent will silently fail to \
launch. Install python3, or use a different agent."
);
}
routines::ensure_default_agents();
routine_storage::migrate_prompt_files();
routine_storage::migrate_prompts_to_subfolder();
routine_storage::migrate_compiled_prompt_filename();
routine_storage::migrate_routine_dirs();
routine_storage::migrate_trigger_logs();
let routines = routine_storage::load_store();
routines::ensure_default_routines(&routines);
routine_storage::repersist_routines(&routines);
if let Err(err) = sync::routines::sync_routines_to_crontab(&routines) {
log::warn!("startup crontab sync failed: {err}");
}
let bind_addr = cli::bind_addr();
match cli::classify_bind(&bind_addr, cli::remote_bind_allowed()) {
cli::BindDecision::Loopback => {}
cli::BindDecision::RemoteAllowed => {
log::warn!(
"moadim is binding to {bind_addr}, which is not loopback-only; the REST/MCP API \
has no authentication, so anyone who can reach this address can create, modify, \
or delete routines, trigger one to run an agent with your credentials, or shut \
the daemon down. Continuing because MOADIM_ALLOW_REMOTE=1 is set (see #253) — \
restrict network access to this port (firewall/VPN/reverse proxy) if you don't \
fully trust that network."
);
}
cli::BindDecision::RemoteRefused => {
anyhow::bail!(
"refusing to bind to {bind_addr}: it is not loopback-only, and the REST/MCP API \
has no authentication — anyone who can reach this address could create, modify, \
or delete routines, trigger one to run an agent with your credentials, or shut \
the daemon down. Set MOADIM_ALLOW_REMOTE=1 to start anyway if you understand and \
accept that risk (see the README's Bind address section and issue #253)."
);
}
}
let listener = tokio::net::TcpListener::bind(bind_addr).await?;
cli::write_pid_file()?;
let result =
routes::http::run_with_listener_until(routines, listener, termination_signal()).await;
cli::clear_pid_file();
result
}
async fn termination_signal() {
#[cfg(unix)]
{
use tokio::signal::unix::{signal, SignalKind};
match signal(SignalKind::terminate()) {
Ok(mut term) => {
tokio::select! {
_ = tokio::signal::ctrl_c() => {}
_ = term.recv() => {}
}
}
Err(_) => {
let _ = tokio::signal::ctrl_c().await;
}
}
}
#[cfg(not(unix))]
{
let _ = tokio::signal::ctrl_c().await;
}
}