"""CLI command definitions.""" from pathlib import Path import click from rich.console import Console from rich.panel import Panel from rich.text import Text from src.models.document import SourceType from src.search.searcher import Searcher from src.utils.config import get_config from src.utils.formatters import ( format_error, format_index_summary, format_search_results, format_success, ) console = Console() @click.group() @click.option("--verbose", "-v", is_flag=True, help="Enable verbose output") @click.pass_context def cli(ctx, verbose): """Local API Docs Search - Index and search your API documentation.""" ctx.ensure_object(dict) ctx.obj["verbose"] = verbose @click.command(name="index") @click.argument( "path", type=click.Path(exists=True, file_okay=True, dir_okay=True, path_type=Path) ) @click.option( "--type", "-t", type=click.Choice(["openapi", "readme", "code", "all"]), default="all", help="Type of documentation to index", ) @click.option( "--recursive", "-r", is_flag=True, default=False, help="Recursively search directories" ) @click.option( "--batch-size", "-b", type=int, default=32, help="Documents per batch" ) @click.pass_context def index_command(ctx, path, type, recursive, batch_size): """Index documentation from a path. PATH is the path to a file or directory to index. """ with console.status(f"Indexing {type} documentation from {path}..."): searcher = Searcher() count = searcher.index(path, doc_type=type, recursive=recursive, batch_size=batch_size) if count > 0: console.print(format_success(f"Successfully indexed {count} documents")) else: console.print(format_error("No documents found to index")) if type == "all": console.print("Try specifying a type: --type openapi|readme|code") @click.command(name="search") @click.argument("query", type=str) @click.option( "--limit", "-l", type=int, default=None, help="Maximum number of results" ) @click.option( "--type", "-t", type=click.Choice(["openapi", "readme", "code"]), help="Filter by source type", ) @click.option("--json", is_flag=True, help="Output as JSON") @click.option( "--hybrid/--semantic", default=True, help="Use hybrid (default) or semantic-only search", ) @click.pass_context def search_command(ctx, query, limit, type, json, hybrid): """Search indexed documentation. QUERY is the search query in natural language. """ config = get_config() if limit is None: limit = config.default_limit searcher = Searcher() with console.status("Searching..."): if hybrid: results = searcher.hybrid_search(query, limit=limit) else: results = searcher.search(query, limit=limit) if not results: console.print(format_info("No results found for your query")) return if json: import json as json_lib output = [r.to_dict() for r in results] console.print(json_lib.dumps(output, indent=2)) else: table = format_search_results(results) console.print(table) console.print(f"\nFound {len(results)} result(s)") @click.command(name="list") @click.option( "--type", "-t", type=click.Choice(["openapi", "readme", "code"]), help="Filter by source type", ) @click.option("--json", is_flag=True, help="Output as JSON") @click.pass_context def list_command(ctx, type, json): """List indexed documents.""" searcher = Searcher() stats = searcher.get_stats() if json: import json output = stats.to_dict() console.print(json.dumps(output, indent=2)) else: table = format_index_summary( stats.total_documents, stats.openapi_count, stats.readme_count, stats.code_count, ) console.print(table) @click.command(name="stats") @click.pass_context def stats_command(ctx): """Show index statistics.""" searcher = Searcher() stats = searcher.get_stats() table = format_index_summary( stats.total_documents, stats.openapi_count, stats.readme_count, stats.code_count, ) console.print(table) @click.command(name="clear") @click.option("--type", "-t", type=click.Choice(["openapi", "readme", "code"])) @click.option("--force", "-f", is_flag=True, help="Skip confirmation prompt") @click.pass_context def clear_command(ctx, type, force): """Clear the index or filtered by type.""" if not force: if type: confirm = click.confirm(f"Delete all {type} documents from the index?") else: confirm = click.confirm("Delete all documents from the index?") else: confirm = True if not confirm: console.print("Cancelled") return searcher = Searcher() if type: source_type = SourceType(type) count = searcher._vector_store.delete_by_source_type(source_type) else: count = searcher._vector_store.count() searcher.clear_index() console.print(format_success(f"Deleted {count} document(s)")) @click.command(name="config") @click.option("--show", is_flag=True, help="Show current configuration") @click.option("--reset", is_flag=True, help="Reset configuration to defaults") @click.pass_context def config_command(ctx, show, reset): """Manage configuration.""" config = get_config() if reset: config.reset() console.print(format_success("Configuration reset to defaults")) return if show or not (reset): config_dict = config.to_dict() if show: import json console.print(json.dumps(config_dict, indent=2)) else: lines = ["Current Configuration:", ""] for key, value in config_dict.items(): lines.append(f" {key}: {value}") panel = Panel( "\n".join(lines), title="Configuration", expand=False, ) console.print(panel) @click.command(name="interactive") @click.pass_context def interactive_command(ctx): """Enter interactive search mode.""" from src.cli.interactive import run_interactive run_interactive() def format_info(message: str) -> Text: """Format an info message.""" return Text(message, style="cyan")