devist 0.1.1

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 std::fs;
use std::path::Path;

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

pub fn run(name: String, target: String, max_lines: usize) -> Result<()> {
    let reg = Registry::load()?;
    let project = reg
        .find(&name)
        .ok_or_else(|| anyhow!("Project not found: {}", name))?;

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

    let resolved = scanner::find_path(&project.path, &target).ok_or_else(|| {
        anyhow!(
            "Could not find `{}` inside project. Try a relative path or filename.",
            target
        )
    })?;

    if resolved.is_dir() {
        explain_directory(&resolved, &project.path, &target)
    } else {
        explain_file(&resolved, &project.path, max_lines)
    }
}

fn explain_file(path: &Path, project_root: &Path, max_lines: usize) -> Result<()> {
    let rel = path
        .strip_prefix(project_root)
        .unwrap_or(path)
        .to_string_lossy()
        .to_string();

    let metadata = fs::metadata(path)?;
    let bytes = metadata.len();
    let language = guess_language(path);

    println!("# File: `{}`", rel);
    println!();
    println!(
        "- **Size:** {} ยท **Language:** {}",
        scanner::human_bytes(bytes),
        language
    );

    if bytes > 1024 * 1024 {
        println!();
        println!(
            "_(file too large to display inline; {})_",
            scanner::human_bytes(bytes)
        );
        return Ok(());
    }

    let content = match fs::read_to_string(path) {
        Ok(c) => c,
        Err(_) => {
            println!();
            println!("_(binary or non-UTF-8 file)_");
            return Ok(());
        }
    };

    let total_lines = content.lines().count();
    println!("- **Lines:** {}", total_lines);
    println!();

    let lang_hint = language_hint(&language);
    println!("```{}", lang_hint);
    let take = max_lines.min(total_lines);
    for (i, line) in content.lines().take(take).enumerate() {
        println!("{:>4}  {}", i + 1, line);
    }
    if total_lines > take {
        println!(
            "... ({} more lines, use --max-lines to see more)",
            total_lines - take
        );
    }
    println!("```");
    Ok(())
}

fn explain_directory(dir: &Path, project_root: &Path, query: &str) -> Result<()> {
    let rel = dir
        .strip_prefix(project_root)
        .unwrap_or(dir)
        .to_string_lossy()
        .to_string();

    println!(
        "# Directory: `{}`",
        if rel.is_empty() {
            query.to_string()
        } else {
            rel
        }
    );
    println!();

    // Sub-tree
    println!("## Tree");
    println!();
    println!("```");
    let lines = scanner::build_tree(dir, 4, 200);
    let display = dir
        .file_name()
        .map(|n| n.to_string_lossy().to_string())
        .unwrap_or_else(|| query.to_string());
    println!("{}/", display);
    for line in lines {
        println!("{}", line);
    }
    println!("```");
    println!();

    // List immediate children with their type
    println!("## Immediate Children");
    println!();
    let mut entries: Vec<_> = fs::read_dir(dir)?.flatten().collect();
    entries.sort_by_key(|e| e.file_name());
    for entry in entries {
        let name = entry.file_name().to_string_lossy().to_string();
        if name.starts_with('.') && name != ".env.example" {
            continue;
        }
        let kind = if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
            "dir"
        } else {
            "file"
        };
        println!("- `{}` ({})", name, kind);
    }
    Ok(())
}

fn guess_language(path: &Path) -> String {
    // Reuse scanner logic via a tiny re-detect
    let ext = path
        .extension()
        .and_then(|e| e.to_str())
        .unwrap_or("")
        .to_lowercase();
    match ext.as_str() {
        "rs" => "Rust",
        "py" => "Python",
        "ts" | "tsx" => "TypeScript",
        "js" | "jsx" | "mjs" | "cjs" => "JavaScript",
        "dart" => "Dart",
        "go" => "Go",
        "kt" => "Kotlin",
        "swift" => "Swift",
        "html" => "HTML",
        "css" | "scss" => "CSS",
        "md" => "Markdown",
        "json" => "JSON",
        "yaml" | "yml" => "YAML",
        "toml" => "TOML",
        "sh" | "bash" => "Shell",
        "tmpl" => "Template",
        _ => "Other",
    }
    .to_string()
}

fn language_hint(lang: &str) -> &'static str {
    match lang {
        "Rust" => "rust",
        "Python" => "python",
        "TypeScript" => "ts",
        "JavaScript" => "js",
        "Dart" => "dart",
        "Go" => "go",
        "Kotlin" => "kotlin",
        "Swift" => "swift",
        "HTML" => "html",
        "CSS" => "css",
        "Markdown" => "md",
        "JSON" => "json",
        "YAML" => "yaml",
        "TOML" => "toml",
        "Shell" => "bash",
        _ => "",
    }
}