devist 0.2.0

Project bootstrap CLI for AI-assisted development. Spin up new projects from templates, manage backends, and keep your codebase comprehensible.
use anyhow::{anyhow, Result};
use console::style;
use std::path::Path;

use crate::git;
use crate::registry::Registry;
use crate::scanner;

pub fn run(name: String, json: bool) -> Result<()> {
    let reg = Registry::load()?;
    let project = reg
        .find(&name)
        .ok_or_else(|| anyhow!("Project not found: {}. Run `devist project list`.", name))?;

    if !project.path.exists() {
        return Err(anyhow!(
            "Project directory missing: {}",
            project.path.display()
        ));
    }

    if json {
        return run_json(&project.path, &name);
    }

    print_markdown_brief(&project.path, &name, &project.template)
}

fn run_json(path: &Path, name: &str) -> Result<()> {
    let report = scanner::scan(path)?;
    let log = git::recent_log(path, 10).unwrap_or_default();
    let status = git::status_porcelain(path).unwrap_or_default();
    let branch = git::current_branch(path).unwrap_or_else(|_| "(no git)".to_string());

    let payload = serde_json::json!({
        "name": name,
        "scan": report,
        "git": {
            "branch": branch,
            "recent_commits": log,
            "uncommitted": status,
        }
    });
    println!("{}", serde_json::to_string_pretty(&payload)?);
    Ok(())
}

fn print_markdown_brief(path: &Path, name: &str, template: &str) -> Result<()> {
    let report = scanner::scan(path)?;
    let is_git = git::is_repo(path);

    println!("# Project Brief: {}", name);
    println!();
    println!("- **Path:** `{}`", path.display());
    println!("- **Template:** {}", template);
    println!(
        "- **Files:** {} ({} lines, {})",
        report.total_files,
        report.total_lines,
        scanner::human_bytes(report.total_bytes)
    );
    println!();

    // Languages
    println!("## Languages");
    println!();
    let mut langs: Vec<_> = report
        .languages
        .iter()
        .filter(|(_, s)| s.lines > 0 || s.files > 0)
        .collect();
    langs.sort_by_key(|(_, s)| std::cmp::Reverse(s.lines));
    if langs.is_empty() {
        println!("_(no source files detected)_");
    } else {
        println!("| Language | Files | Lines | Size |");
        println!("|---|---:|---:|---:|");
        for (name, stats) in langs.iter().take(10) {
            println!(
                "| {} | {} | {} | {} |",
                name,
                stats.files,
                stats.lines,
                scanner::human_bytes(stats.bytes)
            );
        }
    }
    println!();

    // Entry points
    if !report.entry_points.is_empty() {
        println!("## Entry Points");
        println!();
        for ep in &report.entry_points {
            println!("- `{}`", ep);
        }
        println!();
    }

    // Config files
    if !report.config_files.is_empty() {
        println!("## Configuration Files");
        println!();
        for cf in &report.config_files {
            println!("- `{}`", cf);
        }
        println!();
    }

    // Top files by size
    if !report.top_files_by_size.is_empty() {
        println!("## Largest Files");
        println!();
        println!("| File | Lines | Size |");
        println!("|---|---:|---:|");
        for f in &report.top_files_by_size {
            println!(
                "| `{}` | {} | {} |",
                f.path,
                f.lines,
                scanner::human_bytes(f.bytes)
            );
        }
        println!();
    }

    // Git
    if is_git {
        let branch = git::current_branch(path).unwrap_or_else(|_| "?".to_string());
        println!("## Git State");
        println!();
        println!("- **Branch:** `{}`", branch);

        let status = git::status_porcelain(path).unwrap_or_default();
        if status.is_empty() {
            println!("- **Working tree:** clean");
        } else {
            println!("- **Working tree:** {} uncommitted change(s)", status.len());
            println!();
            println!("```");
            for line in status.iter().take(20) {
                println!("{}", line);
            }
            if status.len() > 20 {
                println!("... ({} more)", status.len() - 20);
            }
            println!("```");
        }
        println!();

        let log = git::recent_log(path, 10).unwrap_or_default();
        if !log.is_empty() {
            println!("### Recent Commits");
            println!();
            println!("```");
            for line in &log {
                println!("{}", line);
            }
            println!("```");
            println!();
        }

        let hot = git::changed_files_recent(path, 30).unwrap_or_default();
        if !hot.is_empty() {
            println!("### Hot Files (last 30 commits)");
            println!();
            println!("| File | Commits |");
            println!("|---|---:|");
            for (file, count) in hot.iter().take(10) {
                println!("| `{}` | {} |", file, count);
            }
            println!();
        }
    } else {
        println!("## Git State");
        println!();
        println!("_(not a git repository)_");
        println!();
    }

    println!(
        "{}",
        style("Tip: pipe to `claude` or save to file with `> brief.md`").dim()
    );
    Ok(())
}