mod agents_md;
mod cli;
mod commands;
mod context;
mod event_helper;
mod output;
mod router;
use clap::Parser;
use cli::{Cli, Command};
use libgrite_core::GriteError;
fn main() {
let cli = Cli::parse();
let result = run_command(&cli);
if let Err(e) = result {
output::output_error(&cli, &e);
std::process::exit(e.exit_code());
}
}
fn run_command(cli: &Cli) -> Result<(), GriteError> {
if router::should_route_through_daemon(&cli.command) {
if let Some(ipc_cmd) = router::cli_to_ipc_command(&cli.command) {
if let Some(result) = try_route_through_daemon(cli, ipc_cmd)? {
return result;
}
}
}
match &cli.command {
Command::Init { no_agents_md } => commands::init::run(cli, *no_agents_md),
Command::Actor { cmd } => commands::actor::run(cli, cmd.clone()),
Command::Issue { cmd } => commands::issue::run(cli, cmd.clone()),
Command::Db { cmd } => commands::db::run(cli, cmd.clone()),
Command::Export { format, since } => {
commands::export::run(cli, format.clone(), since.clone())
}
Command::Rebuild { from_snapshot } => commands::rebuild::run(cli, *from_snapshot),
Command::Sync { remote, pull, push } => {
commands::sync::run(cli, remote.clone(), *pull, *push)
}
Command::Snapshot { cmd } => commands::snapshot::run(cli, cmd.clone()),
Command::Daemon { cmd } => commands::daemon::run(cli, cmd.clone()),
Command::Lock { cmd } => commands::lock::run(cli, cmd.clone()),
Command::Doctor { fix } => commands::doctor::run(cli, *fix),
Command::Context { cmd } => commands::context::run(cli, cmd.clone()),
Command::InstallSkill { global, force } => {
commands::install_skill::run(cli, *global, *force)
}
}
}
fn try_route_through_daemon(
cli: &Cli,
ipc_cmd: libgrite_ipc::IpcCommand,
) -> Result<Option<Result<(), GriteError>>, GriteError> {
let ctx = match context::GriteContext::resolve(cli) {
Ok(ctx) => ctx,
Err(_) => return Ok(None), };
match router::route_command(&ctx, cli, ipc_cmd)? {
router::RouteResult::Local => Ok(None),
router::RouteResult::DaemonResponse(response) => {
Ok(Some(handle_daemon_response(cli, response)))
}
router::RouteResult::Blocked { pid, expires_in_ms } => {
Err(GriteError::DbBusy(format!(
"Data directory locked by daemon (PID {}, expires in {}s). Use --no-daemon to wait or try later.",
pid,
expires_in_ms / 1000
)))
}
}
}
fn handle_daemon_response(
cli: &Cli,
response: libgrite_ipc::IpcResponse,
) -> Result<(), GriteError> {
if response.ok {
if let Some(data) = response.data {
if cli.json {
if !cli.quiet {
println!("{}", data);
}
} else {
output_daemon_data(cli, &data)?;
}
}
Ok(())
} else {
let (code, message) = match response.error {
Some(err) => (err.code, err.message),
None => ("unknown".to_string(), "Unknown error".to_string()),
};
match code.as_str() {
"not_found" => Err(GriteError::NotFound(message)),
"invalid_input" | "invalid_args" => Err(GriteError::InvalidArgs(message)),
"conflict" => Err(GriteError::Conflict(message)),
"db_busy" => Err(GriteError::DbBusy(message)),
"ipc_error" => Err(GriteError::Ipc(message)),
_ => Err(GriteError::Internal(message)),
}
}
}
fn output_daemon_data(cli: &Cli, data: &str) -> Result<(), GriteError> {
if cli.quiet {
return Ok(());
}
if let Ok(json) = serde_json::from_str::<serde_json::Value>(data) {
if let Some(issues) = json.get("issues") {
if let Some(arr) = issues.as_array() {
let rows: Vec<output::IssueRow> = arr
.iter()
.map(|issue| {
let id = issue
.get("issue_id")
.and_then(|v| v.as_str())
.unwrap_or("?");
let state = issue.get("state").and_then(|v| v.as_str()).unwrap_or("?");
let title = issue.get("title").and_then(|v| v.as_str()).unwrap_or("?");
let created_ts = issue
.get("created_ts")
.and_then(|v| v.as_u64())
.unwrap_or(0);
output::IssueRow {
id: id.to_string(),
state: state.to_string(),
title: title.to_string(),
created_ts,
}
})
.collect();
println!("{}", output::format_issue_table(&rows));
}
} else if let Some(action) = json.get("action").and_then(|v| v.as_str()) {
let issue_id = json.get("issue_id").and_then(|v| v.as_str()).unwrap_or("?");
let action_str = match action {
libgrite_ipc::issue_action::CREATED => "Created",
libgrite_ipc::issue_action::CLOSED => "Closed",
libgrite_ipc::issue_action::REOPENED => "Reopened",
_ => action,
};
println!("{} issue {}", action_str, issue_id);
} else if json.get("issue_id").is_some() && json.get("title").is_some() {
let id = json.get("issue_id").and_then(|v| v.as_str()).unwrap_or("?");
let title = json.get("title").and_then(|v| v.as_str()).unwrap_or("?");
let state = json.get("state").and_then(|v| v.as_str()).unwrap_or("?");
let body = json.get("body").and_then(|v| v.as_str()).unwrap_or("");
println!("{} [{}] {}", &id[..8.min(id.len())], state, title);
if !body.is_empty() {
println!("\n{}", body);
}
} else if json.get("path").is_some() {
println!("{}", serde_json::to_string_pretty(&json)?);
} else if json.get("event_count").is_some() {
let events = json
.get("event_count")
.and_then(|v| v.as_u64())
.unwrap_or(0);
let issues = json
.get("issue_count")
.and_then(|v| v.as_u64())
.unwrap_or(0);
println!("Rebuilt: {} events, {} issues", events, issues);
} else {
println!("{}", serde_json::to_string_pretty(&json)?);
}
} else {
println!("{}", data);
}
Ok(())
}