import os from pathlib import Path from typing import Optional, List import click from rich.console import Console from rich.panel import Panel from rich.text import Text from codechunk.config import Config, load_config from codechunk.core.chunking import CodeChunker from codechunk.core.parser import CodeParser from codechunk.core.formatter import OutputFormatter from codechunk.core.dependency import DependencyAnalyzer from codechunk.utils.logger import get_logger console = Console() logger = get_logger(__name__) @click.group() @click.option("--verbose", "-v", is_flag=True, help="Enable verbose output") @click.option("--config", "-c", type=click.Path(), help="Path to config file") @click.pass_context def main(ctx: click.Context, verbose: bool, config: Optional[str]) -> None: ctx.ensure_object(dict) ctx.obj["verbose"] = verbose ctx.obj["config_path"] = config if verbose: logger.setLevel("DEBUG") @main.command() @click.argument("path", type=click.Path(exists=True, file_okay=False, dir_okay=True)) @click.option("--output", "-o", type=click.Path(), help="Output file path") @click.option("--format", "-f", type=click.Choice(["ollama", "lmstudio", "markdown"]), default="markdown", help="Output format") @click.option("--max-tokens", "-t", type=int, default=8192, help="Maximum token limit") @click.option("--include", multiple=True, help="File patterns to include") @click.option("--exclude", multiple=True, help="File patterns to exclude") @click.pass_context def generate(ctx: click.Context, path: str, output: Optional[str], format: str, max_tokens: int, include: tuple, exclude: tuple) -> None: """Generate optimized context bundle for LLM.""" config_path = ctx.obj.get("config_path") verbose = ctx.obj.get("verbose", False) try: config = load_config(config_path) if config_path else Config() if include: config.chunking.include_patterns = list(include) if exclude: config.chunking.exclude_patterns = list(exclude) project_path = Path(path) logger.info(f"Analyzing project: {project_path}") parser = CodeParser() parser.discover_files(project_path, config.chunking.include_patterns, config.chunking.exclude_patterns) if verbose: console.print(f"[cyan]Found {len(parser.files)} files to process[/cyan]") chunks = parser.parse_all() chunker = CodeChunker(config.chunking) chunks = chunker.chunk_all(chunks) analyzer = DependencyAnalyzer() analyzer.analyze_dependencies(chunks, parser.files) formatter = OutputFormatter(format, max_tokens) formatted_output = formatter.format(chunks) if output: output_path = Path(output) output_path.write_text(formatted_output) console.print(f"[green]Context written to: {output_path}[/green]") else: console.print(Panel(formatted_output, title="Generated Context", expand=False)) console.print(f"\n[cyan]Summary:[/cyan]") console.print(f" - Files processed: {len(parser.files)}") console.print(f" - Chunks generated: {len(chunks)}") console.print(f" - Estimated tokens: {formatter.estimate_tokens(formatted_output)}") except Exception as e: logger.error(f"Error generating context: {e}") console.print(f"[red]Error: {e}[/red]") raise click.Abort() @main.command() @click.argument("path", type=click.Path(exists=True, file_okay=False, dir_okay=True)) @click.option("--json", is_flag=True, help="Output in JSON format") @click.pass_context def analyze(ctx: click.Context, path: str, json: bool) -> None: """Analyze codebase and report statistics.""" verbose = ctx.obj.get("verbose", False) try: project_path = Path(path) logger.info(f"Analyzing project: {project_path}") config = Config() parser = CodeParser() parser.discover_files(project_path, config.chunking.include_patterns, config.chunking.exclude_patterns) chunks = parser.parse_all() chunker = CodeChunker(config.chunking) chunks = chunker.chunk_all(chunks) stats = { "total_files": len(parser.files), "total_chunks": len(chunks), "files_by_language": {}, "chunks_by_type": {}, "total_lines": sum(c.metadata.line_count for c in chunks), "total_functions": sum(1 for c in chunks if c.chunk_type == "function"), "total_classes": sum(1 for c in chunks if c.chunk_type == "class"), } for chunk in chunks: lang = chunk.metadata.language stats["files_by_language"][lang] = stats["files_by_language"].get(lang, 0) + 1 stats["chunks_by_type"][chunk.chunk_type] = stats["chunks_by_type"].get(chunk.chunk_type, 0) + 1 if json: import json as json_module console.print(json_module.dumps(stats, indent=2)) else: console.print(Panel( Text.from_markup(f""" [b]Project Analysis[/b] Total Files: {stats['total_files']} Total Chunks: {stats['total_chunks']} Total Lines: {stats['total_lines']} Total Functions: {stats['total_functions']} Total Classes: {stats['total_classes']} [b]Files by Language[/b] {chr(10).join(f' - {lang}: {count}' for lang, count in stats['files_by_language'].items())} [b]Chunks by Type[/b] {chr(10).join(f' - {type_}: {count}' for type_, count in stats['chunks_by_type'].items())} """), title="Analysis Results", expand=False )) except Exception as e: logger.error(f"Error analyzing project: {e}") console.print(f"[red]Error: {e}[/red]") raise click.Abort() @main.command() @click.pass_context def version(ctx: click.Context) -> None: """Show version information.""" from codechunk import __version__ console.print(f"[cyan]CodeChunk CLI v{__version__}[/cyan]") if __name__ == "__main__": main()