feat(main): Wire SIGPIPE, color, quiet, completions, and negation flag handling
Runtime setup: - Reset SIGPIPE to SIG_DFL on Unix at the very start of main() so piping to head/grep doesn't cause a panic. - Apply --color flag to console::set_colors_enabled() after CLI parse. - Extract quiet flag and thread it to handle_ingest. Command dispatch: - Add Completions match arm using clap_complete::generate(). - Resolve all --no-X negation flags in handlers: asc, has_due, open (issues/mrs), force/full (ingest/sync), check (stats), explain (search), retry_failed (embed). - Auto-enable --check when --repair is used in handle_stats. - Suppress deprecation warnings in robot mode for List, Show, AuthTest, and SyncStatus deprecated aliases. Stubs: - Change handle_backup/handle_reset from ok:true to structured error JSON on stderr with exit code 1. Remove unused NotImplementedOutput and NotImplementedData structs. Version: - Include GIT_HASH env var in handle_version output (human and robot). - Add git_hash field to VersionData with skip_serializing_if for None. Robot-docs: - Update exit code table with codes 14-18 (Ollama, NotFound, Ambiguous) and code 20 (ConfigNotFound). Clarify code 1 and 2 descriptions. Co-Authored-By: Claude (us.anthropic.claude-opus-4-5-20251101-v1:0) <noreply@anthropic.com>
This commit is contained in:
211
src/main.rs
211
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<String>,
|
||||
}
|
||||
|
||||
fn handle_version(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>>
|
||||
|
||||
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<dyn std::error::Error>>
|
||||
"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!({
|
||||
|
||||
Reference in New Issue
Block a user