solverforge-cli 2.0.1

CLI for scaffolding and managing SolverForge constraint solver projects
use std::fs;
use std::path::Path;

use crate::commands::generate_constraint::parse_domain;
use crate::error::{CliError, CliResult};
use crate::output;

pub fn run() -> CliResult {
    let domain_dir = Path::new("src/domain");
    if !domain_dir.exists() {
        return Err(CliError::NotInProject {
            missing: "src/domain/",
        });
    }

    // Read project name from Cargo.toml
    let project_name = read_project_name().unwrap_or_else(|| "<unknown>".to_string());
    output::print_heading(&format!("Project: {}", project_name));
    println!();

    let domain = parse_domain().map_err(CliError::general)?;
    println!("  Solution:    {}", domain.solution_type);
    println!("  Score type:  {}", domain.score_type);
    println!();

    if !domain.entities.is_empty() {
        println!("  Entities:");
        for entity in &domain.entities {
            let mut solvable_fields = Vec::new();
            solvable_fields.extend(
                entity
                    .scalar_vars
                    .iter()
                    .map(|field| format!("{} [scalar]", field.field)),
            );
            solvable_fields.extend(
                entity
                    .list_vars
                    .iter()
                    .map(|field| format!("{} [list]", field.field)),
            );

            if solvable_fields.is_empty() {
                println!("    - {}", entity.item_type);
            } else {
                println!(
                    "    - {} (solvable fields: {})",
                    entity.item_type,
                    solvable_fields.join(", ")
                );
            }
        }
        println!();
    }

    if !domain.facts.is_empty() {
        println!("  Facts:");
        for fact in &domain.facts {
            println!("    - {}", fact.item_type);
        }
        println!();
    }

    // List constraints
    let constraints_dir = Path::new("src/constraints");
    if constraints_dir.exists() {
        let constraints = list_constraints(constraints_dir);
        if !constraints.is_empty() {
            println!("  Constraints:");
            for c in &constraints {
                println!("    - {}", c);
            }
            println!();
        }
    }

    // Show solver.toml summary
    if Path::new("solver.toml").exists() {
        println!("  Config:      solver.toml");
    }

    Ok(())
}

fn read_project_name() -> Option<String> {
    let cargo = fs::read_to_string("Cargo.toml").ok()?;
    for line in cargo.lines() {
        let trimmed = line.trim();
        if trimmed.starts_with("name") && trimmed.contains('=') {
            let val = trimmed.split('=').nth(1)?.trim();
            return Some(val.trim_matches('"').to_string());
        }
    }
    None
}

fn list_constraints(dir: &Path) -> Vec<String> {
    let mut constraints = Vec::new();
    if let Ok(entries) = fs::read_dir(dir) {
        for entry in entries.flatten() {
            let path = entry.path();
            if path.extension().and_then(|e| e.to_str()) == Some("rs") {
                let name = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
                if name != "mod" {
                    constraints.push(name.to_string());
                }
            }
        }
    }
    constraints.sort();
    constraints
}