mod cli;
mod commands;
mod error;
mod model;
mod output;
mod store;
use std::path::PathBuf;
use clap::Parser;
use uuid::Uuid;
use cli::{Cli, Command, ProjectCommand};
use store::Store;
fn default_dir() -> PathBuf {
std::env::var_os("HOME")
.map(|h| PathBuf::from(h).join(".claimd"))
.unwrap_or_else(|| PathBuf::from(".claimd"))
}
fn main() {
let cli = Cli::parse();
let base_dir = cli.dir.clone().unwrap_or_else(default_dir);
let dir = match &cli.project {
Some(name) => base_dir.join("projects").join(name),
None => base_dir,
};
let store = Store::new(dir);
let json = cli.json;
let result = run(cli, &store);
if let Err(e) = result {
output::print_error(&e, json);
std::process::exit(e.exit_code());
}
}
fn run(cli: Cli, store: &Store) -> error::Result<()> {
let json = cli.json;
let project_name = cli.project.clone();
let base_dir = cli.dir.clone().unwrap_or_else(default_dir);
let needs_project = !matches!(cli.command, Command::Project { .. } | Command::Show { .. });
if needs_project && project_name.is_none() {
return Err(error::Error::ProjectRequired);
}
let project_meta = store.read_project_meta().unwrap_or_default();
let ctx = output::OutputContext::from_meta(&project_meta, project_name.clone());
match cli.command {
Command::Init => {
commands::init(store)?;
output::print_message("Store initialized.", json);
}
Command::Add { title, desc, priority, tags, link, source, author, depends_on } => {
let item = commands::add(store, &title, desc.as_deref(), priority, &tags, link.as_deref(), source.as_deref(), author.as_deref(), &depends_on)?;
output::print_item(&item, &ctx, json);
}
Command::List { status, tag, all } => {
let items = commands::list(store, status.as_ref(), tag.as_deref(), all)?;
let refs: Vec<&model::TaskItem> = items.iter().collect();
output::print_items(&refs, &ctx, json);
}
Command::Show { id } if project_name.is_none() => {
let projects_dir = base_dir.join("projects");
let mut hits: Vec<(String, model::TaskItem, model::ProjectMeta)> = Vec::new();
if projects_dir.is_dir() {
if let Ok(entries) = std::fs::read_dir(&projects_dir) {
for entry in entries.flatten() {
if entry.path().is_dir() {
let name = entry.file_name().to_string_lossy().to_string();
let ps = Store::new(entry.path());
if let Ok(item) = commands::show(&ps, &id) {
let meta = ps.read_project_meta().unwrap_or_default();
hits.push((name, item, meta));
}
}
}
}
}
match hits.len() {
0 => return Err(error::Error::NotFound { id_prefix: id }),
1 => {
let (proj_name, item, meta) = hits.remove(0);
let ctx = output::OutputContext::from_meta(&meta, Some(proj_name));
output::print_item_detail(&item, &ctx, json);
}
_ => {
let matches: Vec<Uuid> = hits.iter().map(|(_, item, _)| item.id).collect();
return Err(error::Error::AmbiguousPrefix { id_prefix: id, matches });
}
}
}
Command::Show { id } => {
let item = commands::show(store, &id)?;
output::print_item_detail(&item, &ctx, json);
}
Command::Claim { id, agent } => {
let item = commands::claim(store, &id, agent.as_deref())?;
output::print_item(&item, &ctx, json);
}
Command::ClaimMulti { ids, agent } => {
let items = commands::claim_multi(store, &ids, agent.as_deref())?;
let refs: Vec<&model::TaskItem> = items.iter().collect();
output::print_items(&refs, &ctx, json);
}
Command::PrOpen { id, pr_url } => {
let item = commands::pr_open(store, &id, &pr_url)?;
output::print_item(&item, &ctx, json);
}
Command::PrChangesRequested { id } => {
let item = commands::pr_changes_requested(store, &id)?;
output::print_item(&item, &ctx, json);
}
Command::Done { id } => {
let item = commands::done(store, &id)?;
output::print_item(&item, &ctx, json);
}
Command::Incomplete { id, reason } => {
let item = commands::incomplete(store, &id, reason.as_deref())?;
output::print_item(&item, &ctx, json);
}
Command::Unclaim { id } => {
let item = commands::unclaim(store, &id)?;
output::print_item(&item, &ctx, json);
}
Command::Edit { id, title, desc, priority, tags, link, source, author, add_deps, remove_deps } => {
let item = commands::edit(store, &id, title.as_deref(), desc.as_deref(), priority, tags.as_deref(), link.as_deref(), source.as_deref(), author.as_deref(), &add_deps, &remove_deps)?;
output::print_item(&item, &ctx, json);
}
Command::Reorder { id, position } => {
let item = commands::reorder(store, &id, position)?;
output::print_item(&item, &ctx, json);
}
Command::Remove { id } => {
let item = commands::remove(store, &id)?;
output::print_message(&format!("Removed: {} ({})", item.title, item.short_id()), json);
}
Command::Project { command } => {
let projects_dir = base_dir.join("projects");
match command {
ProjectCommand::List => {
let mut names = Vec::new();
if projects_dir.is_dir() {
if let Ok(entries) = std::fs::read_dir(&projects_dir) {
for entry in entries.flatten() {
if entry.path().is_dir() {
if let Some(name) = entry.file_name().to_str() {
names.push(name.to_string());
}
}
}
}
}
names.sort();
if json {
let projects: Vec<serde_json::Value> = names.iter().map(|name| {
let project_store = Store::new(projects_dir.join(name));
let active = project_store.read_project_meta().map(|m| m.active).unwrap_or(true);
serde_json::json!({ "name": name, "active": active })
}).collect();
println!("{}", serde_json::to_string_pretty(&serde_json::json!({ "projects": projects })).unwrap());
} else if names.is_empty() {
println!("No projects found.");
} else {
for name in &names {
let project_store = Store::new(projects_dir.join(name));
let active = project_store.read_project_meta().map(|m| m.active).unwrap_or(true);
let state = if active { "active" } else { "INACTIVE" };
println!("{:<20} {:<8} {}", name, state, projects_dir.join(name).display());
}
}
}
ProjectCommand::Status { name } => {
let project_store = Store::new(projects_dir.join(&name));
let meta = commands::project_get_meta(&project_store)?;
if json {
println!("{}", serde_json::json!({ "name": name, "active": meta.active }));
} else {
let state = if meta.active { "active" } else { "INACTIVE" };
println!("{}: {}", name, state);
}
}
ProjectCommand::Activate { name } => {
let project_store = Store::new(projects_dir.join(&name));
commands::project_set_active(&project_store, true)?;
if json {
println!("{}", serde_json::json!({ "name": name, "active": true }));
} else {
println!("{}: active", name);
}
}
ProjectCommand::Deactivate { name } => {
let project_store = Store::new(projects_dir.join(&name));
commands::project_set_active(&project_store, false)?;
if json {
println!("{}", serde_json::json!({ "name": name, "active": false }));
} else {
println!("{}: INACTIVE", name);
}
}
}
}
}
Ok(())
}