use std::path::Path;
use crate::cli::DriftAction;
use crate::config::Config;
use crate::drift::{DriftDetector, DriftFixer, DriftReporter, DriftStatus, EnvironmentState};
pub fn run_drift(file: &str, action: &DriftAction) -> i32 {
let project_dir = Path::new(file)
.parent()
.unwrap_or(Path::new("."))
.to_path_buf();
match action {
DriftAction::Check { output_format } => run_drift_check(&project_dir, file, output_format),
DriftAction::Status { verbose } => run_drift_status(&project_dir, *verbose),
DriftAction::Accept { tools } => run_drift_accept(&project_dir, file, tools.as_deref()),
DriftAction::Fix { dry_run, force: _ } => run_drift_fix(&project_dir, file, *dry_run),
}
}
fn run_drift_check(project_dir: &Path, config_file: &str, output_format: &str) -> i32 {
let config = Config::new(config_file);
let drift_config = config.drift.clone().unwrap_or_default();
if !drift_config.enabled {
println!("Drift detection is disabled in configuration.");
println!("Enable it by setting [drift] enabled = true in jarvy.toml");
return 0;
}
let state = match EnvironmentState::load(project_dir) {
Ok(Some(state)) => state,
Ok(None) => {
println!("\x1b[33m⚠\x1b[0m No baseline state found.");
println!(" Run 'jarvy setup' to capture the initial state, or");
println!(" Run 'jarvy drift accept' to create a baseline from current state.");
return 1;
}
Err(e) => {
eprintln!("Failed to load state: {}", e);
return 1;
}
};
let detector = DriftDetector::new(&drift_config, &state, project_dir);
let report = match detector.detect() {
Ok(report) => report,
Err(e) => {
eprintln!("Drift detection failed: {}", e);
return 1;
}
};
if output_format == "json" {
match DriftReporter::to_json(&report) {
Ok(json) => println!("{}", json),
Err(e) => {
eprintln!("Failed to serialize report: {}", e);
return 1;
}
}
} else {
DriftReporter::print_report(&report);
}
match report.status {
DriftStatus::NoDrift => 0,
DriftStatus::DriftDetected => 1,
DriftStatus::NoBaseline => 2,
}
}
fn run_drift_status(project_dir: &Path, verbose: bool) -> i32 {
let state = match EnvironmentState::load(project_dir) {
Ok(Some(state)) => state,
Ok(None) => {
println!("\x1b[33m⚠\x1b[0m No baseline state found.");
println!(" The baseline is captured automatically after 'jarvy setup'.");
println!(" Or run 'jarvy drift accept' to create one manually.");
return 0;
}
Err(e) => {
eprintln!("Failed to load state: {}", e);
return 1;
}
};
println!("\x1b[1mDrift Detection Baseline\x1b[0m");
println!("========================");
println!("State version: {}", state.version);
println!("Created: {}", state.created_at);
println!("Updated: {}", state.updated_at);
println!();
println!("\x1b[1mTracked Tools ({}):\x1b[0m", state.tools.len());
for (name, tool) in &state.tools {
if verbose {
println!(
" {} {} (via {}, at {})",
name,
tool.version,
tool.install_method,
tool.path.display()
);
} else {
println!(" {} {}", name, tool.version);
}
}
if !state.files.is_empty() {
println!();
println!("\x1b[1mTracked Files ({}):\x1b[0m", state.files.len());
for (path, hash) in &state.files {
if verbose {
println!(" {} ({})", path, hash);
} else {
println!(" {}", path);
}
}
}
0
}
fn run_drift_accept(project_dir: &Path, config_file: &str, tools_filter: Option<&str>) -> i32 {
let config = Config::new(config_file);
let drift_config = config.drift.clone().unwrap_or_default();
let mut state = EnvironmentState::load(project_dir)
.ok()
.flatten()
.unwrap_or_default();
let tool_configs = config.get_tool_configs();
let tools_to_accept: Vec<String> = if let Some(filter) = tools_filter {
filter.split(',').map(|s| s.trim().to_string()).collect()
} else {
tool_configs.keys().cloned().collect()
};
let mut accepted = 0;
for tool_name in &tools_to_accept {
if let Some(version) = get_installed_version(tool_name) {
let path = which::which(tool_name.as_str())
.unwrap_or_else(|_| std::path::PathBuf::from("unknown"));
let install_method = detect_install_method(tool_name);
state.set_tool(tool_name, &version, &path, &install_method);
accepted += 1;
}
}
for file_path in &drift_config.track_files {
let full_path = project_dir.join(file_path);
if full_path.exists() {
if let Ok(hash) = crate::drift::state::hash_file(&full_path) {
state.set_file_hash(file_path, &hash);
}
}
}
let config_path = project_dir.join("jarvy.toml");
if config_path.exists() {
if let Ok(hash) = crate::drift::state::hash_file(&config_path) {
state.set_config_hash(&hash);
}
}
if let Err(e) = state.save(project_dir) {
eprintln!("Failed to save state: {}", e);
return 1;
}
println!("\x1b[32m✓\x1b[0m Baseline state updated");
println!(
" {} tool{} accepted",
accepted,
if accepted == 1 { "" } else { "s" }
);
if !drift_config.track_files.is_empty() {
println!(
" {} file{} tracked",
drift_config.track_files.len(),
if drift_config.track_files.len() == 1 {
""
} else {
"s"
}
);
}
0
}
fn run_drift_fix(project_dir: &Path, config_file: &str, dry_run: bool) -> i32 {
let config = Config::new(config_file);
let drift_config = config.drift.clone().unwrap_or_default();
if !drift_config.enabled {
println!("Drift detection is disabled in configuration.");
return 0;
}
let state = match EnvironmentState::load(project_dir) {
Ok(Some(state)) => state,
Ok(None) => {
println!("\x1b[33m⚠\x1b[0m No baseline state found.");
println!(" Run 'jarvy setup' first to establish a baseline.");
return 1;
}
Err(e) => {
eprintln!("Failed to load state: {}", e);
return 1;
}
};
let detector = DriftDetector::new(&drift_config, &state, project_dir);
let report = match detector.detect() {
Ok(report) => report,
Err(e) => {
eprintln!("Drift detection failed: {}", e);
return 1;
}
};
if report.status == DriftStatus::NoDrift {
println!("\x1b[32m✓\x1b[0m No drift detected, nothing to fix.");
return 0;
}
if dry_run {
println!("\x1b[36mDry run mode\x1b[0m - no changes will be made\n");
}
let fixer = DriftFixer::new(dry_run);
let results = fixer.fix_all(&report);
DriftFixer::print_summary(&results);
0
}
fn get_installed_version(tool: &str) -> Option<String> {
use std::process::Command;
let output = Command::new(tool)
.arg("--version")
.output()
.or_else(|_| Command::new(tool).arg("-V").output())
.or_else(|_| Command::new(tool).arg("version").output())
.ok()?;
if !output.status.success() {
return None;
}
let output_str = String::from_utf8_lossy(&output.stdout);
extract_version(&output_str)
}
fn extract_version(output: &str) -> Option<String> {
let version_regex =
regex::Regex::new(r"(?i)v?(\d+\.\d+(?:\.\d+)?(?:-[a-zA-Z0-9.]+)?(?:\+[a-zA-Z0-9.]+)?)")
.ok()?;
version_regex
.captures(output)
.and_then(|caps| caps.get(1))
.map(|m| m.as_str().to_string())
}
fn detect_install_method(tool: &str) -> String {
if let Ok(path) = which::which(tool) {
let path_str = path.to_string_lossy();
if path_str.contains("/homebrew/") || path_str.contains("/opt/homebrew/") {
return "brew".to_string();
}
if path_str.contains("/.cargo/") {
return "cargo".to_string();
}
if path_str.contains("/.nvm/") {
return "nvm".to_string();
}
if path_str.contains("/.pyenv/") {
return "pyenv".to_string();
}
if path_str.contains("/.rustup/") {
return "rustup".to_string();
}
if path_str.contains("/usr/bin/") || path_str.contains("/usr/local/bin/") {
return "system".to_string();
}
}
"unknown".to_string()
}