diff --git a/Cargo.toml b/Cargo.toml index d912c66..cf34a35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } dialoguer = "0.12" console = "0.15" ctrlc = "3" +uuid = { version = "1.19.0", features = ["v4"] } [dev-dependencies] tempfile = "3" diff --git a/src/cli/install.rs b/src/cli/install.rs index ba03e5b..d67b902 100644 --- a/src/cli/install.rs +++ b/src/cli/install.rs @@ -1,3 +1,4 @@ +use crate::utils::write_atomic; use anyhow::{Context, Result}; use console::style; use std::fs; @@ -61,7 +62,7 @@ pub fn install_claude_code() -> Result<()> { if !content.contains("vgrep") { content.push_str("\n\n"); content.push_str(VGREP_SKILL.trim()); - fs::write(&instructions_path, content)?; + write_atomic(&instructions_path, content)?; println!( " {} Added vgrep skill to Claude Code", style("[+]").green() @@ -101,7 +102,7 @@ pub fn uninstall_claude_code() -> Result<()> { let updated = content .replace(VGREP_SKILL.trim(), "") .replace("\n\n\n", "\n\n"); - fs::write(&instructions_path, updated.trim())?; + write_atomic(&instructions_path, updated.trim())?; println!(" {} Removed vgrep from Claude Code", style("[-]").green()); } else { println!( @@ -144,7 +145,7 @@ Usage: })"#; let tool_path = tool_dir.join("vgrep.ts"); - fs::write(&tool_path, tool_content.trim())?; + write_atomic(&tool_path, tool_content.trim())?; println!(" {} Created vgrep tool", style("[+]").green()); // Update opencode.json @@ -169,7 +170,7 @@ Usage: "enabled": true }); - fs::write(&config_path, serde_json::to_string_pretty(&config)?)?; + write_atomic(&config_path, serde_json::to_string_pretty(&config)?)?; println!(" {} Updated OpenCode config", style("[+]").green()); println!(); @@ -204,7 +205,7 @@ pub fn uninstall_opencode() -> Result<()> { obj.remove("vgrep"); } } - fs::write(&config_path, serde_json::to_string_pretty(&config)?)?; + write_atomic(&config_path, serde_json::to_string_pretty(&config)?)?; println!(" {} Updated OpenCode config", style("[-]").green()); } } @@ -228,7 +229,7 @@ pub fn install_codex() -> Result<()> { if !content.contains("vgrep") { content.push_str("\n\n"); content.push_str(VGREP_SKILL.trim()); - fs::write(&agents_path, content)?; + write_atomic(&agents_path, content)?; println!(" {} Added vgrep skill to Codex", style("[+]").green()); } else { println!( @@ -257,7 +258,7 @@ pub fn uninstall_codex() -> Result<()> { if updated.trim().is_empty() { fs::remove_file(&agents_path)?; } else { - fs::write(&agents_path, updated.trim())?; + write_atomic(&agents_path, updated.trim())?; } println!(" {} Removed vgrep from Codex", style("[-]").green()); } @@ -284,7 +285,7 @@ pub fn install_droid() -> Result<()> { fs::create_dir_all(&skills_dir)?; let skill_content = VGREP_SKILL.trim(); - fs::write(skills_dir.join("SKILL.md"), skill_content)?; + write_atomic(skills_dir.join("SKILL.md"), skill_content)?; println!(" {} Created vgrep skill", style("[+]").green()); // Create hooks @@ -344,8 +345,8 @@ if __name__ == "__main__": main() "#; - fs::write(hooks_dir.join("vgrep_watch.py"), watch_hook)?; - fs::write(hooks_dir.join("vgrep_watch_kill.py"), kill_hook)?; + write_atomic(hooks_dir.join("vgrep_watch.py"), watch_hook)?; + write_atomic(hooks_dir.join("vgrep_watch_kill.py"), kill_hook)?; println!(" {} Created vgrep hooks", style("[+]").green()); // Update settings.json @@ -412,7 +413,7 @@ if __name__ == "__main__": } settings["hooks"] = serde_json::Value::Object(hooks); - fs::write(&settings_path, serde_json::to_string_pretty(&settings)?)?; + write_atomic(&settings_path, serde_json::to_string_pretty(&settings)?)?; println!(" {} Updated Factory Droid settings", style("[+]").green()); println!(); @@ -461,7 +462,7 @@ pub fn uninstall_droid() -> Result<()> { obj.retain(|_, v| v.as_array().map(|a| !a.is_empty()).unwrap_or(true)); } } - fs::write(&settings_path, serde_json::to_string_pretty(&settings)?)?; + write_atomic(&settings_path, serde_json::to_string_pretty(&settings)?)?; } } println!(" {} Updated Factory Droid settings", style("[-]").green()); diff --git a/src/config.rs b/src/config.rs index 6e9bddf..9f93334 100644 --- a/src/config.rs +++ b/src/config.rs @@ -224,7 +224,7 @@ impl Config { std::fs::create_dir_all(&config_dir)?; let config_path = config_dir.join(CONFIG_FILE); let content = serde_json::to_string_pretty(self)?; - std::fs::write(&config_path, content)?; + crate::utils::write_atomic(&config_path, content)?; Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index 6f688f4..7c5158a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,6 +18,7 @@ pub mod core; pub mod server; pub mod ui; pub mod watcher; +pub mod utils; pub use config::Config; pub use core::{Database, EmbeddingEngine, Indexer, SearchEngine, ServerIndexer}; diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..5c89d63 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,40 @@ +use std::path::Path; +use std::fs; +use anyhow::Result; + +/// Write content to a file atomically by writing to a temp file and renaming it +pub fn write_atomic, C: AsRef<[u8]>>(path: P, content: C) -> Result<()> { + let path = path.as_ref(); + let dir = path.parent().unwrap_or_else(|| Path::new(".")); + + // Create directory if it doesn't exist + fs::create_dir_all(dir)?; + + // Create a temporary file in the same directory to ensure it's on the same filesystem + // We use a simple strategy: filename.tmp.uuid + let tmp_file_name = format!("{}.tmp.{}", + path.file_name().unwrap_or_default().to_string_lossy(), + uuid::Uuid::new_v4() + ); + let tmp_path = dir.join(tmp_file_name); + + // Write content to temp file + match fs::write(&tmp_path, content) { + Ok(_) => { + // Rename temp file to destination + match fs::rename(&tmp_path, path) { + Ok(_) => Ok(()), + Err(e) => { + // Try to clean up temp file if rename fails + let _ = fs::remove_file(&tmp_path); + Err(anyhow::anyhow!("Failed to rename temp file to destination: {}", e)) + } + } + }, + Err(e) => { + // Try to clean up temp file if write fails + let _ = fs::remove_file(&tmp_path); + Err(anyhow::anyhow!("Failed to write to temp file: {}", e)) + } + } +}