use std::path::PathBuf;
use std::process::ExitCode;
use anyhow::{Result, anyhow};
use clap::{Parser, Subcommand};
use oximemo_core::Vault;
use oximemo_core::memo::MemoId;
mod commands;
mod format;
#[derive(Parser)]
#[command(
name = "oximemo",
version,
about = "Minimal note capture for humans and agents",
long_about = "Reads/writes the oximemo vault. Agent-facing commands default to JSON/NDJSON."
)]
struct Cli {
#[arg(long, global = true, env = "OXIMEMO_VAULT")]
vault: Option<PathBuf>,
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Subcommand)]
enum Cmd {
New {
text: Option<String>,
#[arg(long = "tag", value_name = "TAG")]
tags: Vec<String>,
#[arg(long, value_name = "ID")]
category: Option<String>,
},
List {
#[arg(long, default_value_t = 50)]
limit: u32,
#[arg(long = "tag", value_name = "TAG")]
tag: Vec<String>,
#[arg(long = "category", value_name = "ID")]
category: Vec<String>,
#[arg(long)]
favorites: bool,
#[arg(long, default_value = "table")]
format: String,
},
Get {
id: String,
#[arg(long)]
md: bool,
},
Search {
query: String,
#[arg(long, default_value_t = 20)]
limit: u32,
#[arg(long, default_value = "ndjson")]
format: String,
},
Export {
#[arg(long, value_name = "RFC3339")]
since: Option<String>,
#[arg(long, value_name = "IDS")]
ids: Option<String>,
#[arg(long, value_name = "PATH")]
ids_file: Option<PathBuf>,
#[arg(long)]
ids_stdin: bool,
#[arg(long)]
full: bool,
#[arg(long, default_value = "ndjson")]
format: String,
},
Delete { id: String },
Update {
id: String,
#[arg(long)]
body: Option<String>,
#[arg(long)]
body_stdin: bool,
#[arg(long)]
favorite: bool,
#[arg(long)]
unfavorite: bool,
#[arg(long, value_name = "ID")]
category: Option<String>,
},
Restore { id: String },
Stats,
Purge {
#[arg(long, default_value = "30d")]
older_than: String,
},
Reindex,
Doctor {
#[arg(long)]
fix: bool,
},
Vault {
#[command(subcommand)]
sub: Option<VaultCmd>,
},
Category {
#[command(subcommand)]
sub: CategoryCmd,
},
}
#[derive(Subcommand)]
enum VaultCmd {
Path,
}
#[derive(Subcommand)]
enum CategoryCmd {
List {
#[arg(long, default_value = "table")]
format: String,
},
New {
id: String,
#[arg(long, value_name = "COLOR")]
color: Option<String>,
},
Recolor {
id: String,
color: Option<String>,
#[arg(long)]
none: bool,
},
Rename { old: String, new: String },
Delete { id: String },
}
fn main() -> ExitCode {
init_tracing();
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("oximemo: {e}");
if let Some(src) = e.source() {
eprintln!(" caused by: {src}");
}
ExitCode::FAILURE
}
}
}
fn run() -> Result<()> {
let cli = Cli::parse();
let vault = Vault::open(cli.vault.as_deref())?;
vault.migrate()?;
match cli.cmd {
Cmd::New {
text,
tags,
category,
} => commands::cmd_new(&vault, text, tags, category),
Cmd::List {
limit,
tag,
category,
favorites,
format,
} => {
let fmt = format::Format::from_arg(&format)
.ok_or_else(|| anyhow!("unknown --format: {format}"))?;
commands::cmd_list(&vault, limit, tag, category, favorites, fmt)
}
Cmd::Get { id, md } => commands::cmd_get(&vault, parse_id(&id)?, md),
Cmd::Search {
query,
limit,
format,
} => {
let fmt = format::Format::from_arg(&format)
.ok_or_else(|| anyhow!("unknown --format: {format}"))?;
commands::cmd_search(&vault, query, limit, fmt)
}
Cmd::Export {
since,
ids,
ids_file,
ids_stdin,
full,
format,
} => {
let fmt = format::Format::from_arg(&format)
.ok_or_else(|| anyhow!("unknown --format: {format}"))?;
commands::cmd_export(&vault, since, ids, ids_file, ids_stdin, full, fmt)
}
Cmd::Delete { id } => commands::cmd_delete(&vault, parse_id(&id)?),
Cmd::Update {
id,
body,
body_stdin,
favorite,
unfavorite,
category,
} => {
let fav = if unfavorite {
Some(false)
} else if favorite {
Some(true)
} else {
None
};
commands::cmd_update(&vault, parse_id(&id)?, body, body_stdin, fav, category)
}
Cmd::Restore { id } => commands::cmd_restore(&vault, parse_id(&id)?),
Cmd::Stats => commands::cmd_stats(&vault),
Cmd::Purge { older_than } => {
let d = commands::parse_duration(&older_than)?;
commands::cmd_purge(&vault, d)
}
Cmd::Reindex => commands::cmd_reindex(&vault),
Cmd::Doctor { fix } => commands::cmd_doctor(&vault, fix),
Cmd::Vault { sub } => match sub {
Some(VaultCmd::Path) | None => commands::cmd_vault_path(&vault),
},
Cmd::Category { sub } => match sub {
CategoryCmd::List { format } => {
let fmt = format::Format::from_arg(&format)
.ok_or_else(|| anyhow!("unknown --format: {format}"))?;
commands::cmd_category_list(&vault, fmt)
}
CategoryCmd::New { id, color } => commands::cmd_category_new(&vault, id, color),
CategoryCmd::Recolor { id, color, none } => {
commands::cmd_category_recolor(&vault, id, color, none)
}
CategoryCmd::Rename { old, new } => commands::cmd_category_rename(&vault, old, new),
CategoryCmd::Delete { id } => commands::cmd_category_delete(&vault, id),
},
}
}
fn parse_id(s: &str) -> Result<MemoId> {
MemoId::parse(s).map_err(|e| anyhow!("invalid id `{s}`: {e}"))
}
fn init_tracing() {
use tracing_subscriber::EnvFilter;
let _ = tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.with_writer(std::io::stderr)
.try_init();
}