Files
codechunk-cli/codechunk/cli.py
7000pctAUTO 8186d226f2
Some checks failed
CI / test (push) Has been cancelled
CI / build (push) Has been cancelled
fix: resolve CI/CD issues - Poetry setup, type annotations, MyPy errors
2026-02-02 00:08:11 +00:00

183 lines
6.5 KiB
Python

import os
from pathlib import Path
from typing import Optional, List, cast
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 # type: ignore[index]
ctx.obj["config_path"] = config # type: ignore[index]
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."""
ctx_obj = cast(dict, ctx.obj)
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."""
ctx_obj = cast(dict, ctx.obj)
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: dict[str, int] = {
"total_files": len(parser.files),
"total_chunks": len(chunks),
"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"),
}
files_by_lang: dict[str, int] = {}
chunks_by_type: dict[str, int] = {}
for chunk in chunks:
lang = chunk.metadata.language
files_by_lang[lang] = files_by_lang.get(lang, 0) + 1
chunks_by_type[chunk.chunk_type] = chunks_by_type.get(chunk.chunk_type, 0) + 1
if json:
import json as json_module
full_stats: dict[str, object] = {
"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"],
"files_by_language": files_by_lang,
"chunks_by_type": chunks_by_type,
}
console.print(json_module.dumps(full_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 files_by_lang.items())}
[b]Chunks by Type[/b]
{chr(10).join(f' - {type_}: {count}' for type_, count in 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()