use std::fs;
use std::path::Path;
use crate::cli::args::InitArgs;
use crate::cli::commands::hooks;
use crate::error::{AgitError, Result};
use crate::storage::{FileHeadStore, FileIndexStore};
use crate::templates::{generate_versioned_protocol, AGIT_VERSION, TEMPLATE_FILES};
const AGIT_DIR: &str = ".agit";
const GITIGNORE_ENTRIES_V2: &str = r#"
# AGIT - AI-Native Git Wrapper (V2 Git-native storage)
# All local state - shared data is in refs/agit/* and Git ODB
.agit/
# MCP configs (shared with team)
!.mcp.json
!.cursor/mcp.json
!.vscode/mcp.json
"#;
pub fn execute(args: InitArgs) -> Result<()> {
let cwd = std::env::current_dir()?;
let agit_dir = cwd.join(AGIT_DIR);
if !cwd.join(".git").exists() {
return Err(AgitError::NotGitRepository);
}
if args.update {
if !agit_dir.exists() {
println!("⚠️ AGIT is not initialized. Run `agit init` first.");
return Ok(());
}
let updated = update_template_files(&cwd)?;
if !updated {
println!("No template files were updated. Ensure CLAUDE.md or .cursorrules exists.");
}
return Ok(());
}
if agit_dir.exists() && !args.force {
return Err(AgitError::AlreadyInitialized { path: agit_dir });
}
create_agit_structure(&agit_dir)?;
if !args.no_templates {
generate_template_files(&cwd)?;
}
if !args.no_templates {
println!("\nGenerated MCP configs:");
generate_mcp_configs(&cwd)?;
}
if !args.no_gitignore {
update_gitignore(&cwd)?;
}
if !args.no_hooks {
if let Err(e) = hooks::install_all_hooks(&cwd) {
eprintln!("Warning: Failed to install git hooks: {}", e);
eprintln!("You can install them manually with: agit hooks install");
}
}
println!("\nInitialized AGIT repository in {}", agit_dir.display());
if !args.no_templates {
println!("\nGenerated instruction files:");
for (name, _) in TEMPLATE_FILES {
println!(" - {}", name);
}
}
if !args.no_hooks {
println!("\nInstalled git hooks: post-commit, post-checkout, post-merge, post-rewrite");
}
println!("\nAGIT is ready! MCP configs auto-detected by Cursor and Claude Code.");
println!("Git hooks will keep agit in sync when you use native git commands.");
println!("Restart your AI assistant to activate AGIT memory.");
Ok(())
}
fn create_agit_structure(agit_dir: &Path) -> Result<()> {
fs::create_dir_all(agit_dir)?;
fs::create_dir_all(agit_dir.join("tmp"))?;
let config_path = agit_dir.join("config.json");
if !config_path.exists() {
fs::write(&config_path, "{\"storage_version\": 2}\n")?;
}
let head_store = FileHeadStore::new(agit_dir);
head_store.ensure_exists("main")?;
let index_store = FileIndexStore::new(agit_dir);
index_store.ensure_exists()?;
Ok(())
}
const AGIT_POLICY_MARKER: &str = "# SYSTEM POLICY: AGIT MEMORY";
const PROTOCOL_START_MARKER: &str = "<system_protocol";
const PROTOCOL_END_MARKER: &str = "</system_protocol>";
fn generate_template_files(project_dir: &Path) -> Result<()> {
for (filename, content) in TEMPLATE_FILES {
let path = project_dir.join(filename);
if path.exists() {
let existing = fs::read_to_string(&path)?;
if existing.contains(AGIT_POLICY_MARKER) {
println!("Skipping {} (AGIT policy already present)", filename);
continue;
}
let new_content = if existing.ends_with('\n') {
format!("{}\n{}", existing, content)
} else {
format!("{}\n\n{}", existing, content)
};
fs::write(&path, new_content)?;
println!("Appended AGIT policy to existing {}", filename);
} else {
fs::write(&path, content)?;
}
}
Ok(())
}
fn update_template_files(project_dir: &Path) -> Result<bool> {
let versioned_protocol = generate_versioned_protocol();
let mut any_updated = false;
let template_files = ["CLAUDE.md", ".cursorrules"];
for filename in template_files {
let path = project_dir.join(filename);
if !path.exists() {
continue;
}
let existing = fs::read_to_string(&path)?;
let start_pos = match existing.find(PROTOCOL_START_MARKER) {
Some(pos) => pos,
None => {
println!(
"⚠️ Could not find Agit block in {}. Please run `agit init --force` to reset the file completely.",
filename
);
continue;
},
};
let end_pos = match existing[start_pos..].find(PROTOCOL_END_MARKER) {
Some(pos) => start_pos + pos + PROTOCOL_END_MARKER.len(),
None => {
println!(
"⚠️ Could not find closing </system_protocol> in {}. Please run `agit init --force` to reset the file completely.",
filename
);
continue;
},
};
let before = &existing[..start_pos];
let after = &existing[end_pos..];
let new_content = format!("{}{}{}", before, versioned_protocol, after);
fs::write(&path, new_content)?;
println!(
"✅ Updated AI Protocols in {} to v{}",
filename, AGIT_VERSION
);
any_updated = true;
}
Ok(any_updated)
}
fn get_agit_command_path() -> String {
std::env::current_exe()
.ok()
.and_then(|p| p.to_str().map(|s| s.to_string()))
.unwrap_or_else(|| "agit".to_string()) }
fn generate_mcp_config(agit_path: &str) -> String {
let escaped_path = agit_path.replace('\\', "\\\\");
format!(
r#"{{
"mcpServers": {{
"agit": {{
"command": "{}",
"args": ["server"]
}}
}}
}}
"#,
escaped_path
)
}
fn generate_vscode_mcp_config(agit_path: &str) -> String {
let escaped_path = agit_path.replace('\\', "\\\\");
format!(
r#"{{
"servers": {{
"agit": {{
"command": "{}",
"args": ["server"]
}}
}}
}}
"#,
escaped_path
)
}
fn generate_mcp_configs(project_dir: &Path) -> Result<()> {
let agit_path = get_agit_command_path();
let mcp_config = generate_mcp_config(&agit_path);
let vscode_config = generate_vscode_mcp_config(&agit_path);
let mcp_json_path = project_dir.join(".mcp.json");
if !mcp_json_path.exists() {
fs::write(&mcp_json_path, &mcp_config)?;
println!(" - .mcp.json (Claude Code)");
} else {
println!(" - Skipping .mcp.json (already exists)");
}
let cursor_dir = project_dir.join(".cursor");
fs::create_dir_all(&cursor_dir)?;
let cursor_mcp_path = cursor_dir.join("mcp.json");
if !cursor_mcp_path.exists() {
fs::write(&cursor_mcp_path, &mcp_config)?;
println!(" - .cursor/mcp.json (Cursor)");
} else {
println!(" - Skipping .cursor/mcp.json (already exists)");
}
let vscode_dir = project_dir.join(".vscode");
fs::create_dir_all(&vscode_dir)?;
let vscode_mcp_path = vscode_dir.join("mcp.json");
if !vscode_mcp_path.exists() {
fs::write(&vscode_mcp_path, &vscode_config)?;
println!(" - .vscode/mcp.json (VS Code Copilot)");
} else {
println!(" - Skipping .vscode/mcp.json (already exists)");
}
Ok(())
}
fn update_gitignore(project_dir: &Path) -> Result<()> {
let gitignore_path = project_dir.join(".gitignore");
let existing = if gitignore_path.exists() {
fs::read_to_string(&gitignore_path)?
} else {
String::new()
};
if existing.contains("# AGIT - AI-Native Git Wrapper") {
return Ok(());
}
let new_content = if existing.ends_with('\n') || existing.is_empty() {
format!("{}{}", existing, GITIGNORE_ENTRIES_V2)
} else {
format!("{}\n{}", existing, GITIGNORE_ENTRIES_V2)
};
fs::write(&gitignore_path, new_content)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn setup_git_repo() -> TempDir {
let temp = TempDir::new().unwrap();
fs::create_dir(temp.path().join(".git")).unwrap();
temp
}
#[test]
fn test_create_agit_structure() {
let temp = setup_git_repo();
let agit_dir = temp.path().join(".agit");
create_agit_structure(&agit_dir).unwrap();
assert!(agit_dir.exists());
assert!(agit_dir.join("tmp").exists());
assert!(agit_dir.join("config.json").exists());
assert!(agit_dir.join("HEAD").exists());
assert!(agit_dir.join("index").exists());
assert!(!agit_dir.join("objects").exists());
assert!(!agit_dir.join("refs").exists());
let config = fs::read_to_string(agit_dir.join("config.json")).unwrap();
assert!(config.contains("\"storage_version\": 2"));
}
#[test]
fn test_generate_template_files() {
let temp = setup_git_repo();
generate_template_files(temp.path()).unwrap();
assert!(temp.path().join("CLAUDE.md").exists());
assert!(temp.path().join(".cursorrules").exists());
}
#[test]
fn test_generate_template_files_appends_to_existing() {
let temp = setup_git_repo();
let user_content = "# My Project\n\nThis is my custom content.\n";
fs::write(temp.path().join("CLAUDE.md"), user_content).unwrap();
generate_template_files(temp.path()).unwrap();
let content = fs::read_to_string(temp.path().join("CLAUDE.md")).unwrap();
assert!(content.contains("# My Project"));
assert!(content.contains("This is my custom content"));
assert!(content.contains(AGIT_POLICY_MARKER));
}
#[test]
fn test_generate_template_files_skips_if_policy_exists() {
let temp = setup_git_repo();
let existing = format!("# My Project\n\n{}\n", AGIT_POLICY_MARKER);
fs::write(temp.path().join("CLAUDE.md"), &existing).unwrap();
generate_template_files(temp.path()).unwrap();
let content = fs::read_to_string(temp.path().join("CLAUDE.md")).unwrap();
assert_eq!(
content.matches(AGIT_POLICY_MARKER).count(),
1,
"AGIT policy should only appear once"
);
}
#[test]
fn test_generate_mcp_configs() {
let temp = setup_git_repo();
generate_mcp_configs(temp.path()).unwrap();
assert!(temp.path().join(".mcp.json").exists());
assert!(temp.path().join(".cursor/mcp.json").exists());
assert!(temp.path().join(".vscode/mcp.json").exists());
let mcp_content = fs::read_to_string(temp.path().join(".mcp.json")).unwrap();
assert!(mcp_content.contains("mcpServers"));
assert!(mcp_content.contains("agit"));
assert!(mcp_content.contains("server"));
let vscode_content = fs::read_to_string(temp.path().join(".vscode/mcp.json")).unwrap();
assert!(vscode_content.contains("\"servers\""));
assert!(!vscode_content.contains("mcpServers"));
assert!(vscode_content.contains("agit"));
}
#[test]
fn test_get_agit_command_path() {
let path = get_agit_command_path();
assert!(!path.is_empty());
}
#[test]
fn test_update_gitignore_creates_new() {
let temp = setup_git_repo();
update_gitignore(temp.path()).unwrap();
let content = fs::read_to_string(temp.path().join(".gitignore")).unwrap();
assert!(content.contains("# AGIT - AI-Native Git Wrapper"));
assert!(content.contains(".agit/"));
}
#[test]
fn test_update_gitignore_appends() {
let temp = setup_git_repo();
fs::write(temp.path().join(".gitignore"), "node_modules/\n").unwrap();
update_gitignore(temp.path()).unwrap();
let content = fs::read_to_string(temp.path().join(".gitignore")).unwrap();
assert!(content.contains("node_modules/"));
assert!(content.contains("# AGIT - AI-Native Git Wrapper"));
}
#[test]
fn test_update_gitignore_idempotent() {
let temp = setup_git_repo();
update_gitignore(temp.path()).unwrap();
let content1 = fs::read_to_string(temp.path().join(".gitignore")).unwrap();
update_gitignore(temp.path()).unwrap();
let content2 = fs::read_to_string(temp.path().join(".gitignore")).unwrap();
assert_eq!(content1, content2);
}
#[test]
fn test_update_template_files_replaces_protocol_block() {
use crate::templates::AGIT_VERSION;
let temp = setup_git_repo();
let old_content = r#"# My Custom Rules
Some custom instructions here.
# SYSTEM POLICY: AGIT MEMORY
<system_protocol>
<critical_rule id="OLD_RULE">
<instruction>Old instruction content</instruction>
</critical_rule>
</system_protocol>
# More Custom Rules
Additional custom content below.
"#;
fs::write(temp.path().join("CLAUDE.md"), old_content).unwrap();
let updated = update_template_files(temp.path()).unwrap();
assert!(updated, "Should have updated at least one file");
let new_content = fs::read_to_string(temp.path().join("CLAUDE.md")).unwrap();
assert!(new_content.contains("# My Custom Rules"));
assert!(new_content.contains("Some custom instructions here."));
assert!(new_content.contains("# More Custom Rules"));
assert!(new_content.contains("Additional custom content below."));
assert!(!new_content.contains("OLD_RULE"));
assert!(!new_content.contains("Old instruction content"));
assert!(new_content.contains(&format!("<system_protocol version=\"{}\">", AGIT_VERSION)));
assert!(new_content.contains("BATCH_LOGGING"));
assert!(new_content.contains("</system_protocol>"));
}
#[test]
fn test_update_template_files_handles_versioned_protocol() {
use crate::templates::AGIT_VERSION;
let temp = setup_git_repo();
let old_content = r#"# SYSTEM POLICY: AGIT MEMORY
<system_protocol version="0.0.1">
<critical_rule id="OLD_VERSIONED_RULE">
<instruction>Some old versioned instruction</instruction>
</critical_rule>
</system_protocol>
"#;
fs::write(temp.path().join("CLAUDE.md"), old_content).unwrap();
let updated = update_template_files(temp.path()).unwrap();
assert!(updated);
let new_content = fs::read_to_string(temp.path().join("CLAUDE.md")).unwrap();
assert!(!new_content.contains("version=\"0.0.1\""));
assert!(new_content.contains(&format!("version=\"{}\"", AGIT_VERSION)));
assert!(!new_content.contains("OLD_VERSIONED_RULE"));
}
#[test]
fn test_update_template_files_no_marker_returns_false() {
let temp = setup_git_repo();
let content = "# My Project\n\nJust some regular markdown content.\n";
fs::write(temp.path().join("CLAUDE.md"), content).unwrap();
let updated = update_template_files(temp.path()).unwrap();
assert!(!updated, "Should return false when no protocol block found");
let after_content = fs::read_to_string(temp.path().join("CLAUDE.md")).unwrap();
assert_eq!(content, after_content);
}
#[test]
fn test_update_template_files_no_files_returns_false() {
let temp = setup_git_repo();
let updated = update_template_files(temp.path()).unwrap();
assert!(!updated, "Should return false when no template files exist");
}
#[test]
fn test_update_template_files_updates_cursorrules() {
use crate::templates::AGIT_VERSION;
let temp = setup_git_repo();
let old_content = r#"# SYSTEM POLICY: AGIT MEMORY
<system_protocol>
<critical_rule id="CURSOR_OLD">
<instruction>Old cursor rule</instruction>
</critical_rule>
</system_protocol>
"#;
fs::write(temp.path().join(".cursorrules"), old_content).unwrap();
let updated = update_template_files(temp.path()).unwrap();
assert!(updated);
let new_content = fs::read_to_string(temp.path().join(".cursorrules")).unwrap();
assert!(new_content.contains(&format!("<system_protocol version=\"{}\">", AGIT_VERSION)));
assert!(!new_content.contains("CURSOR_OLD"));
assert!(new_content.contains("BATCH_LOGGING"));
}
}