Skip to main content

agentstatedeveloper_cli/commands/
sync.rs

1//! `asd sync [--dir <path>] [--prune]` — mirror live ASG state into the
2//! `.asd/v1/` on-disk sidecar. The sidecar travels with `git commit`,
3//! letting a fresh `git clone` hydrate an ASD repo without a network
4//! registry.
5//!
6//! `--prune` removes orphaned sidecar files for symbols that no longer
7//! exist in the index (renamed or deleted). The pre-commit hook runs
8//! `asd sync --prune` automatically.
9
10use std::path::{Path, PathBuf};
11
12use anyhow::Result;
13use clap::Args;
14use serde_json::json;
15
16use agentstatedeveloper_core::{Engine, ledger_counts, prune_sidecar, sync_to_dir};
17
18use crate::config::Config;
19
20#[derive(Debug, Args)]
21pub struct SyncArgs {
22    /// Project root to sync into. `.asd/v1/` is appended internally.
23    /// Defaults to the directory holding the db (the project the store
24    /// belongs to), not the current working directory.
25    #[arg(long)]
26    pub dir: Option<PathBuf>,
27
28    /// Remove orphaned `.asd/v1/` files for symbols that no longer exist
29    /// in the index. Safe to use on every commit via the pre-commit hook.
30    #[arg(long, default_value_t = false)]
31    pub prune: bool,
32}
33
34pub fn run(cfg: &Config, args: SyncArgs) -> Result<()> {
35    let engine = Engine::open_sqlite(&cfg.db_path)?;
36    let dir = resolve_dir(args.dir, &cfg.db_path)?;
37
38    let mut summary = sync_to_dir(&engine.repo, &engine.ref_name, &dir)?;
39
40    if args.prune {
41        summary.pruned = prune_sidecar(&engine.repo, &engine.ref_name, &dir)?;
42    }
43
44    // Sync exports the store, so an entry the store lost is silently absent
45    // from the sidecar too. Count what the ledger cache acknowledged against
46    // what landed, and say so when they differ.
47    let ledger = ledger_counts(
48        &engine.repo,
49        &engine.ref_name,
50        engine.fts.as_ref(),
51        Some(&dir),
52    )?;
53    let ledger_warning = ledger.warning();
54    if let Some(w) = &ledger_warning {
55        eprintln!("asd sync: warning: {w}");
56    }
57
58    let out = json!({
59        "db": cfg.db_path.display().to_string(),
60        "dir": dir.join(".asd/v1").display().to_string(),
61        "effects_written": summary.effects_written,
62        "ledger_entries_written": summary.ledger_entries_written,
63        "symbols_written": summary.symbols_written,
64        "schema_version": summary.schema_version,
65        "pruned": summary.pruned,
66        "moved_entries_removed": summary.moved_entries_removed,
67        "ledger": ledger,
68        "ledger_warning": ledger_warning,
69        "note": "current-state only; ASG commit history is not carried in the sidecar",
70    });
71    println!("{}", serde_json::to_string_pretty(&out)?);
72    Ok(())
73}
74
75/// The sidecar belongs beside the store it mirrors. Defaulting to the current
76/// directory paired a walk-up- or registry-resolved store with wherever `asd`
77/// happened to run, exporting one project's state into another (or into a
78/// subdirectory). The MCP `sync` tool already defaults to the db's directory.
79fn resolve_dir(explicit: Option<PathBuf>, db_path: &Path) -> Result<PathBuf> {
80    if let Some(p) = explicit {
81        return Ok(p);
82    }
83    let cwd = std::env::current_dir()?;
84    Ok(match db_path.parent() {
85        Some(p) if !p.as_os_str().is_empty() => cwd.join(p),
86        _ => cwd,
87    })
88}