main.rs 143 lignes · 4698 octets
//! `screen-select` : génère/complète le plan des écrans à coder pour une UI
//! custom consommant rustwarden-core, et recoche le réalisé (`--check`).

mod catalogue;
mod check;
mod generate;
mod section;
mod security;
mod todo;

use std::io::{self, Write};
use std::path::Path;

const PLAN_PATH: &str = "docs/plans/ui-screens.md";

fn main() -> io::Result<()> {
    if std::env::args().any(|a| a == "--check") {
        run_check()
    } else {
        run_wizard()
    }
}

/// Lit une ligne au clavier (chaîne vide si EOF).
fn prompt(msg: &str) -> io::Result<String> {
    print!("{msg}");
    io::stdout().flush()?;
    let mut line = String::new();
    io::stdin().read_line(&mut line)?;
    Ok(line.trim().to_string())
}

fn run_wizard() -> io::Result<()> {
    println!("== screen-select : écrans à coder ==\n");

    // 1. Sélection des fonctionnalités (les "always" sont incluses d'office).
    let choosable: Vec<&catalogue::Functionality> =
        catalogue::all().iter().filter(|f| !f.always).collect();
    println!("Fonctionnalités activées ? (numéros séparés par des virgules)\n");
    for (i, f) in choosable.iter().enumerate() {
        println!("  {:>2}. {}", i + 1, f.label);
    }
    let raw = prompt("\nSélection : ")?;
    let picked: Vec<usize> = raw
        .split(',')
        .filter_map(|s| s.trim().parse::<usize>().ok())
        .filter(|n| *n >= 1 && *n <= choosable.len())
        .map(|n| n - 1)
        .collect();

    let mut selected: Vec<&catalogue::Functionality> =
        catalogue::all().iter().filter(|f| f.always).collect();
    for i in picked {
        selected.push(choosable[i]);
    }

    // 2. Entrées optionnelles.
    let design_path = prompt("Chemin d'un design.md (charte graphique, vide si aucun) : ")?;
    let design_md = if design_path.is_empty() {
        None
    } else {
        Some(std::fs::read_to_string(&design_path)?)
    };
    let layout = prompt("Structure de page (ex. 'navbar: gauche; 2 colonnes', vide si aucune) : ")?;
    let layout = if layout.is_empty() {
        None
    } else {
        Some(layout)
    };

    let project = std::env::current_dir()?
        .file_name()
        .map(|n| n.to_string_lossy().into_owned())
        .unwrap_or_else(|| "projet".into());

    let inputs = generate::PlanInputs {
        project: &project,
        selected,
        design_md,
        layout,
    };
    let desired = generate::rows_for(&inputs.selected);

    // 3. Fusion si un plan existe déjà (préserve statuts + routes éditées).
    let path = Path::new(PLAN_PATH);
    let content = if path.exists() {
        let existing = std::fs::read_to_string(path)?;
        let prev = todo::parse(&existing);
        let merged = todo::merge(&prev, &desired);
        // conserve l'en-tête existant, remplace le tableau…
        let mut c = todo::replace_table(&existing, &merged);
        // …puis régénère les sections sécurité (idempotent, suit la sélection courante).
        c = section::upsert(&c, security::GUIDES_ID, &security::guides_body());
        c = section::upsert(
            &c,
            security::PER_SCREEN_ID,
            &security::reference_body(&inputs.selected),
        );
        c
    } else {
        generate::render_plan(&inputs, &desired)
    };

    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    std::fs::write(path, content)?;
    println!(
        "\n✓ Plan écrit dans {PLAN_PATH} ({} écrans).",
        desired.len()
    );
    println!(
        "  Contrôlez le réalisé : cargo run -p rustwarden-core --bin screen-select -- --check"
    );
    Ok(())
}

fn run_check() -> io::Result<()> {
    let path = Path::new(PLAN_PATH);
    if !path.exists() {
        eprintln!("Aucun plan à {PLAN_PATH} — lancez d'abord `screen-select` sans --check.");
        std::process::exit(1);
    }
    let content = std::fs::read_to_string(path)?;
    let mut rows = todo::parse(&content);
    let haystack = check::collect_sources(&std::env::current_dir()?);
    check::mark(&mut rows, &haystack);
    let updated = todo::replace_table(&content, &rows);
    std::fs::write(path, updated)?;

    let done = rows.iter().filter(|r| r.done).count();
    let missing: Vec<&todo::TodoRow> = rows.iter().filter(|r| !r.done).collect();
    println!("Couverture : {}/{} écrans réalisés.", done, rows.len());
    if missing.is_empty() {
        println!("✓ Tous les écrans des fonctionnalités activées sont câblés.");
    } else {
        println!("Écrans manquants :");
        for r in missing {
            println!("  ☐ {} — {} ({})", r.functionality, r.screen, r.route);
        }
    }
    Ok(())
}