use super::error::{ProfileError, ProfileResult};
use super::fsops::{
ProfileBackup, backup_profile, calculate_integrity, collect_all_files, restore_profile,
};
use super::installer::{self, ProfileInstaller};
use super::lockfile::{ProfileLockEntry, ProfileLockfile};
use super::manifest::ProfileManifest;
use super::project::ProfilesConfig;
use super::provider_registry::ProviderSource;
use std::path::Path;
pub fn install_profile(
profile_name: &str,
profiles_dir: &Path,
workspace: &Path,
force: bool,
commit: Option<String>,
provider_id: Option<&str>,
source: Option<ProviderSource>,
) -> ProfileResult<()> {
let lockfile_path = workspace.join(".codanna/profiles.lock.json");
let mut lockfile = ProfileLockfile::load(&lockfile_path)?;
let mut backup: Option<ProfileBackup> = None;
let profile_dir = profiles_dir.join(profile_name);
let manifest_path = profile_dir.join("profile.json");
if !manifest_path.exists() {
return Err(ProfileError::InvalidManifest {
reason: format!(
"Profile '{profile_name}' not found at {}",
manifest_path.display()
),
});
}
let manifest = ProfileManifest::from_file(&manifest_path)?;
if let Some(existing) = lockfile.get_profile(profile_name) {
if !force {
return Err(ProfileError::AlreadyInstalled {
name: profile_name.to_string(),
version: existing.version.clone(),
});
}
backup = Some(backup_profile(workspace, existing)?);
}
let files_to_install = if manifest.files.is_empty() {
collect_all_files(&profile_dir)?
} else {
manifest.files.clone()
};
installer::check_all_conflicts(workspace, &files_to_install, profile_name, &lockfile, force)?;
let installer = ProfileInstaller::new();
let provider_name = manifest.provider_name();
let (installed_files, sidecars) = match installer.install_files(
&profile_dir,
workspace,
&files_to_install,
profile_name,
provider_name,
&lockfile,
force,
) {
Ok(result) => result,
Err(e) => {
if let Some(b) = backup {
let _ = restore_profile(&b);
}
return Err(e);
}
};
if !sidecars.is_empty() {
eprintln!("\nWarning: File conflicts resolved with sidecar files:");
for (intended, actual) in &sidecars {
eprintln!(" {intended} exists - installed as {actual}");
eprintln!(" Original file preserved");
}
eprintln!("\nReview and manually merge sidecar files with originals.");
}
let absolute_files: Vec<String> = installed_files
.iter()
.map(|rel| workspace.join(rel).to_string_lossy().to_string())
.collect();
let integrity = match calculate_integrity(&absolute_files) {
Ok(hash) => hash,
Err(e) => {
if let Some(b) = backup {
let _ = restore_profile(&b);
}
return Err(e);
}
};
let entry = ProfileLockEntry {
name: profile_name.to_string(),
version: manifest.version.clone(),
installed_at: current_timestamp(),
files: installed_files,
integrity,
commit,
provider_id: provider_id.map(String::from),
source,
};
lockfile.add_profile(entry);
if let Err(e) = lockfile.save(&lockfile_path) {
lockfile.remove_profile(profile_name);
if let Some(b) = backup {
let _ = restore_profile(&b);
}
return Err(e);
}
let profiles_config_path = workspace.join(".codanna/profiles.json");
let mut profiles_config = ProfilesConfig::load(&profiles_config_path)?;
let profile_ref = if let Some(provider) = provider_id {
format!("{profile_name}@{provider}")
} else {
profile_name.to_string()
};
profiles_config.add_profile(&profile_ref);
if let Err(e) = profiles_config.save(&profiles_config_path) {
lockfile.remove_profile(profile_name);
let _ = lockfile.save(&lockfile_path);
if let Some(b) = backup {
let _ = restore_profile(&b);
}
return Err(e);
}
Ok(())
}
fn current_timestamp() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let duration = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards");
let secs = duration.as_secs();
let days = secs / 86400;
let years = 1970 + days / 365;
let day_of_year = days % 365;
let month = (day_of_year / 30) + 1;
let day = (day_of_year % 30) + 1;
let time_of_day = secs % 86400;
let hours = time_of_day / 3600;
let minutes = (time_of_day % 3600) / 60;
let seconds = time_of_day % 60;
format!("{years:04}-{month:02}-{day:02}T{hours:02}:{minutes:02}:{seconds:02}Z")
}