diff --git a/src/main.rs b/src/main.rs index 237689c..0a1b34a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,6 +20,7 @@ use lore::cli::commands::{ print_show_mr_json, print_sync_status, print_sync_status_json, run_auth_test, run_count, run_doctor, run_embed, run_generate_docs, run_ingest, run_init, run_list_issues, run_list_mrs, run_search, run_show_issue, run_show_mr, run_stats, run_sync, run_sync_status, SyncOptions, + IngestDisplay, }; use lore::cli::{ Cli, Commands, CountArgs, EmbedArgs, GenerateDocsArgs, IngestArgs, IssuesArgs, MrsArgs, @@ -32,6 +33,12 @@ use lore::core::paths::get_db_path; #[tokio::main] async fn main() { + // Reset SIGPIPE to default behavior so piping (e.g. `lore issues | head`) doesn't panic + #[cfg(unix)] + unsafe { + libc::signal(libc::SIGPIPE, libc::SIG_DFL); + } + // Initialize logging with indicatif support for clean progress bar output let indicatif_layer = tracing_indicatif::IndicatifLayer::new(); @@ -52,6 +59,16 @@ async fn main() { let cli = Cli::parse(); let robot_mode = cli.is_robot_mode(); + // Apply color settings (console crate handles NO_COLOR/CLICOLOR natively in "auto" mode) + match cli.color.as_str() { + "never" => console::set_colors_enabled(false), + "always" => console::set_colors_enabled(true), + "auto" => {} // console crate handles this natively + _ => unreachable!(), + } + + let quiet = cli.quiet; + let result = match cli.command { Commands::Issues(args) => handle_issues(cli.config.as_deref(), args, robot_mode).await, Commands::Mrs(args) => handle_mrs(cli.config.as_deref(), args, robot_mode).await, @@ -59,7 +76,7 @@ async fn main() { Commands::Stats(args) => handle_stats(cli.config.as_deref(), args, robot_mode).await, Commands::Embed(args) => handle_embed(cli.config.as_deref(), args, robot_mode).await, Commands::Sync(args) => handle_sync_cmd(cli.config.as_deref(), args, robot_mode).await, - Commands::Ingest(args) => handle_ingest(cli.config.as_deref(), args, robot_mode).await, + Commands::Ingest(args) => handle_ingest(cli.config.as_deref(), args, robot_mode, quiet).await, Commands::Count(args) => { handle_count(cli.config.as_deref(), args, robot_mode).await } @@ -67,6 +84,7 @@ async fn main() { Commands::Auth => handle_auth_test(cli.config.as_deref(), robot_mode).await, Commands::Doctor => handle_doctor(cli.config.as_deref(), robot_mode).await, Commands::Version => handle_version(robot_mode), + Commands::Completions { shell } => handle_completions(&shell), Commands::Init { force, non_interactive, @@ -116,10 +134,12 @@ async fn main() { target_branch, source_branch, } => { - eprintln!( - "{}", - style("warning: 'lore list' is deprecated, use 'lore issues' or 'lore mrs'").yellow() - ); + if !robot_mode { + eprintln!( + "{}", + style("warning: 'lore list' is deprecated, use 'lore issues' or 'lore mrs'").yellow() + ); + } handle_list_compat( cli.config.as_deref(), &entity, @@ -150,14 +170,16 @@ async fn main() { iid, project, } => { - eprintln!( - "{}", - style(format!( - "warning: 'lore show' is deprecated, use 'lore {}s {}'", - entity, iid - )) - .yellow() - ); + if !robot_mode { + eprintln!( + "{}", + style(format!( + "warning: 'lore show' is deprecated, use 'lore {}s {}'", + entity, iid + )) + .yellow() + ); + } handle_show_compat( cli.config.as_deref(), &entity, @@ -168,17 +190,21 @@ async fn main() { .await } Commands::AuthTest => { - eprintln!( - "{}", - style("warning: 'lore auth-test' is deprecated, use 'lore auth'").yellow() - ); + if !robot_mode { + eprintln!( + "{}", + style("warning: 'lore auth-test' is deprecated, use 'lore auth'").yellow() + ); + } handle_auth_test(cli.config.as_deref(), robot_mode).await } Commands::SyncStatus => { - eprintln!( - "{}", - style("warning: 'lore sync-status' is deprecated, use 'lore status'").yellow() - ); + if !robot_mode { + eprintln!( + "{}", + style("warning: 'lore sync-status' is deprecated, use 'lore status'").yellow() + ); + } handle_sync_status_cmd(cli.config.as_deref(), robot_mode).await } }; @@ -259,7 +285,10 @@ async fn handle_issues( robot_mode: bool, ) -> Result<(), Box> { let config = Config::load(config_override)?; - let order = if args.asc { "asc" } else { "desc" }; + let asc = args.asc && !args.no_asc; + let has_due = args.has_due && !args.no_has_due; + let open = args.open && !args.no_open; + let order = if asc { "asc" } else { "desc" }; if let Some(iid) = args.iid { // Show mode @@ -281,14 +310,14 @@ async fn handle_issues( milestone: args.milestone.as_deref(), since: args.since.as_deref(), due_before: args.due_before.as_deref(), - has_due_date: args.has_due, + has_due_date: has_due, sort: &args.sort, order, }; let result = run_list_issues(&config, filters)?; - if args.open { + if open { open_issue_in_browser(&result); } else if robot_mode { print_list_issues_json(&result); @@ -306,7 +335,9 @@ async fn handle_mrs( robot_mode: bool, ) -> Result<(), Box> { let config = Config::load(config_override)?; - let order = if args.asc { "asc" } else { "desc" }; + let asc = args.asc && !args.no_asc; + let open = args.open && !args.no_open; + let order = if asc { "asc" } else { "desc" }; if let Some(iid) = args.iid { // Show mode @@ -337,7 +368,7 @@ async fn handle_mrs( let result = run_list_mrs(&config, filters)?; - if args.open { + if open { open_mr_in_browser(&result); } else if robot_mode { print_list_mrs_json(&result); @@ -353,8 +384,17 @@ async fn handle_ingest( config_override: Option<&str>, args: IngestArgs, robot_mode: bool, + quiet: bool, ) -> Result<(), Box> { let config = Config::load(config_override)?; + let display = if robot_mode || quiet { + IngestDisplay::silent() + } else { + IngestDisplay::interactive() + }; + + let force = args.force && !args.no_force; + let full = args.full && !args.no_full; match args.entity.as_deref() { Some(resource_type) => { @@ -363,9 +403,9 @@ async fn handle_ingest( &config, resource_type, args.project.as_deref(), - args.force, - args.full, - robot_mode, + force, + full, + display, ) .await?; @@ -377,7 +417,7 @@ async fn handle_ingest( } None => { // Ingest everything: issues then MRs - if !robot_mode { + if !robot_mode && !quiet { println!( "{}", style("Ingesting all content (issues + merge requests)...").blue() @@ -389,9 +429,9 @@ async fn handle_ingest( &config, "issues", args.project.as_deref(), - args.force, - args.full, - robot_mode, + force, + full, + display, ) .await?; @@ -399,9 +439,9 @@ async fn handle_ingest( &config, "mrs", args.project.as_deref(), - args.force, - args.full, - robot_mode, + force, + full, + display, ) .await?; @@ -823,65 +863,81 @@ struct VersionOutput { #[derive(Serialize)] struct VersionData { version: String, + #[serde(skip_serializing_if = "Option::is_none")] + git_hash: Option, } fn handle_version(robot_mode: bool) -> Result<(), Box> { let version = env!("CARGO_PKG_VERSION").to_string(); + let git_hash = env!("GIT_HASH").to_string(); if robot_mode { let output = VersionOutput { ok: true, - data: VersionData { version }, + data: VersionData { + version, + git_hash: if git_hash.is_empty() { + None + } else { + Some(git_hash) + }, + }, }; println!("{}", serde_json::to_string(&output)?); - } else { + } else if git_hash.is_empty() { println!("lore version {}", version); + } else { + println!("lore version {} ({})", version, git_hash); } Ok(()) } -/// JSON output for not-implemented commands. -#[derive(Serialize)] -struct NotImplementedOutput { - ok: bool, - data: NotImplementedData, -} +fn handle_completions(shell: &str) -> Result<(), Box> { + use clap::CommandFactory; + use clap_complete::{Shell, generate}; -#[derive(Serialize)] -struct NotImplementedData { - status: String, - command: String, + let shell = match shell { + "bash" => Shell::Bash, + "zsh" => Shell::Zsh, + "fish" => Shell::Fish, + "powershell" => Shell::PowerShell, + _ => unreachable!(), + }; + + let mut cmd = Cli::command(); + generate(shell, &mut cmd, "lore", &mut std::io::stdout()); + Ok(()) } fn handle_backup(robot_mode: bool) -> Result<(), Box> { if robot_mode { - let output = NotImplementedOutput { - ok: true, - data: NotImplementedData { - status: "not_implemented".to_string(), - command: "backup".to_string(), + let output = RobotErrorWithSuggestion { + error: RobotErrorSuggestionData { + code: "NOT_IMPLEMENTED".to_string(), + message: "The 'backup' command is not yet implemented.".to_string(), + suggestion: "Use manual database backup: cp ~/.local/share/lore/lore.db ~/.local/share/lore/lore.db.bak".to_string(), }, }; - println!("{}", serde_json::to_string(&output)?); + eprintln!("{}", serde_json::to_string(&output)?); } else { - println!("lore backup - not yet implemented"); + eprintln!("{} The 'backup' command is not yet implemented.", style("Error:").red()); } - Ok(()) + std::process::exit(1); } fn handle_reset(robot_mode: bool) -> Result<(), Box> { if robot_mode { - let output = NotImplementedOutput { - ok: true, - data: NotImplementedData { - status: "not_implemented".to_string(), - command: "reset".to_string(), + let output = RobotErrorWithSuggestion { + error: RobotErrorSuggestionData { + code: "NOT_IMPLEMENTED".to_string(), + message: "The 'reset' command is not yet implemented.".to_string(), + suggestion: "Manually delete the database: rm ~/.local/share/lore/lore.db".to_string(), }, }; - println!("{}", serde_json::to_string(&output)?); + eprintln!("{}", serde_json::to_string(&output)?); } else { - println!("lore reset - not yet implemented"); + eprintln!("{} The 'reset' command is not yet implemented.", style("Error:").red()); } - Ok(()) + std::process::exit(1); } /// JSON output for migrate command. @@ -987,7 +1043,9 @@ async fn handle_stats( robot_mode: bool, ) -> Result<(), Box> { let config = Config::load(config_override)?; - let result = run_stats(&config, args.check, args.repair)?; + // Auto-enable --check when --repair is used + let check = (args.check && !args.no_check) || args.repair; + let result = run_stats(&config, check, args.repair)?; if robot_mode { print_stats_json(&result); } else { @@ -1002,6 +1060,7 @@ async fn handle_search( robot_mode: bool, ) -> Result<(), Box> { let config = Config::load(config_override)?; + let explain = args.explain && !args.no_explain; let fts_mode = match args.fts_mode.as_str() { "raw" => lore::search::FtsQueryMode::Raw, @@ -1020,7 +1079,7 @@ async fn handle_search( }; let start = std::time::Instant::now(); - let response = run_search(&config, &args.query, cli_filters, fts_mode, args.explain)?; + let response = run_search(&config, &args.query, cli_filters, fts_mode, explain)?; let elapsed_ms = start.elapsed().as_millis() as u64; if robot_mode { @@ -1053,7 +1112,8 @@ async fn handle_embed( robot_mode: bool, ) -> Result<(), Box> { let config = Config::load(config_override)?; - let result = run_embed(&config, args.retry_failed).await?; + let retry_failed = args.retry_failed && !args.no_retry_failed; + let result = run_embed(&config, retry_failed).await?; if robot_mode { print_embed_json(&result); } else { @@ -1069,10 +1129,11 @@ async fn handle_sync_cmd( ) -> Result<(), Box> { let config = Config::load(config_override)?; let options = SyncOptions { - full: args.full, - force: args.force, + full: args.full && !args.no_full, + force: args.force && !args.no_force, no_embed: args.no_embed, no_docs: args.no_docs, + robot_mode, }; let start = std::time::Instant::now(); @@ -1301,8 +1362,8 @@ fn handle_robot_docs(robot_mode: bool) -> Result<(), Box> let exit_codes = serde_json::json!({ "0": "Success", - "1": "Internal error / health check failed", - "2": "Config not found / missing flags", + "1": "Internal error / health check failed / not implemented", + "2": "Usage error (invalid flags or arguments)", "3": "Config invalid", "4": "Token not set", "5": "GitLab auth failed", @@ -1313,7 +1374,13 @@ fn handle_robot_docs(robot_mode: bool) -> Result<(), Box> "10": "Database error", "11": "Migration failed", "12": "I/O error", - "13": "Transform error" + "13": "Transform error", + "14": "Ollama unavailable", + "15": "Ollama model not found", + "16": "Embedding failed", + "17": "Not found", + "18": "Ambiguous match", + "20": "Config not found" }); let workflows = serde_json::json!({