use std::io;
use std::path::PathBuf;
use recall_hooks::{exit, foreign_memory_slug, is_memory_file, payload};
use crate::project;
pub async fn push() -> anyhow::Result<i32> {
let hook = payload::parse_post_tool_use(io::stdin().lock());
if hook.tool_input.file_path.is_empty() {
return Ok(exit::OK);
}
let triggered = PathBuf::from(&hook.tool_input.file_path);
let here = project::resolve();
if !is_memory_file(&here.memory_dir(), &triggered) {
if let Some(slug) = foreign_memory_slug(&here.memory_root(), &here.memory_dir(), &triggered)
{
eprintln!(
"recall-push: that file is memory for {slug}, not for this project, \
so nothing was pushed"
);
eprintln!(
"recall-push: standing in {}; a git worktree has its own memory directory",
here.root.display()
);
}
return Ok(exit::OK);
}
let cfg = here.config();
here.protect_device_key(&cfg, "recall-push");
let cfg = here.enroll_if_needed(cfg, "recall-push").await;
let ctx = here.hook_context_for(&cfg)?;
let result = match recall_hooks::push(&ctx, &triggered).await {
Err(err) => match err.server_error().filter(|e| e.device_gone()) {
Some(refusal) => match here.after_refusal(&cfg, "recall-push", refusal).await {
Some(cfg) => {
let ctx = here.hook_context_for(&cfg)?;
recall_hooks::push(&ctx, &triggered).await
}
None => Err(err),
},
None => Err(err),
},
ok => ok,
};
match result {
Ok(res) => {
if res.pushed.is_some() || !res.deleted.is_empty() {
eprintln!(
"recall-push: pushed {}, deleted {} for {}",
usize::from(res.pushed.is_some()),
res.deleted.len(),
ctx.project_key()
);
}
Ok(exit::OK)
}
Err(err) => {
eprintln!("recall-push: {err}");
Ok(exit::SERVER)
}
}
}
pub async fn pull() -> anyhow::Result<i32> {
fetch("recall-pull", exit::OK).await
}
pub async fn sync() -> anyhow::Result<i32> {
fetch("recall-sync", exit::SERVER).await
}
async fn fetch(tag: &str, on_failure: i32) -> anyhow::Result<i32> {
let here = project::resolve();
let cfg = here.config();
here.protect_device_key(&cfg, tag);
let cfg = here.enroll_if_needed(cfg, tag).await;
if cfg.device_error.is_none()
&& cfg.device.is_none()
&& cfg.token.is_empty()
&& cfg.authkey.is_some()
{
eprintln!("{tag}: no device key and no RECALL_TOKEN, leaving local memory untouched");
return Ok(on_failure);
}
let ctx = match here.hook_context_for(&cfg) {
Ok(ctx) => ctx,
Err(err) => {
eprintln!("{tag}: {err}, leaving local memory untouched");
return Ok(on_failure);
}
};
let result = match recall_hooks::pull(&ctx).await {
Err(err) => match err.server_error().filter(|e| e.device_gone()) {
Some(refusal) => match here
.after_refusal(&cfg, tag, refusal)
.await
.and_then(|cfg| here.hook_context_for(&cfg).ok())
{
Some(ctx) => recall_hooks::pull(&ctx).await,
None => Err(err),
},
None => Err(err),
},
ok => ok,
};
let code = match &result {
Ok(res) => {
let line = res.describe(ctx.project_key());
eprintln!("{}", line.replacen("recall-pull", tag, 1));
exit::OK
}
Err(err) => {
eprintln!("{tag}: fetch failed ({err}), leaving local memory untouched");
on_failure
}
};
audit_notes(&cfg);
Ok(code)
}
fn audit_notes(cfg: &recall_hooks::ClientConfig) {
use recall_hooks::audit::{Witness, NUDGE_AFTER};
let Some(file) = &cfg.audit_file else {
return;
};
if cfg.url.is_empty() {
return;
}
let saved = match Witness::new(file, &cfg.url).load() {
Ok(saved) => saved,
Err(e) => {
eprintln!(
"recall-pull: WARNING: {e}; it may hold the only record of a rewrite of the \
server's audit log. Run recall doctor."
);
return;
}
};
if let Some(found) = &saved.inconsistent {
eprintln!(
"recall-pull: WARNING: the server's audit log no longer extends a checkpoint this \
machine saved ({}), found {}. Run recall doctor.",
found.detail, found.found_at
);
} else if saved.dropped > 0 {
eprintln!(
"recall-pull: WARNING: {} audit checkpoint(s) were dropped before they were \
checked. Run recall doctor.",
saved.dropped
);
} else if saved.unchecked.len() > NUDGE_AFTER {
eprintln!(
"recall-pull: {} audit checkpoints wait to be checked; recall doctor checks them",
saved.unchecked.len()
);
}
}