mod cli;
mod config;
mod cost;
mod deps;
mod events;
mod executor;
mod gate;
mod help;
mod mailbox;
mod paths;
mod prompt;
mod run;
mod runner;
mod seed;
mod sensor;
mod service;
mod session;
mod store;
mod tick;
mod util;
mod watch;
mod worldhash;
use anyhow::Result;
use paths::Paths;
use std::process::ExitCode;
fn main() -> ExitCode {
restore_sigpipe();
let paths = Paths::resolve();
export_env(&paths);
util::init_format();
util::init_color();
let raw: Vec<String> = std::env::args().skip(1).collect();
if raw.first().map(String::as_str) == Some("run")
&& raw.get(1).map(String::as_str) == Some("--detached-id")
{
return match session::run_detached_worker(&raw[1..]) {
Ok(c) => ExitCode::from(c.clamp(0, 255) as u8),
Err(e) => {
eprintln!("{e}");
ExitCode::from(1)
}
};
}
if matches!(raw.first().map(String::as_str), Some("-h") | Some("--help")) {
help::print(&paths);
return ExitCode::SUCCESS;
}
use clap::Parser;
let cli = match cli::Cli::try_parse() {
Ok(c) => c,
Err(e) => {
let _ = e.print();
return ExitCode::from(if e.use_stderr() { 1 } else { 0 });
}
};
let result: Result<ExitCode> = dispatch(&paths, cli.cmd);
match result {
Ok(code) => code,
Err(e) => {
eprintln!("{e}");
ExitCode::from(1)
}
}
}
fn dispatch(paths: &Paths, cmd: Option<cli::Cmd>) -> Result<ExitCode> {
use cli::{Cmd, CostRecordOp, GoalOp, PlaybookOp, SensorOp, Shell, Verb, WorkerOp};
let Some(cmd) = cmd else {
help::print(paths);
return Ok(ExitCode::SUCCESS);
};
let gated = |f: &dyn Fn() -> Result<ExitCode>| deps::require_deps(paths).and_then(|_| f());
match cmd {
Cmd::Help => {
help::print(paths);
Ok(ExitCode::SUCCESS)
}
Cmd::Version => {
println!("looop {}", env!("CARGO_PKG_VERSION"));
Ok(ExitCode::SUCCESS)
}
Cmd::Up(a) => gated(&|| service::cmd_up(paths, a.json)),
Cmd::Down => gated(&|| service::cmd_down(paths)),
Cmd::Watch(a) => watch::cmd_watch(paths, &a),
Cmd::Cost => cost::cmd_cost(paths),
Cmd::Config(a) => {
match a.shell {
Shell::Zsh => print!("{}", include_str!("completions/looop.zsh")),
Shell::Bash => print!("{}", include_str!("completions/looop.bash")),
}
Ok(ExitCode::SUCCESS)
}
Cmd::Underscore { verb } => match verb {
Verb::Pulse => gated(&|| service::cmd_pulse(paths)),
Verb::State(a) => gated(&|| tick::cmd_state(paths, a.json)),
Verb::Wait(a) => gated(&|| tick::cmd_wait(paths, &a)),
Verb::Asks(a) => gated(&|| mailbox::cmd_asks(paths, a.json)),
Verb::Answer(a) => gated(&|| mailbox::cmd_answer(paths, &a)),
Verb::Goal(a) => gated(&|| match &a.op {
GoalOp::Write { id, body, journal } => {
executor::write_goal(paths, id, body, journal.journal.as_deref())
}
GoalOp::Archive { id, journal } => {
executor::archive_goal(paths, id, journal.journal.as_deref())
}
}),
Verb::Sensor(a) => gated(&|| {
let SensorOp::Write {
name,
script,
journal,
} = &a.op;
executor::write_sensor(paths, name, script, journal.journal.as_deref())
}),
Verb::Playbook(a) => gated(&|| {
let PlaybookOp::Write { body, journal } = &a.op;
executor::write_playbook(paths, body, journal.journal.as_deref())
}),
Verb::Run(a) => gated(&|| executor::cmd_run(paths, &a)),
Verb::Worker(a) => gated(&|| match &a.op {
WorkerOp::Start {
id,
prompt,
journal,
} => executor::start_worker(paths, id, prompt, journal.journal.as_deref()),
WorkerOp::Kill { id } => session::cmd_kill(paths, id),
}),
Verb::Ask(a) => gated(&|| mailbox::cmd_ask(paths, &a)),
Verb::Kill(a) => gated(&|| session::cmd_kill(paths, &a.id)),
Verb::Send(a) => gated(&|| session::cmd_send(paths, &a)),
Verb::Screenshot(a) => gated(&|| session::cmd_screenshot(paths, &a)),
Verb::Claim(a) => gated(&|| gate::cmd_claim(paths, &a)),
Verb::Unclaim(a) => gated(&|| gate::cmd_unclaim(paths, &a)),
Verb::Cost(a) => {
let CostRecordOp::Session { id, runner, usd } = &a.op;
cost::record_cost(paths, "session", id, runner, usd);
Ok(ExitCode::SUCCESS)
}
},
}
}
#[cfg(unix)]
fn restore_sigpipe() {
const SIGPIPE: i32 = 13;
const SIG_DFL: usize = 0;
unsafe extern "C" {
fn signal(signum: i32, handler: usize) -> usize;
}
unsafe {
signal(SIGPIPE, SIG_DFL);
}
}
#[cfg(not(unix))]
fn restore_sigpipe() {}
fn export_env(paths: &Paths) {
let set = |k: &str, v: &std::ffi::OsStr| unsafe { std::env::set_var(k, v) };
set("LOOOP_BIN", paths.bin.as_os_str());
set("LOOOP_DATA_DIR", paths.data_dir.as_os_str());
set("LOOOP_CONFIG", paths.config.as_os_str());
set("LOOOP_CLAIMS_DIR", paths.claims_dir().as_os_str());
set("LOOOP_REPORTS_DIR", paths.reports_dir().as_os_str());
set("LOOOP_COST_LEDGER", paths.cost_ledger().as_os_str());
}
#[cfg(test)]
mod tests {
#[test]
fn zsh_completion_registers_itself() {
let s = include_str!("completions/looop.zsh");
assert!(s.contains("#compdef looop"), "missing #compdef tag");
assert!(s.contains("compdef _looop looop"), "missing compdef call");
assert!(s.contains("'watch:"), "watch missing from zsh command list");
}
#[test]
fn bash_completion_registers_itself() {
let s = include_str!("completions/looop.bash");
assert!(
s.contains("complete -F _looop looop"),
"missing complete -F registration"
);
assert!(
s.contains("up down watch"),
"watch missing from bash subcommand list"
);
}
}