From e2d94f5f6ffd0b71f369a33b2353a5faa414fd4e Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Tue, 3 Feb 2026 03:54:35 +0000 Subject: [PATCH] fix: resolve CI/CD issues with proper package structure and imports --- src/local_api_docs_search/cli/commands.py | 235 ++++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 src/local_api_docs_search/cli/commands.py diff --git a/src/local_api_docs_search/cli/commands.py b/src/local_api_docs_search/cli/commands.py new file mode 100644 index 0000000..a4d9bbf --- /dev/null +++ b/src/local_api_docs_search/cli/commands.py @@ -0,0 +1,235 @@ +"""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 local_api_docs_search.models.document import SourceType +from local_api_docs_search.search.searcher import Searcher +from local_api_docs_search.utils.config import get_config +from local_api_docs_search.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 + + +@cli.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") + + +@cli.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)") + + +@cli.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) + + +@cli.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) + + +@cli.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)")) + + +@cli.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) + + +@cli.command(name="interactive") +@click.pass_context +def interactive_command(ctx): + """Enter interactive search mode.""" + from local_api_docs_search.cli.interactive import run_interactive + + run_interactive() + + +def format_info(message: str) -> Text: + """Format an info message.""" + return Text(message, style="cyan")