Files
codechunk-cli/codechunk/cli.py
7000pctAUTO fce6fb3fec
Some checks failed
CI / test (push) Has been cancelled
CI / build (push) Has been cancelled
Add codechunk package files
2026-02-01 23:42:00 +00:00

171 lines
6.0 KiB
Python

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()