pub mod commands;
pub mod error;
pub mod fsops;
pub mod git;
pub mod installer;
pub mod local;
pub mod lockfile;
pub mod manifest;
pub mod orchestrator;
pub mod project;
pub mod provider;
pub mod provider_registry;
pub mod reference;
pub mod resolver;
pub mod source_resolver;
pub mod template;
pub mod variables;
pub mod verification;
use error::ProfileResult;
use orchestrator::install_profile;
use provider::ProviderManifest;
use provider_registry::{ProviderRegistry, ProviderSource};
use reference::ProfileReference;
use source_resolver::resolve_profile_source;
use std::path::{Path, PathBuf};
pub fn profiles_dir() -> PathBuf {
crate::init::global_dir().join("profiles")
}
pub fn provider_registry_path() -> PathBuf {
crate::init::global_dir().join("providers.json")
}
pub fn init_profile(profile_name: &str, source: Option<&Path>, force: bool) -> ProfileResult<()> {
let workspace = std::env::current_dir()?;
let profiles_dir = source.map(|p| p.to_path_buf()).unwrap_or_else(profiles_dir);
if force {
println!(
"Installing profile '{profile_name}' with --force (will overwrite conflicting files)..."
);
} else {
println!("Installing profile '{profile_name}' to workspace...");
}
install_profile(
profile_name,
&profiles_dir,
&workspace,
force,
None,
None,
None,
)?;
println!("\nProfile '{profile_name}' installed successfully");
if force {
println!(" Note: Conflicting files handled with --force");
println!(" Use 'codanna profile verify {profile_name}' to check integrity");
}
Ok(())
}
pub fn add_provider(source: &str, provider_id: Option<&str>) -> ProfileResult<()> {
let registry_path = provider_registry_path();
let mut registry = ProviderRegistry::load(®istry_path)?;
let provider_source = ProviderSource::parse(source);
let id = provider_id
.map(String::from)
.unwrap_or_else(|| derive_provider_id(&provider_source));
if registry.get_provider(&id).is_some() {
println!("Provider '{id}' is already registered");
println!("Use --force to update or remove it first");
return Ok(());
}
let manifest = load_provider_manifest(&provider_source)?;
registry.add_provider(id.clone(), &manifest, provider_source);
registry.save(®istry_path)?;
println!(
"Added provider '{id}' ({} profiles available)",
manifest.profiles.len()
);
for profile in &manifest.profiles {
println!(" - {}", profile.name);
}
Ok(())
}
pub fn remove_provider(provider_id: &str) -> ProfileResult<()> {
let registry_path = provider_registry_path();
let mut registry = ProviderRegistry::load(®istry_path)?;
if registry.remove_provider(provider_id) {
registry.save(®istry_path)?;
println!("Removed provider '{provider_id}'");
} else {
println!("Provider '{provider_id}' not found");
}
Ok(())
}
pub fn list_providers(verbose: bool) -> ProfileResult<()> {
let registry_path = provider_registry_path();
let registry = ProviderRegistry::load(®istry_path)?;
if registry.providers.is_empty() {
println!("No providers registered");
println!("\nAdd a provider with:");
println!(" codanna profile provider add <source>");
return Ok(());
}
println!("Registered providers:");
for (id, provider) in ®istry.providers {
println!("\n{id}:");
println!(" Name: {}", provider.name);
match &provider.source {
ProviderSource::Github { repo } => println!(" Source: github:{repo}"),
ProviderSource::Url { url } => println!(" Source: {url}"),
ProviderSource::Local { path } => println!(" Source: {path}"),
}
if verbose {
println!(" Profiles ({}):", provider.profiles.len());
for (name, info) in &provider.profiles {
print!(" - {name} ({})", info.version);
if let Some(desc) = &info.description {
print!(": {desc}");
}
println!();
}
} else {
println!(" Profiles: {}", provider.profiles.len());
}
}
Ok(())
}
fn derive_provider_id(source: &ProviderSource) -> String {
match source {
ProviderSource::Github { repo } => {
repo.split('/').next_back().unwrap_or(repo).to_string()
}
ProviderSource::Url { url } => {
url.trim_end_matches(".git")
.split('/')
.next_back()
.unwrap_or("provider")
.to_string()
}
ProviderSource::Local { path } => {
Path::new(path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("local")
.to_string()
}
}
}
fn load_provider_manifest(source: &ProviderSource) -> ProfileResult<ProviderManifest> {
match source {
ProviderSource::Local { path } => {
let manifest_path = Path::new(path).join(".codanna-profile/provider.json");
ProviderManifest::from_file(&manifest_path)
}
ProviderSource::Github { repo } => {
let url = format!("https://github.com/{repo}.git");
load_manifest_from_git(&url)
}
ProviderSource::Url { url } => load_manifest_from_git(url),
}
}
fn load_manifest_from_git(url: &str) -> ProfileResult<ProviderManifest> {
let temp_dir = tempfile::tempdir()?;
git::clone_repository(url, temp_dir.path(), None)?;
let manifest_path = temp_dir.path().join(".codanna-profile/provider.json");
ProviderManifest::from_file(&manifest_path)
}
pub fn verify_profile(profile_name: &str, verbose: bool) -> ProfileResult<()> {
let workspace = std::env::current_dir()?;
verification::verify_profile(&workspace, profile_name, verbose)
}
pub fn verify_all_profiles(verbose: bool) -> ProfileResult<()> {
let workspace = std::env::current_dir()?;
verification::verify_all_profiles(&workspace, verbose)
}
pub fn list_profiles(verbose: bool, json: bool) -> ProfileResult<()> {
use provider_registry::ProviderRegistry;
let registry_path = provider_registry_path();
let registry = ProviderRegistry::load(®istry_path)?;
if registry.providers.is_empty() {
println!("No providers registered");
println!("\nAdd a provider with:");
println!(" codanna profile provider add <source>");
return Ok(());
}
if json {
let output = serde_json::to_string_pretty(®istry.providers)?;
println!("{output}");
return Ok(());
}
println!("Available profiles:\n");
for (provider_id, provider) in ®istry.providers {
println!("From provider '{provider_id}':");
if provider.profiles.is_empty() {
println!(" (no profiles)");
} else {
for (profile_name, profile_info) in &provider.profiles {
print!(" - {profile_name} (v{})", profile_info.version);
if verbose {
if let Some(desc) = &profile_info.description {
print!(": {desc}");
}
}
println!();
}
}
println!();
}
Ok(())
}
pub fn show_status(verbose: bool) -> ProfileResult<()> {
use lockfile::ProfileLockfile;
use project::ProfilesConfig;
let workspace = std::env::current_dir()?;
let profiles_config_path = workspace.join(".codanna/profiles.json");
let lockfile_path = workspace.join(".codanna/profiles.lock.json");
if profiles_config_path.exists() {
let profiles_config = ProfilesConfig::load(&profiles_config_path)?;
if !profiles_config.is_empty() {
let registry_path = provider_registry_path();
let registry = ProviderRegistry::load(®istry_path)?;
let required_providers = profiles_config.get_required_provider_ids();
let missing_providers: Vec<String> = required_providers
.iter()
.filter(|id| registry.get_provider(id).is_none())
.cloned()
.collect();
let lockfile = lockfile::ProfileLockfile::load(&lockfile_path).unwrap_or_default();
let missing_profiles: Vec<String> = profiles_config
.profiles
.iter()
.filter(|profile_ref| {
let reference = ProfileReference::parse(profile_ref);
lockfile.get_profile(&reference.profile).is_none()
})
.cloned()
.collect();
if !missing_providers.is_empty() || !missing_profiles.is_empty() {
println!("Team profile configuration detected at .codanna/profiles.json");
if !missing_providers.is_empty() {
println!(" Missing providers: {}", missing_providers.join(", "));
}
if !missing_profiles.is_empty() {
println!(" Missing profiles: {}", missing_profiles.join(", "));
}
println!();
println!("Run 'codanna profile sync' to register providers and install profiles");
println!();
}
}
}
if !lockfile_path.exists() {
println!("No profiles installed");
return Ok(());
}
let lockfile = ProfileLockfile::load(&lockfile_path)?;
if lockfile.profiles.is_empty() {
println!("No profiles installed");
return Ok(());
}
println!("Installed profiles ({}):", lockfile.profiles.len());
println!();
for (name, entry) in &lockfile.profiles {
println!(" {} (v{})", name, entry.version);
if verbose {
println!(" Installed: {}", entry.installed_at);
if let Some(commit) = &entry.commit {
println!(" Commit: {}", &commit[..8]);
}
println!(" Files: {}", entry.files.len());
for file in &entry.files {
println!(" - {file}");
}
println!(" Integrity: {}", &entry.integrity[..16]);
println!();
}
}
Ok(())
}
pub fn sync_team_config(force: bool) -> ProfileResult<()> {
use project::ProfilesConfig;
let workspace = std::env::current_dir()?;
let profiles_config_path = workspace.join(".codanna/profiles.json");
if !profiles_config_path.exists() {
println!("No team configuration found at .codanna/profiles.json");
println!("Nothing to sync");
return Ok(());
}
let profiles_config = ProfilesConfig::load(&profiles_config_path)?;
if profiles_config.is_empty() {
println!("Team configuration is empty");
println!("Nothing to sync");
return Ok(());
}
println!("Syncing team configuration...");
println!();
let registry_path = provider_registry_path();
let mut registry = ProviderRegistry::load(®istry_path)?;
for (provider_id, extra_provider) in &profiles_config.extra_known_providers {
if registry.get_provider(provider_id).is_some() {
println!("Provider '{provider_id}' already registered, skipping");
continue;
}
println!("Registering provider '{provider_id}'...");
let manifest = load_provider_manifest(&extra_provider.source)?;
registry.add_provider(
provider_id.clone(),
&manifest,
extra_provider.source.clone(),
);
println!(
" Registered provider '{provider_id}' with {} profiles",
manifest.profiles.len()
);
}
registry.save(®istry_path)?;
println!();
let lockfile_path = workspace.join(".codanna/profiles.lock.json");
let lockfile = lockfile::ProfileLockfile::load(&lockfile_path).unwrap_or_default();
for profile_ref in &profiles_config.profiles {
let reference = ProfileReference::parse(profile_ref);
if lockfile.get_profile(&reference.profile).is_some() {
println!(
"Profile '{}' already installed, skipping",
reference.profile
);
continue;
}
println!("Installing profile '{}'...", reference.profile);
if let Err(e) = install_profile_from_registry(profile_ref, force) {
eprintln!(" Error installing '{}': {e}", reference.profile);
eprintln!(" Continuing with remaining profiles...");
} else {
println!(" Installed '{}'", reference.profile);
}
}
println!();
println!("Sync complete!");
Ok(())
}
pub fn remove_profile(profile_name: &str, verbose: bool) -> ProfileResult<()> {
use lockfile::ProfileLockfile;
let workspace = std::env::current_dir()?;
let lockfile_path = workspace.join(".codanna/profiles.lock.json");
let mut lockfile = ProfileLockfile::load(&lockfile_path)?;
let entry =
lockfile
.get_profile(profile_name)
.ok_or_else(|| error::ProfileError::NotInstalled {
name: profile_name.to_string(),
})?;
if verbose {
println!("Removing profile '{profile_name}'...");
println!(" Files to remove: {}", entry.files.len());
}
let mut removed_count = 0;
let mut failed_removals = Vec::new();
for file_path in &entry.files {
let full_path = workspace.join(file_path);
if verbose {
println!(" Removing: {file_path}");
}
if full_path.exists() {
match std::fs::remove_file(&full_path) {
Ok(_) => {
removed_count += 1;
if let Some(parent) = full_path.parent() {
let _ = std::fs::remove_dir(parent); }
}
Err(e) => {
eprintln!(" Warning: Failed to remove {file_path}: {e}");
failed_removals.push(file_path.clone());
}
}
} else if verbose {
println!(" (file not found, skipping)");
}
}
lockfile.remove_profile(profile_name);
if lockfile.profiles.is_empty() {
if verbose {
println!(" Lockfile is now empty, removing it");
}
std::fs::remove_file(&lockfile_path)?;
println!("\nProfile '{profile_name}' removed successfully");
println!(" Files removed: {removed_count}");
if !failed_removals.is_empty() {
println!(" Failed removals: {}", failed_removals.len());
}
} else {
lockfile.save(&lockfile_path)?;
println!("\nProfile '{profile_name}' removed successfully");
println!(" Files removed: {removed_count}");
if !failed_removals.is_empty() {
println!(" Failed removals: {}", failed_removals.len());
}
if verbose {
println!(" Remaining profiles: {}", lockfile.profiles.len());
}
}
Ok(())
}
pub fn install_profile_from_registry(profile_ref: &str, force: bool) -> ProfileResult<()> {
let workspace = std::env::current_dir()?;
let reference = ProfileReference::parse(profile_ref);
let registry_path = provider_registry_path();
let registry = ProviderRegistry::load(®istry_path)?;
if registry.providers.is_empty() {
return Err(error::ProfileError::InvalidManifest {
reason: "No providers registered. Add a provider first:\n codanna profile provider add <source>".to_string(),
});
}
let (provider_id, provider) = match &reference.provider {
Some(id) => {
let p = registry.get_provider(id).ok_or_else(|| {
error::ProfileError::InvalidManifest {
reason: format!(
"Provider '{id}' not found\nUse 'codanna profile provider list' to see registered providers"
),
}
})?;
(id.as_str(), p)
}
None => {
registry
.find_provider_with_id(&reference.profile)
.ok_or_else(|| error::ProfileError::InvalidManifest {
reason: format!(
"Profile '{}' not found in any registered provider\nUse 'codanna profile provider list --verbose' to see available profiles",
reference.profile
),
})?
}
};
if !provider.profiles.contains_key(&reference.profile) {
return Err(error::ProfileError::InvalidManifest {
reason: format!(
"Profile '{}' not found in provider '{}'\nAvailable profiles: {}",
reference.profile,
provider.name,
provider
.profiles
.keys()
.map(|k| k.as_str())
.collect::<Vec<_>>()
.join(", ")
),
});
}
println!(
"Resolving profile '{}' from provider '{}'...",
reference.profile, provider.name
);
let resolved = resolve_profile_source(&provider.source, &reference.profile)?;
let profile_dir = resolved.profile_dir(&reference.profile);
if !profile_dir.exists() {
return Err(error::ProfileError::InvalidManifest {
reason: format!("Profile directory not found: {}", profile_dir.display()),
});
}
if force {
println!(
"Installing profile '{}' from provider '{}' with --force...",
reference.profile, provider.name
);
} else {
println!(
"Installing profile '{}' from provider '{}'...",
reference.profile, provider.name
);
}
let commit = resolved.commit().map(String::from);
install_profile(
&reference.profile,
profile_dir.parent().unwrap(),
&workspace,
force,
commit,
Some(provider_id),
Some(provider.source.clone()),
)?;
println!("\nProfile '{}' installed successfully", reference.profile);
if force {
println!(" Note: Conflicting files handled with --force");
println!(
" Use 'codanna profile verify {}' to check integrity",
reference.profile
);
}
Ok(())
}
pub fn update_profile(profile_name: &str, force: bool) -> ProfileResult<()> {
let workspace = std::env::current_dir()?;
let lockfile_path = workspace.join(".codanna/profiles.lock.json");
let lockfile = lockfile::ProfileLockfile::load(&lockfile_path)?;
let existing =
lockfile
.get_profile(profile_name)
.ok_or_else(|| error::ProfileError::NotInstalled {
name: profile_name.to_string(),
})?;
let existing_commit =
existing
.commit
.as_ref()
.ok_or_else(|| error::ProfileError::InvalidManifest {
reason: format!(
"Profile '{profile_name}' was installed from local source and cannot be updated"
),
})?;
let registry_path = provider_registry_path();
let registry = ProviderRegistry::load(®istry_path)?;
let provider = registry
.find_provider_for_profile(profile_name)
.ok_or_else(|| error::ProfileError::ProfileNotFoundInAnyProvider {
profile: profile_name.to_string(),
})?;
let repo_url = match &provider.source {
ProviderSource::Github { repo } => format!("https://github.com/{repo}.git"),
ProviderSource::Url { url } => url.clone(),
ProviderSource::Local { .. } => {
return Err(error::ProfileError::InvalidManifest {
reason: "Cannot update profile from local provider".to_string(),
});
}
};
let remote_commit = if force {
None
} else {
Some(git::resolve_reference(&repo_url, "HEAD")?)
};
if !force {
if let Some(ref remote) = remote_commit {
if remote == existing_commit {
match verification::verify_profile(&workspace, profile_name, false) {
Ok(()) => {
println!(
"Profile '{profile_name}' already up to date (commit {})",
&existing_commit[..8]
);
return Ok(());
}
Err(_) => {
println!(
"Profile '{profile_name}' integrity check failed, reinstalling..."
);
}
}
} else {
println!(
"Updating profile '{profile_name}' from {} to {}",
&existing_commit[..8],
&remote[..8]
);
}
}
}
install_profile_from_registry(profile_name, true)?;
if let Some(ref remote) = remote_commit {
println!(
"\nProfile '{profile_name}' updated to commit {}",
&remote[..8]
);
} else {
println!("\nProfile '{profile_name}' updated (force reinstall)");
}
Ok(())
}