Skip to main content

agent_trace/commands/
replace.rs

1use crate::git_store::CommitInfo;
2use crate::observability::CliOutput;
3use crate::permissions::{check_permission, PermissionResult};
4use crate::store::Store;
5use crate::types::{Action, Actor, DocType};
6use anyhow::Result;
7use std::path::Path;
8
9pub fn run(
10    store_root: &Path,
11    find: &str,
12    replace: &str,
13    type_filter: Option<&DocType>,
14    dry_run: bool,
15    output: &dyn CliOutput,
16) -> Result<()> {
17    let store = Store::open(store_root)?;
18    let docs = store.manifest.list(type_filter);
19
20    let mut matches: Vec<(std::path::PathBuf, DocType, String)> = Vec::new();
21
22    for doc in &docs {
23        let full = store_root.join(&doc.path);
24        if !full.exists() {
25            continue;
26        }
27        let content = std::fs::read_to_string(&full)?;
28        if content.contains(find) {
29            matches.push((doc.path.clone(), doc.doc_type.clone(), content));
30        }
31    }
32
33    if matches.is_empty() {
34        output.line("No matches found.")?;
35        return Ok(());
36    }
37
38    output.line(&format!("Found {} file(s) with matches:", matches.len()))?;
39    for (path, dt, _) in &matches {
40        output.line(&format!("  [{}] {}", dt.indicator(), path.display()))?;
41    }
42
43    if dry_run {
44        output.line("(dry-run: no changes applied)")?;
45        return Ok(());
46    }
47
48    let mut committed: Vec<(std::path::PathBuf, Action, DocType)> = Vec::new();
49
50    for (path, doc_type, content) in matches {
51        // Check permission.
52        let perm = check_permission(&doc_type, &Actor::User, &store.overrides, Some(&path));
53        if let PermissionResult::Denied { reason } = perm {
54            output.warn(&format!("Skipping {} (denied): {}", path.display(), reason))?;
55            continue;
56        }
57
58        let new_content = content.replace(find, replace);
59        std::fs::write(store_root.join(&path), &new_content)?;
60        committed.push((path, Action::Modify, doc_type));
61    }
62
63    if !committed.is_empty() {
64        let info = CommitInfo {
65            action: Action::Modify,
66            files: committed,
67            actor: Actor::User,
68            summary: format!("replace '{find}' with '{replace}'"),
69            agent_name: None,
70            session_id: None,
71        };
72        store.commit(&info)?;
73        output.line("Changes applied.")?;
74    }
75
76    Ok(())
77}