feat(cli): Improve help text, error handling, and add fuzzy command suggestions
CLI help improvements (cli/mod.rs): - Add descriptive help text to all global flags (-c, --robot, -J, etc.) - Add descriptions to all subcommands (Issues, Mrs, Sync, etc.) - Add --no-quiet flag for explicit quiet override - Shell completions now shows installation instructions for each shell - Optional subcommand: running bare 'lore' shows help in terminal mode, robot-docs in robot mode Structured clap error handling (main.rs): - Early robot mode detection before parsing (env + args) - JSON error output for parse failures in robot mode - Semantic error codes: UNKNOWN_COMMAND, UNKNOWN_FLAG, MISSING_REQUIRED, INVALID_VALUE, ARGUMENT_CONFLICT, etc. - Fuzzy command suggestion using Jaro-Winkler similarity (>0.7 threshold) - Help/version requests handled normally (exit 0, not error) Robot-docs enhancements (main.rs): - Document deprecated command aliases (list issues -> issues, etc.) - Document clap error codes for programmatic error handling - Include completions command in manifest - Update flag documentation to show short forms (-n, -s, -p, etc.) Dependencies: - Add strsim 0.11 for Jaro-Winkler fuzzy matching Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
395
src/main.rs
395
src/main.rs
@@ -2,6 +2,7 @@ use clap::Parser;
|
||||
use console::style;
|
||||
use dialoguer::{Confirm, Input};
|
||||
use serde::Serialize;
|
||||
use strsim::jaro_winkler;
|
||||
use tracing_subscriber::Layer;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
@@ -10,13 +11,14 @@ use lore::Config;
|
||||
use lore::cli::commands::{
|
||||
IngestDisplay, InitInputs, InitOptions, InitResult, ListFilters, MrListFilters,
|
||||
SearchCliFilters, SyncOptions, open_issue_in_browser, open_mr_in_browser, print_count,
|
||||
print_count_json, print_doctor_results, print_embed, print_embed_json, print_event_count,
|
||||
print_event_count_json, print_generate_docs, print_generate_docs_json, print_ingest_summary,
|
||||
print_ingest_summary_json, print_list_issues, print_list_issues_json, print_list_mrs,
|
||||
print_list_mrs_json, print_search_results, print_search_results_json, print_show_issue,
|
||||
print_show_issue_json, print_show_mr, print_show_mr_json, print_stats, print_stats_json,
|
||||
print_sync, print_sync_json, print_sync_status, print_sync_status_json, run_auth_test,
|
||||
run_count, run_count_events, run_doctor, run_embed, run_generate_docs, run_ingest, run_init,
|
||||
print_count_json, print_doctor_results, print_dry_run_preview, print_dry_run_preview_json,
|
||||
print_embed, print_embed_json, print_event_count, print_event_count_json, print_generate_docs,
|
||||
print_generate_docs_json, print_ingest_summary, print_ingest_summary_json, print_list_issues,
|
||||
print_list_issues_json, print_list_mrs, print_list_mrs_json, print_search_results,
|
||||
print_search_results_json, print_show_issue, print_show_issue_json, print_show_mr,
|
||||
print_show_mr_json, print_stats, print_stats_json, print_sync, print_sync_json,
|
||||
print_sync_status, print_sync_status_json, run_auth_test, run_count, run_count_events,
|
||||
run_doctor, run_embed, run_generate_docs, run_ingest, run_ingest_dry_run, run_init,
|
||||
run_list_issues, run_list_mrs, run_search, run_show_issue, run_show_mr, run_stats, run_sync,
|
||||
run_sync_status,
|
||||
};
|
||||
@@ -40,7 +42,15 @@ async fn main() {
|
||||
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
|
||||
}
|
||||
|
||||
let cli = Cli::parse();
|
||||
// Phase 1: Early robot mode detection for structured clap errors
|
||||
let robot_mode_early = Cli::detect_robot_mode_from_env();
|
||||
|
||||
let cli = match Cli::try_parse() {
|
||||
Ok(cli) => cli,
|
||||
Err(e) => {
|
||||
handle_clap_error(e, robot_mode_early);
|
||||
}
|
||||
};
|
||||
let robot_mode = cli.is_robot_mode();
|
||||
|
||||
let logging_config = lore::Config::load(cli.config.as_deref())
|
||||
@@ -127,15 +137,29 @@ async fn main() {
|
||||
let quiet = cli.quiet;
|
||||
|
||||
let result = match cli.command {
|
||||
Commands::Issues(args) => handle_issues(cli.config.as_deref(), args, robot_mode),
|
||||
Commands::Mrs(args) => handle_mrs(cli.config.as_deref(), args, robot_mode),
|
||||
Commands::Search(args) => handle_search(cli.config.as_deref(), args, robot_mode).await,
|
||||
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) => {
|
||||
// Phase 2: Handle no-args case - in robot mode, output robot-docs; otherwise show help
|
||||
None => {
|
||||
if robot_mode {
|
||||
handle_robot_docs(robot_mode)
|
||||
} else {
|
||||
use clap::CommandFactory;
|
||||
let mut cmd = Cli::command();
|
||||
cmd.print_help().ok();
|
||||
println!();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Some(Commands::Issues(args)) => handle_issues(cli.config.as_deref(), args, robot_mode),
|
||||
Some(Commands::Mrs(args)) => handle_mrs(cli.config.as_deref(), args, robot_mode),
|
||||
Some(Commands::Search(args)) => {
|
||||
handle_search(cli.config.as_deref(), args, robot_mode).await
|
||||
}
|
||||
Some(Commands::Stats(args)) => handle_stats(cli.config.as_deref(), args, robot_mode).await,
|
||||
Some(Commands::Embed(args)) => handle_embed(cli.config.as_deref(), args, robot_mode).await,
|
||||
Some(Commands::Sync(args)) => {
|
||||
handle_sync_cmd(cli.config.as_deref(), args, robot_mode, &metrics_layer).await
|
||||
}
|
||||
Commands::Ingest(args) => {
|
||||
Some(Commands::Ingest(args)) => {
|
||||
handle_ingest(
|
||||
cli.config.as_deref(),
|
||||
args,
|
||||
@@ -145,19 +169,19 @@ async fn main() {
|
||||
)
|
||||
.await
|
||||
}
|
||||
Commands::Count(args) => handle_count(cli.config.as_deref(), args, robot_mode).await,
|
||||
Commands::Status => handle_sync_status_cmd(cli.config.as_deref(), robot_mode).await,
|
||||
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 {
|
||||
Some(Commands::Count(args)) => handle_count(cli.config.as_deref(), args, robot_mode).await,
|
||||
Some(Commands::Status) => handle_sync_status_cmd(cli.config.as_deref(), robot_mode).await,
|
||||
Some(Commands::Auth) => handle_auth_test(cli.config.as_deref(), robot_mode).await,
|
||||
Some(Commands::Doctor) => handle_doctor(cli.config.as_deref(), robot_mode).await,
|
||||
Some(Commands::Version) => handle_version(robot_mode),
|
||||
Some(Commands::Completions { shell }) => handle_completions(&shell),
|
||||
Some(Commands::Init {
|
||||
force,
|
||||
non_interactive,
|
||||
gitlab_url,
|
||||
token_env_var,
|
||||
projects,
|
||||
} => {
|
||||
}) => {
|
||||
handle_init(
|
||||
cli.config.as_deref(),
|
||||
force,
|
||||
@@ -169,16 +193,16 @@ async fn main() {
|
||||
)
|
||||
.await
|
||||
}
|
||||
Commands::GenerateDocs(args) => {
|
||||
Some(Commands::GenerateDocs(args)) => {
|
||||
handle_generate_docs(cli.config.as_deref(), args, robot_mode).await
|
||||
}
|
||||
Commands::Backup => handle_backup(robot_mode),
|
||||
Commands::Reset { yes: _ } => handle_reset(robot_mode),
|
||||
Commands::Migrate => handle_migrate(cli.config.as_deref(), robot_mode).await,
|
||||
Commands::Health => handle_health(cli.config.as_deref(), robot_mode).await,
|
||||
Commands::RobotDocs => handle_robot_docs(robot_mode),
|
||||
Some(Commands::Backup) => handle_backup(robot_mode),
|
||||
Some(Commands::Reset { yes: _ }) => handle_reset(robot_mode),
|
||||
Some(Commands::Migrate) => handle_migrate(cli.config.as_deref(), robot_mode).await,
|
||||
Some(Commands::Health) => handle_health(cli.config.as_deref(), robot_mode).await,
|
||||
Some(Commands::RobotDocs) => handle_robot_docs(robot_mode),
|
||||
|
||||
Commands::List {
|
||||
Some(Commands::List {
|
||||
entity,
|
||||
limit,
|
||||
project,
|
||||
@@ -198,7 +222,7 @@ async fn main() {
|
||||
reviewer,
|
||||
target_branch,
|
||||
source_branch,
|
||||
} => {
|
||||
}) => {
|
||||
if !robot_mode {
|
||||
eprintln!(
|
||||
"{}",
|
||||
@@ -231,11 +255,11 @@ async fn main() {
|
||||
)
|
||||
.await
|
||||
}
|
||||
Commands::Show {
|
||||
Some(Commands::Show {
|
||||
entity,
|
||||
iid,
|
||||
project,
|
||||
} => {
|
||||
}) => {
|
||||
if !robot_mode {
|
||||
eprintln!(
|
||||
"{}",
|
||||
@@ -255,7 +279,7 @@ async fn main() {
|
||||
)
|
||||
.await
|
||||
}
|
||||
Commands::AuthTest => {
|
||||
Some(Commands::AuthTest) => {
|
||||
if !robot_mode {
|
||||
eprintln!(
|
||||
"{}",
|
||||
@@ -264,7 +288,7 @@ async fn main() {
|
||||
}
|
||||
handle_auth_test(cli.config.as_deref(), robot_mode).await
|
||||
}
|
||||
Commands::SyncStatus => {
|
||||
Some(Commands::SyncStatus) => {
|
||||
if !robot_mode {
|
||||
eprintln!(
|
||||
"{}",
|
||||
@@ -338,11 +362,143 @@ fn handle_error(e: Box<dyn std::error::Error>, robot_mode: bool) -> ! {
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
/// Phase 1 & 4: Handle clap parsing errors with structured JSON output in robot mode.
|
||||
/// Also includes fuzzy command matching to suggest similar commands.
|
||||
fn handle_clap_error(e: clap::Error, robot_mode: bool) -> ! {
|
||||
use clap::error::ErrorKind;
|
||||
|
||||
// Always let clap handle --help and --version normally (print and exit 0).
|
||||
// These are intentional user actions, not errors, even when stdout is redirected.
|
||||
if matches!(e.kind(), ErrorKind::DisplayHelp | ErrorKind::DisplayVersion) {
|
||||
e.exit()
|
||||
}
|
||||
|
||||
if robot_mode {
|
||||
let error_code = map_clap_error_kind(e.kind());
|
||||
let message = e
|
||||
.to_string()
|
||||
.lines()
|
||||
.next()
|
||||
.unwrap_or("Parse error")
|
||||
.to_string();
|
||||
|
||||
// Phase 4: Try to suggest similar command for unknown commands
|
||||
let suggestion = if e.kind() == ErrorKind::InvalidSubcommand {
|
||||
if let Some(invalid_cmd) = extract_invalid_subcommand(&e) {
|
||||
suggest_similar_command(&invalid_cmd)
|
||||
} else {
|
||||
"Run 'lore robot-docs' for valid commands".to_string()
|
||||
}
|
||||
} else {
|
||||
"Run 'lore robot-docs' for valid commands".to_string()
|
||||
};
|
||||
|
||||
let output = RobotErrorWithSuggestion {
|
||||
error: RobotErrorSuggestionData {
|
||||
code: error_code.to_string(),
|
||||
message,
|
||||
suggestion,
|
||||
},
|
||||
};
|
||||
eprintln!(
|
||||
"{}",
|
||||
serde_json::to_string(&output).unwrap_or_else(|_| {
|
||||
r#"{"error":{"code":"PARSE_ERROR","message":"Parse error"}}"#.to_string()
|
||||
})
|
||||
);
|
||||
std::process::exit(2);
|
||||
} else {
|
||||
e.exit()
|
||||
}
|
||||
}
|
||||
|
||||
/// Map clap ErrorKind to semantic error codes
|
||||
fn map_clap_error_kind(kind: clap::error::ErrorKind) -> &'static str {
|
||||
use clap::error::ErrorKind;
|
||||
match kind {
|
||||
ErrorKind::InvalidSubcommand => "UNKNOWN_COMMAND",
|
||||
ErrorKind::UnknownArgument => "UNKNOWN_FLAG",
|
||||
ErrorKind::MissingRequiredArgument => "MISSING_REQUIRED",
|
||||
ErrorKind::InvalidValue => "INVALID_VALUE",
|
||||
ErrorKind::ValueValidation => "INVALID_VALUE",
|
||||
ErrorKind::TooManyValues => "TOO_MANY_VALUES",
|
||||
ErrorKind::TooFewValues => "TOO_FEW_VALUES",
|
||||
ErrorKind::ArgumentConflict => "ARGUMENT_CONFLICT",
|
||||
ErrorKind::MissingSubcommand => "MISSING_COMMAND",
|
||||
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => "HELP_REQUESTED",
|
||||
_ => "PARSE_ERROR",
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the invalid subcommand from a clap error (Phase 4)
|
||||
fn extract_invalid_subcommand(e: &clap::Error) -> Option<String> {
|
||||
// Parse the error message to find the invalid subcommand
|
||||
// Format is typically: "error: unrecognized subcommand 'foo'"
|
||||
let msg = e.to_string();
|
||||
if let Some(start) = msg.find('\'')
|
||||
&& let Some(end) = msg[start + 1..].find('\'')
|
||||
{
|
||||
return Some(msg[start + 1..start + 1 + end].to_string());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Phase 4: Suggest similar command using fuzzy matching
|
||||
fn suggest_similar_command(invalid: &str) -> String {
|
||||
const VALID_COMMANDS: &[&str] = &[
|
||||
"issues",
|
||||
"mrs",
|
||||
"search",
|
||||
"sync",
|
||||
"ingest",
|
||||
"count",
|
||||
"status",
|
||||
"auth",
|
||||
"doctor",
|
||||
"version",
|
||||
"init",
|
||||
"stats",
|
||||
"generate-docs",
|
||||
"embed",
|
||||
"migrate",
|
||||
"health",
|
||||
"robot-docs",
|
||||
"completions",
|
||||
];
|
||||
|
||||
let invalid_lower = invalid.to_lowercase();
|
||||
|
||||
// Find the best match using Jaro-Winkler similarity
|
||||
let best_match = VALID_COMMANDS
|
||||
.iter()
|
||||
.map(|cmd| (*cmd, jaro_winkler(&invalid_lower, cmd)))
|
||||
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||
|
||||
if let Some((cmd, score)) = best_match
|
||||
&& score > 0.7
|
||||
{
|
||||
return format!(
|
||||
"Did you mean 'lore {}'? Run 'lore robot-docs' for all commands",
|
||||
cmd
|
||||
);
|
||||
}
|
||||
|
||||
"Run 'lore robot-docs' for valid commands".to_string()
|
||||
}
|
||||
|
||||
fn handle_issues(
|
||||
config_override: Option<&str>,
|
||||
args: IssuesArgs,
|
||||
robot_mode: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Warn about unimplemented --fields
|
||||
if args.fields.is_some() && !robot_mode {
|
||||
eprintln!(
|
||||
"{}",
|
||||
style("warning: --fields is not yet implemented, showing all fields").yellow()
|
||||
);
|
||||
}
|
||||
|
||||
let config = Config::load(config_override)?;
|
||||
let asc = args.asc && !args.no_asc;
|
||||
let has_due = args.has_due && !args.no_has_due;
|
||||
@@ -391,6 +547,14 @@ fn handle_mrs(
|
||||
args: MrsArgs,
|
||||
robot_mode: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Warn about unimplemented --fields
|
||||
if args.fields.is_some() && !robot_mode {
|
||||
eprintln!(
|
||||
"{}",
|
||||
style("warning: --fields is not yet implemented, showing all fields").yellow()
|
||||
);
|
||||
}
|
||||
|
||||
let config = Config::load(config_override)?;
|
||||
let asc = args.asc && !args.no_asc;
|
||||
let open = args.open && !args.no_open;
|
||||
@@ -442,16 +606,47 @@ async fn handle_ingest(
|
||||
quiet: bool,
|
||||
metrics: &MetricsLayer,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let dry_run = args.dry_run && !args.no_dry_run;
|
||||
let config = Config::load(config_override)?;
|
||||
|
||||
let force = args.force && !args.no_force;
|
||||
let full = args.full && !args.no_full;
|
||||
|
||||
// Handle dry run mode - show preview without making any changes
|
||||
if dry_run {
|
||||
match args.entity.as_deref() {
|
||||
Some(resource_type) => {
|
||||
let preview =
|
||||
run_ingest_dry_run(&config, resource_type, args.project.as_deref(), full)?;
|
||||
if robot_mode {
|
||||
print_dry_run_preview_json(&preview);
|
||||
} else {
|
||||
print_dry_run_preview(&preview);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let issues_preview =
|
||||
run_ingest_dry_run(&config, "issues", args.project.as_deref(), full)?;
|
||||
let mrs_preview =
|
||||
run_ingest_dry_run(&config, "mrs", args.project.as_deref(), full)?;
|
||||
if robot_mode {
|
||||
print_combined_dry_run_json(&issues_preview, &mrs_preview);
|
||||
} else {
|
||||
print_dry_run_preview(&issues_preview);
|
||||
println!();
|
||||
print_dry_run_preview(&mrs_preview);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
let entity_label = args.entity.as_deref().unwrap_or("all");
|
||||
let command = format!("ingest:{entity_label}");
|
||||
let db_path = get_db_path(config.storage.db_path.as_deref());
|
||||
@@ -469,6 +664,7 @@ async fn handle_ingest(
|
||||
args.project.as_deref(),
|
||||
force,
|
||||
full,
|
||||
false,
|
||||
display,
|
||||
None,
|
||||
)
|
||||
@@ -495,6 +691,7 @@ async fn handle_ingest(
|
||||
args.project.as_deref(),
|
||||
force,
|
||||
full,
|
||||
false,
|
||||
display,
|
||||
None,
|
||||
)
|
||||
@@ -506,6 +703,7 @@ async fn handle_ingest(
|
||||
args.project.as_deref(),
|
||||
force,
|
||||
full,
|
||||
false,
|
||||
display,
|
||||
None,
|
||||
)
|
||||
@@ -592,6 +790,35 @@ fn print_combined_ingest_json(
|
||||
println!("{}", serde_json::to_string(&output).unwrap());
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CombinedDryRunOutput {
|
||||
ok: bool,
|
||||
dry_run: bool,
|
||||
data: CombinedDryRunData,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CombinedDryRunData {
|
||||
issues: lore::cli::commands::DryRunPreview,
|
||||
merge_requests: lore::cli::commands::DryRunPreview,
|
||||
}
|
||||
|
||||
fn print_combined_dry_run_json(
|
||||
issues: &lore::cli::commands::DryRunPreview,
|
||||
mrs: &lore::cli::commands::DryRunPreview,
|
||||
) {
|
||||
let output = CombinedDryRunOutput {
|
||||
ok: true,
|
||||
dry_run: true,
|
||||
data: CombinedDryRunData {
|
||||
issues: issues.clone(),
|
||||
merge_requests: mrs.clone(),
|
||||
},
|
||||
};
|
||||
|
||||
println!("{}", serde_json::to_string(&output).unwrap());
|
||||
}
|
||||
|
||||
async fn handle_count(
|
||||
config_override: Option<&str>,
|
||||
args: CountArgs,
|
||||
@@ -921,6 +1148,18 @@ async fn handle_auth_test(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct DoctorOutput {
|
||||
ok: bool,
|
||||
data: DoctorData,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct DoctorData {
|
||||
success: bool,
|
||||
checks: lore::cli::commands::DoctorChecks,
|
||||
}
|
||||
|
||||
async fn handle_doctor(
|
||||
config_override: Option<&str>,
|
||||
robot_mode: bool,
|
||||
@@ -928,7 +1167,14 @@ async fn handle_doctor(
|
||||
let result = run_doctor(config_override).await;
|
||||
|
||||
if robot_mode {
|
||||
println!("{}", serde_json::to_string_pretty(&result)?);
|
||||
let output = DoctorOutput {
|
||||
ok: true,
|
||||
data: DoctorData {
|
||||
success: result.success,
|
||||
checks: result.checks,
|
||||
},
|
||||
};
|
||||
println!("{}", serde_json::to_string(&output)?);
|
||||
} else {
|
||||
print_doctor_results(&result);
|
||||
}
|
||||
@@ -1133,9 +1379,10 @@ async fn handle_stats(
|
||||
args: StatsArgs,
|
||||
robot_mode: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let dry_run = args.dry_run && !args.no_dry_run;
|
||||
let config = Config::load(config_override)?;
|
||||
let check = (args.check && !args.no_check) || args.repair;
|
||||
let result = run_stats(&config, check, args.repair)?;
|
||||
let result = run_stats(&config, check, args.repair, dry_run)?;
|
||||
if robot_mode {
|
||||
print_stats_json(&result);
|
||||
} else {
|
||||
@@ -1219,6 +1466,8 @@ async fn handle_sync_cmd(
|
||||
robot_mode: bool,
|
||||
metrics: &MetricsLayer,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let dry_run = args.dry_run && !args.no_dry_run;
|
||||
|
||||
let mut config = Config::load(config_override)?;
|
||||
if args.no_events {
|
||||
config.sync.fetch_resource_events = false;
|
||||
@@ -1230,8 +1479,15 @@ async fn handle_sync_cmd(
|
||||
no_docs: args.no_docs,
|
||||
no_events: args.no_events,
|
||||
robot_mode,
|
||||
dry_run,
|
||||
};
|
||||
|
||||
// For dry_run, skip recording and just show the preview
|
||||
if dry_run {
|
||||
run_sync(&config, options, None).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let db_path = get_db_path(config.storage.db_path.as_deref());
|
||||
let recorder_conn = create_connection(&db_path)?;
|
||||
let run_id = uuid::Uuid::new_v4().simple().to_string();
|
||||
@@ -1371,7 +1627,11 @@ struct RobotDocsData {
|
||||
description: String,
|
||||
activation: RobotDocsActivation,
|
||||
commands: serde_json::Value,
|
||||
/// Deprecated command aliases (old -> new)
|
||||
aliases: serde_json::Value,
|
||||
exit_codes: serde_json::Value,
|
||||
/// Error codes emitted by clap parse failures
|
||||
clap_error_codes: serde_json::Value,
|
||||
error_format: String,
|
||||
workflows: serde_json::Value,
|
||||
}
|
||||
@@ -1410,37 +1670,37 @@ fn handle_robot_docs(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>>
|
||||
},
|
||||
"ingest": {
|
||||
"description": "Sync data from GitLab",
|
||||
"flags": ["--project <path>", "--force", "--full", "<entity: issues|mrs>"],
|
||||
"flags": ["--project <path>", "--force", "--no-force", "--full", "--no-full", "--dry-run", "--no-dry-run", "<entity: issues|mrs>"],
|
||||
"example": "lore --robot ingest issues --project group/repo"
|
||||
},
|
||||
"sync": {
|
||||
"description": "Full sync pipeline: ingest -> generate-docs -> embed",
|
||||
"flags": ["--full", "--force", "--no-embed", "--no-docs"],
|
||||
"flags": ["--full", "--no-full", "--force", "--no-force", "--no-embed", "--no-docs", "--no-events", "--dry-run", "--no-dry-run"],
|
||||
"example": "lore --robot sync"
|
||||
},
|
||||
"issues": {
|
||||
"description": "List or show issues",
|
||||
"flags": ["<IID>", "--limit", "--state", "--project", "--author", "--assignee", "--label", "--milestone", "--since", "--due-before", "--has-due", "--sort", "--asc"],
|
||||
"flags": ["<IID>", "-n/--limit", "--fields <list>", "-s/--state", "-p/--project", "-a/--author", "-A/--assignee", "-l/--label", "-m/--milestone", "--since", "--due-before", "--has-due", "--no-has-due", "--sort", "--asc", "--no-asc", "-o/--open", "--no-open"],
|
||||
"example": "lore --robot issues --state opened --limit 10"
|
||||
},
|
||||
"mrs": {
|
||||
"description": "List or show merge requests",
|
||||
"flags": ["<IID>", "--limit", "--state", "--project", "--author", "--assignee", "--reviewer", "--label", "--since", "--draft", "--no-draft", "--target", "--source", "--sort", "--asc"],
|
||||
"flags": ["<IID>", "-n/--limit", "--fields <list>", "-s/--state", "-p/--project", "-a/--author", "-A/--assignee", "-r/--reviewer", "-l/--label", "--since", "-d/--draft", "-D/--no-draft", "--target", "--source", "--sort", "--asc", "--no-asc", "-o/--open", "--no-open"],
|
||||
"example": "lore --robot mrs --state opened"
|
||||
},
|
||||
"search": {
|
||||
"description": "Search indexed documents (lexical, hybrid, semantic)",
|
||||
"flags": ["<QUERY>", "--mode", "--type", "--author", "--project", "--label", "--path", "--after", "--updated-after", "--limit", "--explain", "--fts-mode"],
|
||||
"flags": ["<QUERY>", "--mode", "--type", "--author", "-p/--project", "--label", "--path", "--after", "--updated-after", "-n/--limit", "--explain", "--no-explain", "--fts-mode"],
|
||||
"example": "lore --robot search 'authentication bug' --mode hybrid --limit 10"
|
||||
},
|
||||
"count": {
|
||||
"description": "Count entities in local database",
|
||||
"flags": ["<entity: issues|mrs|discussions|notes>", "--for <issue|mr>"],
|
||||
"flags": ["<entity: issues|mrs|discussions|notes|events>", "-f/--for <issue|mr>"],
|
||||
"example": "lore --robot count issues"
|
||||
},
|
||||
"stats": {
|
||||
"description": "Show document and index statistics",
|
||||
"flags": ["--check", "--repair"],
|
||||
"flags": ["--check", "--no-check", "--repair", "--dry-run", "--no-dry-run"],
|
||||
"example": "lore --robot stats"
|
||||
},
|
||||
"status": {
|
||||
@@ -1450,12 +1710,12 @@ fn handle_robot_docs(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>>
|
||||
},
|
||||
"generate-docs": {
|
||||
"description": "Generate searchable documents from ingested data",
|
||||
"flags": ["--full", "--project <path>"],
|
||||
"flags": ["--full", "-p/--project <path>"],
|
||||
"example": "lore --robot generate-docs --full"
|
||||
},
|
||||
"embed": {
|
||||
"description": "Generate vector embeddings for documents via Ollama",
|
||||
"flags": ["--full", "--retry-failed"],
|
||||
"flags": ["--full", "--no-full", "--retry-failed", "--no-retry-failed"],
|
||||
"example": "lore --robot embed"
|
||||
},
|
||||
"migrate": {
|
||||
@@ -1468,6 +1728,11 @@ fn handle_robot_docs(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>>
|
||||
"flags": [],
|
||||
"example": "lore --robot version"
|
||||
},
|
||||
"completions": {
|
||||
"description": "Generate shell completions",
|
||||
"flags": ["<shell: bash|zsh|fish|powershell>"],
|
||||
"example": "lore completions bash > ~/.local/share/bash-completion/completions/lore"
|
||||
},
|
||||
"robot-docs": {
|
||||
"description": "This command (agent self-discovery manifest)",
|
||||
"flags": [],
|
||||
@@ -1515,6 +1780,30 @@ fn handle_robot_docs(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>>
|
||||
]
|
||||
});
|
||||
|
||||
// Phase 3: Deprecated command aliases
|
||||
let aliases = serde_json::json!({
|
||||
"list issues": "issues",
|
||||
"list mrs": "mrs",
|
||||
"show issue <IID>": "issues <IID>",
|
||||
"show mr <IID>": "mrs <IID>",
|
||||
"auth-test": "auth",
|
||||
"sync-status": "status"
|
||||
});
|
||||
|
||||
// Phase 3: Clap error codes (emitted by handle_clap_error)
|
||||
let clap_error_codes = serde_json::json!({
|
||||
"UNKNOWN_COMMAND": "Unrecognized subcommand (includes fuzzy suggestion)",
|
||||
"UNKNOWN_FLAG": "Unrecognized command-line flag",
|
||||
"MISSING_REQUIRED": "Required argument not provided",
|
||||
"INVALID_VALUE": "Invalid value for argument",
|
||||
"TOO_MANY_VALUES": "Too many values provided",
|
||||
"TOO_FEW_VALUES": "Too few values provided",
|
||||
"ARGUMENT_CONFLICT": "Conflicting arguments",
|
||||
"MISSING_COMMAND": "No subcommand provided (in non-robot mode, shows help)",
|
||||
"HELP_REQUESTED": "Help or version flag used",
|
||||
"PARSE_ERROR": "General parse error"
|
||||
});
|
||||
|
||||
let output = RobotDocsOutput {
|
||||
ok: true,
|
||||
data: RobotDocsData {
|
||||
@@ -1527,7 +1816,9 @@ fn handle_robot_docs(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>>
|
||||
auto: "Non-TTY stdout".to_string(),
|
||||
},
|
||||
commands,
|
||||
aliases,
|
||||
exit_codes,
|
||||
clap_error_codes,
|
||||
error_format: "stderr JSON: {\"error\":{\"code\":\"...\",\"message\":\"...\",\"suggestion\":\"...\"}}".to_string(),
|
||||
workflows,
|
||||
},
|
||||
@@ -1639,14 +1930,14 @@ async fn handle_show_compat(
|
||||
entity: &str,
|
||||
iid: i64,
|
||||
project_filter: Option<&str>,
|
||||
json: bool,
|
||||
robot_mode: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let config = Config::load(config_override)?;
|
||||
|
||||
match entity {
|
||||
"issue" => {
|
||||
let result = run_show_issue(&config, iid, project_filter)?;
|
||||
if json {
|
||||
if robot_mode {
|
||||
print_show_issue_json(&result);
|
||||
} else {
|
||||
print_show_issue(&result);
|
||||
@@ -1655,7 +1946,7 @@ async fn handle_show_compat(
|
||||
}
|
||||
"mr" => {
|
||||
let result = run_show_mr(&config, iid, project_filter)?;
|
||||
if json {
|
||||
if robot_mode {
|
||||
print_show_mr_json(&result);
|
||||
} else {
|
||||
print_show_mr(&result);
|
||||
|
||||
Reference in New Issue
Block a user