use anyhow::Result;
use clap::Subcommand;
use crate::cache::{list_cached, remove_cached};
#[cfg(not(target_os = "emscripten"))]
use crate::{
artifact::{ArtifactRef, ArtifactType},
harness::HarnessBundle,
sync::{SyncObserver, SyncOutcome, sync_bundle},
};
#[derive(Subcommand, Debug)]
pub enum CacheOp {
Ls,
Rm {
id: String,
},
#[cfg(not(target_os = "emscripten"))]
Sync {
#[arg(value_enum)]
types: Vec<crate::artifact::ArtifactType>,
},
}
pub fn run(op: CacheOp) -> Result<()> {
match op {
CacheOp::Ls => run_ls(),
CacheOp::Rm { id } => run_rm(&id),
#[cfg(not(target_os = "emscripten"))]
CacheOp::Sync { types } => run_sync(types),
}
}
fn run_ls() -> Result<()> {
let entries = list_cached()?;
if entries.is_empty() {
eprintln!("No cached documents. Run `path import <source>` to create one.");
return Ok(());
}
for e in entries {
println!("{}\t{}\t{}", e.id, e.bytes, e.path.display());
}
Ok(())
}
fn run_rm(id: &str) -> Result<()> {
remove_cached(id)?;
#[cfg(not(target_os = "emscripten"))]
if let Err(e) = crate::sync::evict_cache_id(id) {
eprintln!("warning: sync manifest not updated: {e}");
}
eprintln!("Removed {id}");
Ok(())
}
#[cfg(not(target_os = "emscripten"))]
fn run_sync(types: Vec<ArtifactType>) -> Result<()> {
let explicit = !types.is_empty();
let types = resolve_types(&types);
let bundle = HarnessBundle::from_environment();
let outcomes = sync_bundle(&bundle, &types, &mut Progress::new())?;
eprint!("{}", render_summary(&outcomes, explicit));
Ok(())
}
#[cfg(not(target_os = "emscripten"))]
fn resolve_types(args: &[ArtifactType]) -> Vec<ArtifactType> {
if args.is_empty() {
return ArtifactType::ALL.to_vec();
}
let mut out: Vec<ArtifactType> = Vec::with_capacity(args.len());
for &t in args {
if !out.contains(&t) {
out.push(t);
}
}
out
}
#[cfg(not(target_os = "emscripten"))]
struct Progress {
label: String,
total: usize,
done: usize,
tty: bool,
}
#[cfg(not(target_os = "emscripten"))]
impl Progress {
fn new() -> Self {
use std::io::IsTerminal;
Self {
label: String::new(),
total: 0,
done: 0,
tty: std::io::stderr().is_terminal(),
}
}
fn line(&self) -> String {
format!("{} {}/{}", self.label, self.done, self.total)
}
fn draw(&self) {
if self.total > 0 && self.tty {
eprint!("\r{}", self.line());
}
}
fn interrupt(&self) {
if self.total > 0 && self.tty {
eprint!("\r\x1b[2K");
}
}
}
#[cfg(not(target_os = "emscripten"))]
impl SyncObserver for Progress {
fn begin(&mut self, artifact_type: ArtifactType, pending: usize) {
self.label = artifact_type.padded_name();
self.total = pending;
self.done = 0;
self.draw();
}
fn tick(&mut self) {
if self.total == 0 {
return;
}
self.done += 1;
if self.tty {
self.draw();
} else if self.done.is_multiple_of(25) {
eprintln!("{}", self.line());
}
}
fn failed(&mut self, artifact: &ArtifactRef, error: &anyhow::Error) {
self.interrupt();
eprintln!(
"warning: sync {}: {}: {error}",
artifact.artifact_type.name(),
artifact.id
);
}
fn end(&mut self) {
self.interrupt();
}
}
#[cfg(not(target_os = "emscripten"))]
fn render_summary(outcomes: &[(ArtifactType, SyncOutcome)], explicit: bool) -> String {
let mut s = String::new();
for (artifact_type, o) in outcomes {
if o.total() == 0 && !explicit {
continue;
}
s.push_str(&format!(
"{} {} new, {} updated, {} unchanged",
artifact_type.padded_name(),
o.new,
o.updated,
o.unchanged
));
if o.failed > 0 {
s.push_str(&format!(", {} failed", o.failed));
}
s.push('\n');
}
if s.is_empty() {
s.push_str("nothing to sync\n");
}
s
}
#[cfg(all(test, not(target_os = "emscripten")))]
mod tests {
use super::*;
#[test]
fn progress_line_counts_only_pending_work() {
let mut progress = Progress {
label: ArtifactType::Claude.padded_name(),
total: 3,
done: 0,
tty: false,
};
progress.tick();
progress.tick();
assert_eq!(progress.line(), "claude 2/3");
}
#[test]
fn resolve_types_defaults_to_all_and_dedups() {
assert_eq!(resolve_types(&[]), ArtifactType::ALL.to_vec());
assert_eq!(
resolve_types(&[
ArtifactType::Codex,
ArtifactType::Claude,
ArtifactType::Codex
]),
vec![ArtifactType::Codex, ArtifactType::Claude]
);
}
#[test]
fn render_summary_hides_empty_types_unless_explicit() {
let outcomes = vec![
(
ArtifactType::Claude,
SyncOutcome {
new: 2,
updated: 1,
unchanged: 3,
failed: 0,
},
),
(ArtifactType::Cursor, SyncOutcome::default()),
];
let default_run = render_summary(&outcomes, false);
assert!(default_run.contains("claude"));
assert!(!default_run.contains("cursor"));
let explicit_run = render_summary(&outcomes, true);
assert!(explicit_run.contains("cursor"));
}
#[test]
fn render_summary_shows_failures_and_empty_case() {
let outcomes = vec![(
ArtifactType::Codex,
SyncOutcome {
new: 0,
updated: 0,
unchanged: 1,
failed: 2,
},
)];
let s = render_summary(&outcomes, false);
assert!(s.contains("2 failed"));
assert_eq!(render_summary(&[], false), "nothing to sync\n");
}
}