use std::io::Read;
use std::path::PathBuf;
use std::process::ExitCode;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::time::{Duration, Instant};
use marver::Scanner;
use marver::daemon::{self, Config, Daemon};
use marver::hook::{self, Delivery, Payload};
use marver::notify::SystemNotifier;
use marver::tmux::Tmux;
fn usage() -> ExitCode {
eprintln!(
"usage:
marver [options] open the interface, starting a daemon if needed
marver status [options] report whether a daemon is running
marver daemon [options] run the scheduler and hook receiver in the foreground
marver scan [root] list git repos under a root
marver hook --task <id> --socket <path>
forward a Claude Code hook to the daemon
options:
--data-dir <path> where the database, socket, and log live
--scan-root <path> directory scanned for repos
--cap <n> how many agents may run at once"
);
ExitCode::FAILURE
}
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
match args.first().map(String::as_str) {
Some("hook") => hook_command(&args[1..]),
Some("daemon") => daemon_command(&args[1..]),
Some("status") => status_command(&args[1..]),
Some("scan") => scan_command(args.get(1).map(PathBuf::from)),
Some("-h" | "--help") => usage(),
None => tui_command(&args),
Some(arg) if arg.starts_with('-') => tui_command(&args),
Some(other) => {
eprintln!("marver: unknown command {other:?}");
usage()
}
}
}
fn config_from(args: &[String]) -> Config {
let data_dir = flag(args, "--data-dir")
.map(PathBuf::from)
.unwrap_or_else(Config::default_data_dir);
let scan_root = flag(args, "--scan-root")
.map(PathBuf::from)
.unwrap_or_else(Config::default_scan_root);
let mut config = Config::new(&data_dir, scan_root);
if let Some(cap) = flag(args, "--cap").and_then(|v| v.parse().ok()) {
config.cap = cap;
}
config
}
fn tui_command(args: &[String]) -> ExitCode {
let config = config_from(args);
match daemon::ensure_running(&config) {
Ok(_) => {}
Err(err) => {
eprintln!("marver: {err}");
return ExitCode::FAILURE;
}
}
let store = match marver::Store::open(&config.db) {
Ok(store) => store,
Err(err) => {
eprintln!("marver: could not open {}: {err}", config.db.display());
return ExitCode::FAILURE;
}
};
if let Err(err) = marver::tui::run(store, config) {
eprintln!("marver: {err}");
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
}
fn flag(args: &[String], name: &str) -> Option<String> {
args.iter()
.position(|a| a == name)
.and_then(|i| args.get(i + 1))
.cloned()
}
fn daemon_command(args: &[String]) -> ExitCode {
let config = config_from(args);
let mut daemon = match Daemon::new(config.clone(), Tmux::new(), SystemNotifier) {
Ok(daemon) => daemon,
Err(err) => {
eprintln!("marverd: {err}");
return ExitCode::FAILURE;
}
};
println!("marverd: database {}", config.db.display());
println!("marverd: socket {}", config.socket.display());
println!("marverd: scanning {}", config.scan_root.display());
println!("marverd: cap {}", config.cap);
if let Err(err) = daemon.run(Arc::new(AtomicBool::new(false))) {
eprintln!("marverd: {err}");
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
}
fn hook_command(args: &[String]) -> ExitCode {
let task_id: Option<i64> = flag(args, "--task").and_then(|v| v.parse().ok());
let socket = flag(args, "--socket").map(PathBuf::from);
let (Some(task_id), Some(socket)) = (task_id, socket) else {
eprintln!("marver hook: --task and --socket are both required");
return ExitCode::SUCCESS;
};
let mut body = Vec::new();
if let Err(err) = std::io::stdin().read_to_end(&mut body) {
eprintln!("marver hook: could not read the payload: {err}");
return ExitCode::SUCCESS;
}
match Payload::parse(&body) {
Ok(payload) => {
let delivery = Delivery { task_id, payload };
if let Err(err) = hook::send(&socket, &delivery) {
eprintln!("marver hook: could not reach the daemon: {err}");
}
}
Err(err) => eprintln!("marver hook: {err}"),
}
ExitCode::SUCCESS
}
fn status_command(args: &[String]) -> ExitCode {
let config = config_from(args);
let running = daemon::is_running(&config);
println!(
"daemon {}",
if running { "running" } else { "not running" }
);
println!("socket {}", config.socket.display());
println!("database {}", config.db.display());
println!("log {}", config.log.display());
if !config.db.exists() {
println!("tasks no database yet");
} else {
match marver::Store::open(&config.db) {
Ok(store) => {
let counts: Vec<String> = marver::TaskState::ALL
.iter()
.filter_map(|&state| match store.list_tasks_in_state(state) {
Ok(tasks) if !tasks.is_empty() => Some(format!("{} {state}", tasks.len())),
_ => None,
})
.collect();
println!(
"tasks {}",
if counts.is_empty() {
"none".to_string()
} else {
counts.join(", ")
}
);
}
Err(err) => println!("tasks unreadable: {err}"),
}
}
if running {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
}
}
fn scan_command(root: Option<PathBuf>) -> ExitCode {
let root = root.unwrap_or_else(Config::default_scan_root);
let started = Instant::now();
let scan = match Scanner::new(&root).walk() {
Ok(scan) => scan,
Err(err) => {
eprintln!("marver: {err}");
return ExitCode::FAILURE;
}
};
let elapsed: Duration = started.elapsed();
println!("scanning {}", root.display());
for repo in &scan.repos {
println!(" {:<24} {}", repo.name, repo.path.display());
}
if !scan.unreadable.is_empty() {
println!("\n{} unreadable:", scan.unreadable.len());
for path in &scan.unreadable {
println!(" {}", path.display());
}
}
println!("\n{} repos in {:.0?}", scan.repos.len(), elapsed);
ExitCode::SUCCESS
}