peeker 1.1.0

A CLI tool for extracting code structure using Tree-sitter
use anyhow::{Context, Result};
use clap::Parser;
use std::path::PathBuf;
use termimad::MadSkin;

mod languages;
mod mcp;
mod parser;

use parser::CodeStructure;

#[derive(Parser, Debug)]
#[command(name = "peeker")]
#[command(about = "Peek into source code structure using tree-sitter")]
#[command(version)]
enum Args {
    /// Analyze a source file
    #[command(name = "peek", visible_alias = "p")]
    Peek {
        /// Path to the source file to analyze
        file: PathBuf,

        /// Show only public/exported items
        #[arg(long)]
        exports_only: bool,

        /// Output format (pretty, json, markdown)
        #[arg(long, short, default_value = "pretty")]
        format: OutputFormat,
    },

    /// Run as an MCP server (JSON-RPC over stdio)
    Mcp,
}

#[derive(Debug, Clone, Copy, Default, clap::ValueEnum)]
enum OutputFormat {
    #[default]
    Pretty,
    Json,
    Markdown,
}

fn main() -> Result<()> {
    let args = Args::parse();

    match args {
        Args::Mcp => mcp::run_mcp_server(),
        Args::Peek {
            file,
            exports_only,
            format,
        } => {
            let source = std::fs::read_to_string(&file)
                .with_context(|| format!("Failed to read file: {}", file.display()))?;

            let extension = file.extension().and_then(|e| e.to_str()).unwrap_or("");

            let structure = parser::parse_file(&source, extension)?;

            let filtered = if exports_only {
                structure.exports_only()
            } else {
                structure
            };

            match format {
                OutputFormat::Pretty => print_pretty(&filtered, &file),
                OutputFormat::Json => print_json(&filtered)?,
                OutputFormat::Markdown => print_markdown(&filtered, &file),
            }

            Ok(())
        }
    }
}

fn print_pretty(structure: &CodeStructure, file: &std::path::Path) {
    let skin = MadSkin::default();

    println!();
    skin.print_text(&format!("# {}\n", file.display()));

    if !structure.imports.is_empty() {
        skin.print_text("## Imports\n");
        for import in &structure.imports {
            println!("  {} (line {})", import.name, import.line);
        }
        println!();
    }

    if !structure.structs.is_empty() {
        skin.print_text("## Structs/Classes\n");
        for s in &structure.structs {
            let visibility = if s.is_public { "pub " } else { "" };
            println!(
                "  {}{} (lines {}-{})",
                visibility, s.name, s.start_line, s.end_line
            );
            for field in &s.fields {
                let field_vis = if field.is_public { "pub " } else { "" };
                println!("    {}{}: {}", field_vis, field.name, field.type_name);
            }
        }
        println!();
    }

    if !structure.functions.is_empty() {
        skin.print_text("## Functions\n");
        for f in &structure.functions {
            let visibility = if f.is_public { "pub " } else { "" };
            let async_kw = if f.is_async { "async " } else { "" };
            println!(
                "  {}{}fn {}({}) -> {} (lines {}-{})",
                visibility,
                async_kw,
                f.name,
                f.params.join(", "),
                f.return_type.as_deref().unwrap_or("()"),
                f.start_line,
                f.end_line
            );
        }
        println!();
    }

    if !structure.traits.is_empty() {
        skin.print_text("## Traits/Interfaces\n");
        for t in &structure.traits {
            let visibility = if t.is_public { "pub " } else { "" };
            println!(
                "  {}trait {} (lines {}-{})",
                visibility, t.name, t.start_line, t.end_line
            );
        }
        println!();
    }

    if !structure.enums.is_empty() {
        skin.print_text("## Enums\n");
        for e in &structure.enums {
            let visibility = if e.is_public { "pub " } else { "" };
            println!(
                "  {}enum {} (lines {}-{})",
                visibility, e.name, e.start_line, e.end_line
            );
            for variant in &e.variants {
                println!("    {}", variant);
            }
        }
        println!();
    }
}

fn print_json(structure: &CodeStructure) -> Result<()> {
    let json = serde_json::to_string_pretty(structure)?;
    println!("{}", json);
    Ok(())
}

fn print_markdown(structure: &CodeStructure, file: &std::path::Path) {
    println!("# {}\n", file.display());

    if !structure.imports.is_empty() {
        println!("## Imports\n");
        for import in &structure.imports {
            println!("- `{}` (line {})", import.name, import.line);
        }
        println!();
    }

    if !structure.structs.is_empty() {
        println!("## Structs/Classes\n");
        for s in &structure.structs {
            let visibility = if s.is_public { "pub " } else { "" };
            println!(
                "### {}`{}`\n",
                if s.is_public { "🔓 " } else { "🔒 " },
                format!("{}{}", visibility, s.name)
            );
            println!("Lines {}-{}\n", s.start_line, s.end_line);
            if !s.fields.is_empty() {
                println!("| Field | Type | Visibility |");
                println!("|-------|------|------------|");
                for field in &s.fields {
                    let vis = if field.is_public { "pub" } else { "private" };
                    println!("| `{}` | `{}` | {} |", field.name, field.type_name, vis);
                }
                println!();
            }
        }
    }

    if !structure.functions.is_empty() {
        println!("## Functions\n");
        for f in &structure.functions {
            let visibility = if f.is_public { "pub " } else { "" };
            let async_kw = if f.is_async { "async " } else { "" };
            println!(
                "### {}`{}{}fn {}`\n",
                if f.is_public { "🔓 " } else { "🔒 " },
                visibility,
                async_kw,
                f.name
            );
            println!("Lines {}-{}\n", f.start_line, f.end_line);
            println!("```");
            println!(
                "fn {}({}) -> {}",
                f.name,
                f.params.join(", "),
                f.return_type.as_deref().unwrap_or("()")
            );
            println!("```\n");
        }
    }

    if !structure.traits.is_empty() {
        println!("## Traits/Interfaces\n");
        for t in &structure.traits {
            let visibility = if t.is_public { "pub " } else { "" };
            println!(
                "- `{}trait {}` (lines {}-{})",
                visibility, t.name, t.start_line, t.end_line
            );
        }
        println!();
    }

    if !structure.enums.is_empty() {
        println!("## Enums\n");
        for e in &structure.enums {
            let visibility = if e.is_public { "pub " } else { "" };
            println!(
                "### {}`{}enum {}`\n",
                if e.is_public { "🔓 " } else { "🔒 " },
                visibility,
                e.name
            );
            println!("Lines {}-{}\n", e.start_line, e.end_line);
            println!("Variants: {}\n", e.variants.join(", "));
        }
    }
}