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