use crate::config::{PollingConfig, StoreConfig, StoreInfo};
use crate::git_store::GitStore;
use crate::manifest::Manifest;
use crate::observability::CliOutput;
use crate::types::DocType;
use anyhow::Result;
use std::path::Path;
pub fn run(path: &Path, scan: bool, output: &dyn CliOutput) -> Result<()> {
let path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
let store_dir = path.join(".agent-trace");
if store_dir.join("config.toml").exists() {
output.line(&format!("Store already initialised at {}", path.display()))?;
output.line(&format!(
" Config: {}",
store_dir.join("config.toml").display()
))?;
output.line(&format!(
" Manifest: {}",
store_dir.join("manifest.toml").display()
))?;
return Ok(());
}
#[cfg(unix)]
{
use std::os::unix::fs::DirBuilderExt;
std::fs::DirBuilder::new()
.recursive(true)
.mode(0o700)
.create(&store_dir)?;
}
#[cfg(not(unix))]
{
std::fs::create_dir_all(&store_dir)?;
}
std::fs::create_dir_all(store_dir.join("locks"))?;
let context_updates = store_dir.join("context_updates.jsonl");
if !context_updates.exists() {
std::fs::write(&context_updates, "")?;
}
let summary_events = store_dir.join("summary_events.jsonl");
if !summary_events.exists() {
std::fs::write(&summary_events, "")?;
}
let cmd_history = store_dir.join("command_history.txt");
if !cmd_history.exists() {
std::fs::write(&cmd_history, "")?;
}
let store_name = path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "agent-trace-store".to_string());
let store_info = StoreInfo::new(store_name);
let store_config = StoreConfig {
store: store_info.clone(),
llm: None,
synthesis: None,
polling: PollingConfig::default(),
};
store_config.save(&path)?;
let git = GitStore::init(&path)?;
let mut manifest = Manifest::create_empty(store_info, &path)?;
let gitignore = path.join(".gitignore");
if !gitignore.exists() {
std::fs::write(&gitignore, DEFAULT_GITIGNORE)?;
}
let agent_trace_content = crate::agent_trace_md::generate(&path, &manifest);
std::fs::write(path.join("AGENT-TRACE.md"), &agent_trace_content)?;
{
let agent_trace_info = crate::git_store::CommitInfo {
action: crate::types::Action::Create,
files: vec![
(
std::path::PathBuf::from("AGENT-TRACE.md"),
crate::types::Action::Create,
crate::types::DocType::Reference,
),
(
std::path::PathBuf::from(".gitignore"),
crate::types::Action::Create,
DocType::Reference,
),
],
actor: crate::types::Actor::System,
summary: "init: create AGENT-TRACE.md and .gitignore".into(),
agent_name: None,
session_id: None,
};
git.commit(&agent_trace_info)?;
}
if scan {
let count = scan_and_register(&path, &mut manifest)?;
manifest.save(&path)?;
if count > 0 {
let agent_trace_content = crate::agent_trace_md::generate(&path, &manifest);
std::fs::write(path.join("AGENT-TRACE.md"), &agent_trace_content)?;
let mut files: Vec<_> = manifest
.documents()
.iter()
.map(|d| {
(
d.path.clone(),
crate::types::Action::Create,
d.doc_type.clone(),
)
})
.collect();
files.push((
std::path::PathBuf::from("AGENT-TRACE.md"),
crate::types::Action::Modify,
crate::types::DocType::Reference,
));
let info = crate::git_store::CommitInfo {
action: crate::types::Action::Init,
files,
actor: crate::types::Actor::System,
summary: format!("scanned {count} existing markdown files"),
agent_name: None,
session_id: None,
};
git.commit(&info)?;
output.line(&format!(
"Registered {count} existing markdown files as scratch."
))?;
}
}
output.line(&format!(
"Initialised agent-trace store at {}",
path.display()
))?;
output.line("Store ready. Use `agent-trace mcp` or `agent-trace open` to start monitoring.")?;
Ok(())
}
fn scan_and_register(root: &Path, manifest: &mut Manifest) -> Result<usize> {
let mut count = 0;
for entry in walkdir_md(root) {
let rel = entry.strip_prefix(root).unwrap_or(&entry);
if rel.starts_with(".agent-trace") {
continue;
}
if rel == std::path::Path::new("AGENT-TRACE.md")
|| rel == std::path::Path::new("context.md")
{
continue;
}
if manifest.is_tracked(rel) {
continue;
}
manifest.register(rel, DocType::Scratch, "")?;
count += 1;
}
Ok(count)
}
fn walkdir_md(root: &Path) -> Vec<std::path::PathBuf> {
let mut results = Vec::new();
walk(root, &mut results);
results
}
fn walk(dir: &Path, out: &mut Vec<std::path::PathBuf>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if path
.file_name()
.map(|n| n.to_string_lossy().starts_with('.'))
.unwrap_or(false)
{
continue;
}
walk(&path, out);
} else if path.extension().and_then(|e| e.to_str()) == Some("md") {
out.push(path);
}
}
}
const DEFAULT_GITIGNORE: &str = r#"# agent-trace defaults
.DS_Store
*.tmp
*.swp
*.swo
~*
.venv/
venv/
node_modules/
__pycache__/
*.pyc
"#;
#[cfg(test)]
mod tests {
use super::*;
use crate::observability::NoopOutput;
use tempfile::TempDir;
#[test]
fn test_init_empty_directory() {
let tmp = TempDir::new().unwrap();
run(tmp.path(), false, &NoopOutput).unwrap();
assert!(tmp.path().join(".agent-trace").exists());
assert!(tmp.path().join(".agent-trace").join("config.toml").exists());
assert!(tmp
.path()
.join(".agent-trace")
.join("manifest.toml")
.exists());
assert!(tmp.path().join(".agent-trace").join("repo").exists());
assert!(tmp.path().join(".agent-trace").join("locks").exists());
assert!(tmp.path().join(".gitignore").exists());
}
#[test]
fn test_init_idempotent() {
let tmp = TempDir::new().unwrap();
run(tmp.path(), false, &NoopOutput).unwrap();
run(tmp.path(), false, &NoopOutput).unwrap();
}
#[test]
fn test_init_with_scan() {
let tmp = TempDir::new().unwrap();
std::fs::write(tmp.path().join("prd.md"), "# PRD").unwrap();
std::fs::write(tmp.path().join("notes.md"), "notes").unwrap();
run(tmp.path(), true, &NoopOutput).unwrap();
let manifest = crate::manifest::Manifest::load(tmp.path()).unwrap();
assert_eq!(manifest.len(), 2);
assert!(manifest
.documents()
.iter()
.all(|d| d.doc_type == DocType::Scratch));
}
#[test]
fn test_config_has_uuid() {
let tmp = TempDir::new().unwrap();
run(tmp.path(), false, &NoopOutput).unwrap();
let cfg = crate::config::StoreConfig::load(tmp.path()).unwrap();
assert!(cfg.store.id.0.parse::<uuid::Uuid>().is_ok());
assert!(!cfg.store.agent_trace_version.is_empty());
}
#[cfg(unix)]
#[test]
fn test_agent_trace_dir_permissions() {
use std::os::unix::fs::MetadataExt;
let tmp = TempDir::new().unwrap();
run(tmp.path(), false, &NoopOutput).unwrap();
let meta = std::fs::metadata(tmp.path().join(".agent-trace")).unwrap();
assert_eq!(meta.mode() & 0o777, 0o700);
}
}