rft-cli 0.5.1

Zero-config Docker Compose isolation for git worktrees
use owo_colors::OwoColorize;

use crate::compose::{detect_compose_file, parse_compose_file};
use crate::error::Result;
use crate::git::resolve_repo_identity;
use crate::ports::extract_port_mappings;

pub async fn run() -> Result<()> {
    let cwd = std::env::current_dir()?;
    let identity = resolve_repo_identity(&cwd).await?;
    let repo_root = identity.working_root;
    let repo_name = identity.project_name;

    println!(
        "{} {} ({})",
        "Initializing rft for".green().bold(),
        repo_name.bold(),
        repo_root.display()
    );

    let compose_path = detect_compose_file(&repo_root);

    let compose_path = match compose_path {
        Some(path) => {
            println!(
                "  {} Found {}",
                "".green(),
                path.strip_prefix(&repo_root).unwrap_or(&path).display()
            );
            path
        }
        None => {
            eprintln!(
                "  {} No compose file found (compose.yaml, docker-compose.yml, etc.)",
                "".red()
            );
            eprintln!(
                "\n{}",
                "Create a docker-compose.yml first, then run rft init again.".dimmed()
            );
            return Ok(());
        }
    };

    let compose_file = tokio::task::spawn_blocking({
        let path = compose_path.clone();
        move || parse_compose_file(&path)
    })
    .await
    .map_err(|e| crate::error::RftError::TaskPanicked(e.to_string()))??;

    let port_mappings = extract_port_mappings(&compose_file.services);

    let managed_count = port_mappings.iter().filter(|m| m.env_var.is_some()).count();
    let raw_count = port_mappings.iter().filter(|m| m.env_var.is_none()).count();

    println!(
        "  {} {} service(s), {} port mapping(s)",
        "".green(),
        compose_file.services.len(),
        port_mappings.len()
    );

    if managed_count > 0 {
        println!(
            "  {} {} port(s) using ${{VAR:-default}} format (managed by rft)",
            "".green(),
            managed_count
        );
    }

    if raw_count > 0 {
        println!(
            "\n{}",
            "The following ports use hardcoded values. rft cannot override them:"
                .yellow()
                .bold()
        );
        for mapping in port_mappings.iter().filter(|m| m.env_var.is_none()) {
            let suggested =
                crate::ports::suggest_env_var(&mapping.service_name, mapping.default_port);
            println!(
                "  {} {} port {} → change to ${{{}:-{}}}:{}",
                "".yellow(),
                mapping.service_name,
                mapping.raw,
                suggested,
                mapping.default_port,
                mapping.container_port
            );
        }
    }

    let config_path = repo_root.join(".rftrc.toml");
    if config_path.exists() {
        println!("\n  {} .rftrc.toml already exists", "".green());
    } else if managed_count > 0 {
        tokio::fs::write(&config_path, "# rft configuration\n# See: https://github.com/supostat/rft-cli\n\n# sync = []\n# port_offset = 20000\n# main_branch = \"main\"\n").await?;
        println!("\n  {} Created .rftrc.toml", "".green());
    }

    ensure_gitignore_has_local_config(&repo_root).await;

    if managed_count == 0 && raw_count > 0 {
        println!(
            "\n{}",
            "No ports use ${VAR:-default} format. Fix the ports above, then run rft init again."
                .yellow()
        );
    } else {
        println!(
            "\n{} Run {} to see your worktrees.",
            "Ready!".green().bold(),
            "rft list".bold()
        );
    }

    Ok(())
}

async fn ensure_gitignore_has_local_config(repo_root: &std::path::Path) {
    let gitignore_path = repo_root.join(".gitignore");
    let content = tokio::fs::read_to_string(&gitignore_path)
        .await
        .unwrap_or_default();

    if content.contains(".rftrc.local.toml") {
        return;
    }

    let entry = if content.is_empty() || content.ends_with('\n') {
        ".rftrc.local.toml\n"
    } else {
        "\n.rftrc.local.toml\n"
    };

    match tokio::fs::write(&gitignore_path, format!("{content}{entry}")).await {
        Ok(()) => {
            println!("  {} Added .rftrc.local.toml to .gitignore", "".green());
        }
        Err(error) => {
            eprintln!("warning: could not update .gitignore: {error}");
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::ports::suggest_env_var;

    #[test]
    fn suggest_env_var_for_common_services() {
        assert_eq!(suggest_env_var("frontend", 3000), "FRONTEND_PORT_3000");
        assert_eq!(suggest_env_var("api", 8080), "API_PORT_8080");
        assert_eq!(suggest_env_var("postgres", 5432), "POSTGRES_PORT_5432");
    }
}