mod backfill;
mod connect;
mod doctor;
mod hook;
mod init;
mod project;
mod promote;
mod serve;
mod status;
mod ui;
use std::path::PathBuf;
use clap::{CommandFactory, Parser, Subcommand};
use recall_hooks::exit;
const VERSION: &str = env!("CARGO_PKG_VERSION");
const COMMIT: Option<&str> = option_env!("RECALL_GIT_COMMIT");
#[derive(Parser)]
#[command(
name = "recall",
about = "Sync Claude Code's auto memory across machines and cloud sessions",
// clap's own `--version` is replaced rather than merely enabled. Its
// default prints `recall <version>` and nothing else, while `recall
// version` prints the commit too — two ways of asking the same question
// giving different answers is the failure this project keeps finding in
// itself. `version_line` below is the single source both go through.
disable_version_flag = true,
// `recall` alone still prints help and exits 2, which it did before this
// flag existed. Without this, an optional subcommand would make a bare
// `recall` a silent success.
arg_required_else_help = true
)]
struct Cli {
#[arg(short = 'V', long = "version", action = clap::ArgAction::SetTrue)]
version: bool,
#[command(subcommand)]
command: Option<Cmd>,
}
fn version_line() -> String {
format!("recall {VERSION} ({})", COMMIT.unwrap_or("unknown"))
}
#[derive(Subcommand)]
enum Cmd {
Init {
#[arg(long)]
path: Option<PathBuf>,
},
Backfill,
Promote {
file: PathBuf,
#[arg(long, value_enum, default_value_t = promote::Target::Global)]
to: promote::Target,
},
Connect {
url: Option<String>,
#[arg(long)]
name: Option<String>,
#[arg(long, short)]
yes: bool,
},
Disconnect {
url: Option<String>,
},
Status {
#[arg(long)]
json: bool,
},
Doctor {
#[arg(long)]
json: bool,
},
Serve,
Push,
Pull,
Version,
}
fn main() {
let cli = Cli::parse();
if cli.version {
println!("{}", version_line());
std::process::exit(exit::OK);
}
let Some(command) = cli.command else {
Cli::command().print_help().ok();
std::process::exit(exit::CONFIG);
};
let result = match command {
Cmd::Version => {
println!("{}", version_line());
Ok(exit::OK)
}
Cmd::Init { path } => init::run(path.as_deref()),
Cmd::Backfill => block_on_current(backfill::run()),
Cmd::Serve => block_on_multi(serve::run()),
Cmd::Promote { file, to } => block_on_current(promote::run(&file, to)),
Cmd::Connect { url, name, yes } => {
block_on_current(connect::connect(connect::Args { url, name, yes }))
}
Cmd::Disconnect { url } => connect::disconnect(url.as_deref()),
Cmd::Status { json } => block_on_current(status::run(json)),
Cmd::Doctor { json } => block_on_current(doctor::run(json)),
Cmd::Push => block_on_current(hook::push()),
Cmd::Pull => block_on_current(hook::pull()),
};
match result {
Ok(code) => std::process::exit(code),
Err(err) => {
eprintln!("recall: {err:#}");
std::process::exit(exit::CONFIG)
}
}
}
fn block_on_current<F: std::future::Future<Output = anyhow::Result<i32>>>(
fut: F,
) -> anyhow::Result<i32> {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?
.block_on(fut)
}
fn block_on_multi<F: std::future::Future<Output = anyhow::Result<i32>>>(
fut: F,
) -> anyhow::Result<i32> {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?
.block_on(fut)
}