Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
25 changes: 13 additions & 12 deletions src/cli/install.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::utils::write_atomic;
use anyhow::{Context, Result};
use console::style;
use std::fs;
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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!(
Expand Down Expand Up @@ -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
Expand All @@ -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!();
Expand Down Expand Up @@ -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());
}
}
Expand All @@ -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!(
Expand Down Expand Up @@ -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());
}
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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!();
Expand Down Expand Up @@ -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());
Expand Down
2 changes: 1 addition & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}

Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
40 changes: 40 additions & 0 deletions src/utils.rs
Original file line number Diff line number Diff line change
@@ -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<P: AsRef<Path>, 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))
}
}
}
Loading