Initial commit: Add Local API Docs Search CLI tool
Some checks failed
CI / test (push) Has been cancelled
CI / build (push) Has been cancelled

This commit is contained in:
2026-02-03 01:18:59 +00:00
parent 141a5bc7b7
commit 6be0dde4fb

248
src/cli/commands.py Normal file
View File

@@ -0,0 +1,248 @@
"""CLI command definitions."""
from pathlib import Path
from typing import Optional
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,
format_help_header,
)
from src.utils.config import reset_config
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.
"""
verbose = ctx.obj.get("verbose", False)
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
source_filter = None
if type:
source_filter = SourceType(type)
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."""
source_filter = None
if type:
source_filter = SourceType(type)
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")