pub mod config;
pub mod patterns;
pub mod validation;
use anyhow::{Context, Result};
use std::fs;
use std::path::{Path, PathBuf};
use patina::environment::Environment;
use patina::layer::Layer;
use self::config::{create_project_config, handle_version_manifest};
use self::patterns::copy_core_patterns_safe;
use self::validation::validate_environment;
use super::design_wizard::confirm;
pub fn execute_init(name: String, force: bool, local: bool, no_commit: bool) -> Result<()> {
let json_output = false;
check_hierarchy_conflicts(force)?;
println!("🎨 Initializing Patina...\n");
if !local {
check_gh_cli_available()?;
}
ensure_git_initialized()?;
let is_reinit_early = name == "." && Path::new(".patina").exists();
if is_reinit_early {
println!("🔄 Re-initializing existing Patina project...\n");
} else {
patina::git::ensure_fork(local)?;
println!();
patina::git::ensure_patina_branch(force)?;
println!("✓ On branch 'patina'\n");
}
ensure_gitignore(Path::new("."))?;
if name != "." && Path::new(".patina").exists() {
println!("⚠️ You're already in a Patina project!");
println!(
" Running 'patina init {}' would create: {}",
name,
std::env::current_dir()?.join(&name).display()
);
if !confirm("Continue anyway?")? {
println!("Initialization cancelled.");
return Ok(());
}
}
let is_reinit = if name == "." {
is_reinit_early
} else {
let path = PathBuf::from(&name);
path.exists() && path.join(".patina").exists()
};
if !is_reinit_early {
if is_reinit {
println!("🔄 Re-initializing Patina project...");
} else {
println!("🎨 Initializing Patina project: {name}");
}
}
println!("🔍 Detecting environment...");
let environment = Environment::detect()?;
display_environment_info(&environment);
let project_path = setup_project_path(&name)?;
let project_name = if name == "." {
project_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("project")
.to_string()
} else {
name.clone()
};
write_environment_toml(&project_path, &environment)?;
let layer_path = project_path.join("layer");
let layer = Layer::new(&layer_path);
layer
.init()
.context("Failed to initialize layer structure")?;
println!(" ✓ Created layer structure");
let uid = patina::project::create_uid_if_missing(&project_path)?;
if !is_reinit {
println!(" ✓ Created project UID: {}", uid);
}
create_project_config(&project_path, &name, &environment)?;
handle_version_manifest(&project_path, is_reinit, json_output)?;
let patterns_copied = copy_core_patterns_safe(&project_path, &layer_path)?;
if patterns_copied {
println!(" ✓ Copied core patterns from Patina");
}
create_init_session(&layer_path, &project_name)?;
initialize_navigation(&project_path)?;
if let Some(warnings) = validate_environment(&environment)? {
println!("\n⚠️ Environment warnings:");
for warning in warnings {
println!(" {warning}");
}
}
if !no_commit && name == "." {
println!("\n📦 Committing Patina setup...");
patina::git::add_paths(&[
".gitignore",
".patina/config.toml",
".patina/uid",
".patina/oxidize.yaml",
".patina/versions.json",
"layer",
])?;
let commit_msg = if is_reinit {
"chore: update Patina configuration"
} else {
"chore: initialize Patina project"
};
patina::git::commit(commit_msg)?;
println!("✓ Committed Patina initialization");
}
suggest_missing_tools(&environment)?;
println!("\n✨ Project '{name}' initialized successfully!");
println!(" Add an adapter: patina adapter add <claude|gemini|opencode>");
Ok(())
}
fn display_environment_info(environment: &Environment) {
println!(" ✓ OS: {} ({})", environment.os, environment.arch);
for (tool, info) in &environment.tools {
if info.available {
println!(
" ✓ {}: {}",
tool,
info.version.as_ref().unwrap_or(&"detected".to_string())
);
}
}
}
fn write_environment_toml(project_path: &Path, environment: &Environment) -> Result<()> {
let toml_path = project_path.join("ENVIRONMENT.toml");
let content =
toml::to_string_pretty(environment).context("Failed to serialize environment data")?;
fs::write(&toml_path, content).context("Failed to write ENVIRONMENT.toml")?;
println!(" ✓ Created ENVIRONMENT.toml with full environment data");
Ok(())
}
fn setup_project_path(name: &str) -> Result<PathBuf> {
let path = if name == "." {
std::env::current_dir()?
} else {
let path = PathBuf::from(name);
if !path.exists() {
fs::create_dir_all(&path)?;
}
path.canonicalize()?
};
Ok(path)
}
fn create_init_session(layer_path: &Path, name: &str) -> Result<()> {
let session_filename = format!("{}-init.md", chrono::Utc::now().format("%Y%m%d-%H%M%S"));
let session_content = format!(
"# {} Initialization\n\nInitialized on: {}\n\nNote: Run 'patina adapter add <name>' to add LLM support.\n",
name,
chrono::Utc::now().to_rfc3339(),
);
let sessions_path = layer_path.join("sessions");
fs::create_dir_all(&sessions_path)?;
fs::write(sessions_path.join(session_filename), session_content)?;
Ok(())
}
fn initialize_navigation(project_path: &Path) -> Result<()> {
if project_path.join("layer").exists() {
println!("🔍 Reindexing patterns for navigation...");
println!(" Indexing patterns... ✓ (0 patterns indexed)");
} else {
println!("🔍 Initializing navigation database...");
println!(" ✓ Created navigation database");
println!(" Indexing patterns... ✓ (0 patterns indexed)");
}
Ok(())
}
fn suggest_missing_tools(environment: &Environment) -> Result<()> {
use crate::commands::init::tool_installer;
let available_tools = tool_installer::get_available_tools();
let missing: Vec<_> = available_tools
.iter()
.filter(|tool| {
!environment
.tools
.get(tool.name)
.map(|info| info.available)
.unwrap_or(false)
})
.collect();
if !missing.is_empty() {
println!("\n💡 Missing optional tools that can enhance your Patina experience:");
for tool in &missing {
println!(" - {}", tool.name);
}
println!("\n Run 'patina init --install-tools' to install them automatically");
println!(" (Note: --install-tools flag is not yet implemented)");
}
Ok(())
}
fn check_gh_cli_available() -> Result<()> {
use std::process::Command;
let output = Command::new("gh").arg("--version").output();
match output {
Ok(output) if output.status.success() => Ok(()),
_ => {
eprintln!("Error: GitHub CLI (gh) is required but not found.");
eprintln!();
eprintln!("Please install the GitHub CLI:");
eprintln!(" • macOS: brew install gh");
eprintln!(" • Linux: See https://cli.github.com/manual/installation");
eprintln!(" • Windows: winget install GitHub.cli");
eprintln!();
eprintln!("Or use --local flag to skip GitHub integration.");
anyhow::bail!("GitHub CLI (gh) not found")
}
}
}
fn ensure_git_initialized() -> Result<()> {
use std::process::Command;
let output = Command::new("git")
.args(["rev-parse", "--git-dir"])
.output()
.context("Failed to check git status")?;
if !output.status.success() {
println!("📝 No git repository found. Initializing...");
let output = Command::new("git")
.arg("init")
.output()
.context("Failed to initialize git repository")?;
if !output.status.success() {
anyhow::bail!("Failed to initialize git repository");
}
println!("✓ Initialized git repository");
}
Ok(())
}
pub fn ensure_gitignore(project_path: &Path) -> Result<()> {
let gitignore_path = project_path.join(".gitignore");
if !gitignore_path.exists() {
create_default_gitignore(&gitignore_path)?;
} else {
ensure_gitignore_entries(&gitignore_path)?;
}
Ok(())
}
fn create_default_gitignore(gitignore_path: &Path) -> Result<()> {
let content = r#"# Build artifacts
/target/
**/*.rs.bk
Cargo.lock
# Environment and secrets
.env
.env.*
*.pem
*.key
credentials.json
secrets.toml
# Dependencies
node_modules/
vendor/
venv/
__pycache__/
*.pyc
# Build outputs
dist/
build/
*.o
*.so
*.dylib
*.dll
*.exe
# IDE and editor files
.idea/
.vscode/
*.iml
*.swp
*.swo
*~
.DS_Store
# Patina local state (derived, not committed)
.patina/local/
ENVIRONMENT.toml
# Temporary files
*.tmp
*.bak
*.backup
*.old
# Database files
*.db
*.db-shm
*.db-wal
*.sqlite
*.sqlite3
# Logs
*.log
logs/
"#;
fs::write(gitignore_path, content).context("Failed to create .gitignore")?;
println!("✓ Created .gitignore with standard patterns");
Ok(())
}
const CHILD_PROJECT_SEARCH_DEPTH: usize = 6;
fn check_hierarchy_conflicts(force: bool) -> Result<()> {
let current_dir = std::env::current_dir()?;
let mut parent = current_dir.parent();
let mut conflicting_parents = Vec::new();
while let Some(p) = parent {
let claude_dir = p.join(".claude");
let commands_dir = claude_dir.join("commands");
if commands_dir.exists() {
conflicting_parents.push(p.to_path_buf());
}
parent = p.parent();
}
if !conflicting_parents.is_empty() {
eprintln!("Error: Found .claude/commands/ in parent directory:");
for p in &conflicting_parents {
eprintln!(" → {}", p.display());
}
eprintln!();
eprintln!("Claude Code walks up the directory tree and loads commands from each");
eprintln!(".claude/commands/ it finds. This would cause duplicate slash commands.");
eprintln!();
eprintln!("To fix:");
eprintln!(
" 1. Remove the parent .claude/: rm -rf {}/.claude",
conflicting_parents[0].display()
);
eprintln!(" 2. Or use --force to ignore this check (not recommended)");
if !force {
anyhow::bail!("Hierarchy conflict: parent directory has .claude/commands/");
}
eprintln!();
eprintln!("⚠️ Proceeding anyway due to --force flag...");
}
let child_projects = find_child_patina_projects(¤t_dir)?;
if !child_projects.is_empty() {
eprintln!(
"Error: Found Patina project(s) in subdirectories (checked {} levels):",
CHILD_PROJECT_SEARCH_DEPTH
);
for p in &child_projects {
eprintln!(" → {}", p.display());
}
eprintln!();
eprintln!("Initializing here would create a parent project over existing ones,");
eprintln!("causing duplicate commands and configuration conflicts.");
eprintln!();
eprintln!("To fix:");
eprintln!(" 1. Initialize in a different directory");
eprintln!(" 2. Or use --force to ignore this check (not recommended)");
if !force {
anyhow::bail!("Hierarchy conflict: child directories contain Patina projects");
}
eprintln!();
eprintln!("⚠️ Proceeding anyway due to --force flag...");
}
Ok(())
}
fn find_child_patina_projects(dir: &Path) -> Result<Vec<PathBuf>> {
use ignore::WalkBuilder;
let walker = WalkBuilder::new(dir)
.max_depth(Some(CHILD_PROJECT_SEARCH_DEPTH))
.hidden(false) .git_ignore(true) .build();
for entry in walker {
let entry = match entry {
Ok(e) => e,
Err(_) => continue, };
let path = entry.path();
if path == dir {
continue;
}
let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if file_name == ".patina"
|| (file_name == "commands"
&& path
.parent()
.map(|p| p.ends_with(".claude"))
.unwrap_or(false))
{
if let Some(project_dir) = path.parent() {
let project_dir = if file_name == "commands" {
project_dir.parent().unwrap_or(project_dir)
} else {
project_dir
};
if project_dir == dir {
continue;
}
return Ok(vec![project_dir.to_path_buf()]);
}
}
}
Ok(vec![])
}
fn ensure_gitignore_entries(gitignore_path: &Path) -> Result<()> {
let content = fs::read_to_string(gitignore_path).context("Failed to read .gitignore")?;
let must_have = [
("/target/", "Rust build artifacts"),
("node_modules/", "Node.js dependencies"),
(".env", "Environment secrets"),
(
".patina/local/",
"Patina local state (derived, not committed)",
),
("*.db", "Database files"),
("*.key", "Private keys"),
("*.pem", "Certificates"),
];
let mut added = Vec::new();
let mut updated_content = content.clone();
for (pattern, _description) in must_have {
let pattern_exists = content.lines().any(|line| {
let line = line.trim();
line == pattern || line == pattern.trim_end_matches('/')
});
if !pattern_exists {
if !updated_content.ends_with('\n') {
updated_content.push('\n');
}
if added.is_empty() {
updated_content.push_str("\n# Added by Patina for safety\n");
}
updated_content.push_str(pattern);
updated_content.push('\n');
added.push(pattern);
}
}
if !added.is_empty() {
fs::write(gitignore_path, updated_content).context("Failed to update .gitignore")?;
println!("✓ Added to .gitignore: {}", added.join(", "));
}
Ok(())
}