use anyhow::{Context, Result};
use std::fs;
use std::path::Path;
use patina::environment::Environment;
use patina::project::{
AdaptersSection, EmbeddingsSection, EnvironmentSection, ProjectConfig, ProjectSection,
RetrievalSection, SearchSection,
};
use patina::version::VersionManifest;
pub fn create_project_config(
project_path: &Path,
name: &str,
environment: &Environment,
) -> Result<()> {
let patina_dir = project_path.join(".patina");
fs::create_dir_all(&patina_dir).context("Failed to create .patina directory")?;
let config_file = patina_dir.join("config.toml");
let existing_config = if config_file.exists() {
patina::project::load(project_path).ok()
} else {
None
};
let detected_tools: Vec<String> = environment
.tools
.iter()
.filter(|(_, info)| info.available)
.map(|(name, _)| name.clone())
.collect();
let config = ProjectConfig {
project: ProjectSection {
name: existing_config
.as_ref()
.map(|c| c.project.name.clone())
.unwrap_or_else(|| name.to_string()),
created: existing_config
.as_ref()
.and_then(|c| c.project.created.clone())
.or_else(|| Some(chrono::Utc::now().to_rfc3339())),
},
dev: Default::default(),
adapters: existing_config
.as_ref()
.map(|c| c.adapters.clone())
.unwrap_or_else(|| AdaptersSection {
allowed: vec![],
default: String::new(),
}),
upstream: existing_config.as_ref().and_then(|c| c.upstream.clone()),
ci: existing_config.as_ref().and_then(|c| c.ci.clone()),
embeddings: EmbeddingsSection {
model: "e5-base-v2".to_string(),
},
search: SearchSection::default(),
retrieval: RetrievalSection::default(),
environment: Some(EnvironmentSection {
os: environment.os.clone(),
arch: environment.arch.clone(),
detected_tools,
}),
};
patina::project::save(project_path, &config)?;
create_oxidize_recipe(&patina_dir)?;
Ok(())
}
fn create_oxidize_recipe(patina_dir: &Path) -> Result<()> {
let recipe_path = patina_dir.join("oxidize.yaml");
if recipe_path.exists() {
return Ok(()); }
let default_recipe = r#"# Oxidize Recipe - Build embeddings and projections
# Run: patina oxidize
version: 1
embedding_model: e5-base-v2
projections:
# Semantic projection - observations from same session are similar
semantic:
layers: [768, 1024, 256]
epochs: 10
batch_size: 32
# Temporal projection - files that co-change are related
temporal:
layers: [768, 1024, 256]
epochs: 10
batch_size: 32
# Dependency projection - functions that call each other are related
dependency:
layers: [768, 1024, 256]
epochs: 10
batch_size: 32
"#;
fs::write(&recipe_path, default_recipe)
.with_context(|| format!("Failed to create oxidize recipe: {}", recipe_path.display()))?;
Ok(())
}
pub fn handle_version_manifest(
project_path: &Path,
is_reinit: bool,
json_output: bool,
) -> Result<Option<Vec<(String, String, String)>>> {
let manifest_path = project_path.join(".patina");
let mut updates_available = Vec::new();
if is_reinit && manifest_path.join("versions.json").exists() {
if !json_output {
println!("🔍 Checking for component updates...");
}
let current_manifest = VersionManifest::load(&manifest_path)?;
let latest_manifest = VersionManifest::new();
for (component, latest_info) in &latest_manifest.components {
if let Some(current_version) = current_manifest.get_component_version(component) {
if current_version != latest_info.version {
updates_available.push((
component.clone(),
current_version.to_string(),
latest_info.version.clone(),
));
}
}
}
if !updates_available.is_empty() && !json_output {
println!("\n📦 Component updates available:");
for (component, current, latest) in &updates_available {
println!(" • {component}: {current} → {latest}");
}
println!();
}
}
let manifest = VersionManifest::new();
manifest.save(project_path)?;
Ok(if updates_available.is_empty() {
None
} else {
Some(updates_available)
})
}