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:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1129,6 +1129,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
"sqlite-vec",
|
"sqlite-vec",
|
||||||
|
"strsim",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ flate2 = "1"
|
|||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
uuid = { version = "1", features = ["v4"] }
|
uuid = { version = "1", features = ["v4"] }
|
||||||
regex = "1"
|
regex = "1"
|
||||||
|
strsim = "0.11"
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
|
|||||||
@@ -17,10 +17,13 @@ pub use count::{
|
|||||||
print_count, print_count_json, print_event_count, print_event_count_json, run_count,
|
print_count, print_count_json, print_event_count, print_event_count_json, run_count,
|
||||||
run_count_events,
|
run_count_events,
|
||||||
};
|
};
|
||||||
pub use doctor::{print_doctor_results, run_doctor};
|
pub use doctor::{DoctorChecks, print_doctor_results, run_doctor};
|
||||||
pub use embed::{print_embed, print_embed_json, run_embed};
|
pub use embed::{print_embed, print_embed_json, run_embed};
|
||||||
pub use generate_docs::{print_generate_docs, print_generate_docs_json, run_generate_docs};
|
pub use generate_docs::{print_generate_docs, print_generate_docs_json, run_generate_docs};
|
||||||
pub use ingest::{IngestDisplay, print_ingest_summary, print_ingest_summary_json, run_ingest};
|
pub use ingest::{
|
||||||
|
DryRunPreview, IngestDisplay, print_dry_run_preview, print_dry_run_preview_json,
|
||||||
|
print_ingest_summary, print_ingest_summary_json, run_ingest, run_ingest_dry_run,
|
||||||
|
};
|
||||||
pub use init::{InitInputs, InitOptions, InitResult, run_init};
|
pub use init::{InitInputs, InitOptions, InitResult, run_init};
|
||||||
pub use list::{
|
pub use list::{
|
||||||
ListFilters, MrListFilters, open_issue_in_browser, open_mr_in_browser, print_list_issues,
|
ListFilters, MrListFilters, open_issue_in_browser, open_mr_in_browser, print_list_issues,
|
||||||
|
|||||||
179
src/cli/mod.rs
179
src/cli/mod.rs
@@ -6,71 +6,127 @@ use std::io::IsTerminal;
|
|||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "lore")]
|
#[command(name = "lore")]
|
||||||
#[command(version, about, long_about = None)]
|
#[command(version, about = "Local GitLab data management with semantic search", long_about = None)]
|
||||||
|
#[command(subcommand_required = false)]
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
#[arg(short = 'c', long, global = true)]
|
/// Path to config file
|
||||||
|
#[arg(short = 'c', long, global = true, help = "Path to config file")]
|
||||||
pub config: Option<String>,
|
pub config: Option<String>,
|
||||||
|
|
||||||
#[arg(long, global = true, env = "LORE_ROBOT")]
|
/// Machine-readable JSON output (auto-enabled when piped)
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
global = true,
|
||||||
|
env = "LORE_ROBOT",
|
||||||
|
help = "Machine-readable JSON output (auto-enabled when piped)"
|
||||||
|
)]
|
||||||
pub robot: bool,
|
pub robot: bool,
|
||||||
|
|
||||||
#[arg(short = 'J', long = "json", global = true)]
|
/// JSON output (global shorthand)
|
||||||
|
#[arg(
|
||||||
|
short = 'J',
|
||||||
|
long = "json",
|
||||||
|
global = true,
|
||||||
|
help = "JSON output (global shorthand)"
|
||||||
|
)]
|
||||||
pub json: bool,
|
pub json: bool,
|
||||||
|
|
||||||
#[arg(long, global = true, value_parser = ["auto", "always", "never"], default_value = "auto")]
|
/// Color output: auto (default), always, or never
|
||||||
|
#[arg(long, global = true, value_parser = ["auto", "always", "never"], default_value = "auto", help = "Color output: auto (default), always, or never")]
|
||||||
pub color: String,
|
pub color: String,
|
||||||
|
|
||||||
#[arg(short = 'q', long, global = true)]
|
/// Suppress non-essential output
|
||||||
|
#[arg(
|
||||||
|
short = 'q',
|
||||||
|
long,
|
||||||
|
global = true,
|
||||||
|
overrides_with = "no_quiet",
|
||||||
|
help = "Suppress non-essential output"
|
||||||
|
)]
|
||||||
pub quiet: bool,
|
pub quiet: bool,
|
||||||
|
|
||||||
#[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count, global = true)]
|
#[arg(
|
||||||
|
long = "no-quiet",
|
||||||
|
global = true,
|
||||||
|
hide = true,
|
||||||
|
overrides_with = "quiet"
|
||||||
|
)]
|
||||||
|
pub no_quiet: bool,
|
||||||
|
|
||||||
|
/// Increase log verbosity (-v, -vv, -vvv)
|
||||||
|
#[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count, global = true, help = "Increase log verbosity (-v, -vv, -vvv)")]
|
||||||
pub verbose: u8,
|
pub verbose: u8,
|
||||||
|
|
||||||
#[arg(long = "log-format", global = true, value_parser = ["text", "json"], default_value = "text")]
|
/// Log format for stderr output: text (default) or json
|
||||||
|
#[arg(long = "log-format", global = true, value_parser = ["text", "json"], default_value = "text", help = "Log format for stderr output: text (default) or json")]
|
||||||
pub log_format: String,
|
pub log_format: String,
|
||||||
|
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub command: Commands,
|
pub command: Option<Commands>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cli {
|
impl Cli {
|
||||||
pub fn is_robot_mode(&self) -> bool {
|
pub fn is_robot_mode(&self) -> bool {
|
||||||
self.robot || self.json || !std::io::stdout().is_terminal()
|
self.robot || self.json || !std::io::stdout().is_terminal()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Detect robot mode from environment before parsing succeeds.
|
||||||
|
/// Used for structured error output when clap parsing fails.
|
||||||
|
pub fn detect_robot_mode_from_env() -> bool {
|
||||||
|
let args: Vec<String> = std::env::args().collect();
|
||||||
|
args.iter()
|
||||||
|
.any(|a| a == "--robot" || a == "-J" || a == "--json")
|
||||||
|
|| std::env::var("LORE_ROBOT").is_ok()
|
||||||
|
|| !std::io::stdout().is_terminal()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
#[allow(clippy::large_enum_variant)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
pub enum Commands {
|
pub enum Commands {
|
||||||
|
/// List or show issues
|
||||||
Issues(IssuesArgs),
|
Issues(IssuesArgs),
|
||||||
|
|
||||||
|
/// List or show merge requests
|
||||||
Mrs(MrsArgs),
|
Mrs(MrsArgs),
|
||||||
|
|
||||||
|
/// Ingest data from GitLab
|
||||||
Ingest(IngestArgs),
|
Ingest(IngestArgs),
|
||||||
|
|
||||||
|
/// Count entities in local database
|
||||||
Count(CountArgs),
|
Count(CountArgs),
|
||||||
|
|
||||||
|
/// Show sync state
|
||||||
Status,
|
Status,
|
||||||
|
|
||||||
|
/// Verify GitLab authentication
|
||||||
Auth,
|
Auth,
|
||||||
|
|
||||||
|
/// Check environment health
|
||||||
Doctor,
|
Doctor,
|
||||||
|
|
||||||
|
/// Show version information
|
||||||
Version,
|
Version,
|
||||||
|
|
||||||
|
/// Initialize configuration and database
|
||||||
Init {
|
Init {
|
||||||
|
/// Skip overwrite confirmation
|
||||||
#[arg(short = 'f', long)]
|
#[arg(short = 'f', long)]
|
||||||
force: bool,
|
force: bool,
|
||||||
|
|
||||||
|
/// Fail if prompts would be shown
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
non_interactive: bool,
|
non_interactive: bool,
|
||||||
|
|
||||||
|
/// GitLab base URL (required in robot mode)
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
gitlab_url: Option<String>,
|
gitlab_url: Option<String>,
|
||||||
|
|
||||||
|
/// Environment variable name holding GitLab token (required in robot mode)
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
token_env_var: Option<String>,
|
token_env_var: Option<String>,
|
||||||
|
|
||||||
|
/// Comma-separated project paths (required in robot mode)
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
projects: Option<String>,
|
projects: Option<String>,
|
||||||
},
|
},
|
||||||
@@ -84,26 +140,41 @@ pub enum Commands {
|
|||||||
yes: bool,
|
yes: bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Search indexed documents
|
||||||
Search(SearchArgs),
|
Search(SearchArgs),
|
||||||
|
|
||||||
|
/// Show document and index statistics
|
||||||
Stats(StatsArgs),
|
Stats(StatsArgs),
|
||||||
|
|
||||||
|
/// Generate searchable documents from ingested data
|
||||||
#[command(name = "generate-docs")]
|
#[command(name = "generate-docs")]
|
||||||
GenerateDocs(GenerateDocsArgs),
|
GenerateDocs(GenerateDocsArgs),
|
||||||
|
|
||||||
|
/// Generate vector embeddings for documents via Ollama
|
||||||
Embed(EmbedArgs),
|
Embed(EmbedArgs),
|
||||||
|
|
||||||
|
/// Run full sync pipeline: ingest -> generate-docs -> embed
|
||||||
Sync(SyncArgs),
|
Sync(SyncArgs),
|
||||||
|
|
||||||
|
/// Run pending database migrations
|
||||||
Migrate,
|
Migrate,
|
||||||
|
|
||||||
|
/// Quick health check: config, database, schema version
|
||||||
Health,
|
Health,
|
||||||
|
|
||||||
|
/// Machine-readable command manifest for agent self-discovery
|
||||||
#[command(name = "robot-docs")]
|
#[command(name = "robot-docs")]
|
||||||
RobotDocs,
|
RobotDocs,
|
||||||
|
|
||||||
#[command(hide = true)]
|
/// Generate shell completions
|
||||||
|
#[command(long_about = "Generate shell completions for lore.\n\n\
|
||||||
|
Installation:\n \
|
||||||
|
bash: lore completions bash > ~/.local/share/bash-completion/completions/lore\n \
|
||||||
|
zsh: lore completions zsh > ~/.zfunc/_lore && echo 'fpath+=~/.zfunc' >> ~/.zshrc\n \
|
||||||
|
fish: lore completions fish > ~/.config/fish/completions/lore.fish\n \
|
||||||
|
pwsh: lore completions powershell >> $PROFILE")]
|
||||||
Completions {
|
Completions {
|
||||||
|
/// Shell to generate completions for
|
||||||
#[arg(value_parser = ["bash", "zsh", "fish", "powershell"])]
|
#[arg(value_parser = ["bash", "zsh", "fish", "powershell"])]
|
||||||
shell: String,
|
shell: String,
|
||||||
},
|
},
|
||||||
@@ -171,8 +242,10 @@ pub enum Commands {
|
|||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
pub struct IssuesArgs {
|
pub struct IssuesArgs {
|
||||||
|
/// Issue IID (omit to list, provide to show details)
|
||||||
pub iid: Option<i64>,
|
pub iid: Option<i64>,
|
||||||
|
|
||||||
|
/// Maximum results
|
||||||
#[arg(
|
#[arg(
|
||||||
short = 'n',
|
short = 'n',
|
||||||
long = "limit",
|
long = "limit",
|
||||||
@@ -181,30 +254,43 @@ pub struct IssuesArgs {
|
|||||||
)]
|
)]
|
||||||
pub limit: usize,
|
pub limit: usize,
|
||||||
|
|
||||||
|
/// Select output fields (comma-separated: iid,title,state,author,labels,updated)
|
||||||
|
#[arg(long, help_heading = "Output", value_delimiter = ',')]
|
||||||
|
pub fields: Option<Vec<String>>,
|
||||||
|
|
||||||
|
/// Filter by state (opened, closed, all)
|
||||||
#[arg(short = 's', long, help_heading = "Filters")]
|
#[arg(short = 's', long, help_heading = "Filters")]
|
||||||
pub state: Option<String>,
|
pub state: Option<String>,
|
||||||
|
|
||||||
|
/// Filter by project path
|
||||||
#[arg(short = 'p', long, help_heading = "Filters")]
|
#[arg(short = 'p', long, help_heading = "Filters")]
|
||||||
pub project: Option<String>,
|
pub project: Option<String>,
|
||||||
|
|
||||||
|
/// Filter by author username
|
||||||
#[arg(short = 'a', long, help_heading = "Filters")]
|
#[arg(short = 'a', long, help_heading = "Filters")]
|
||||||
pub author: Option<String>,
|
pub author: Option<String>,
|
||||||
|
|
||||||
|
/// Filter by assignee username
|
||||||
#[arg(short = 'A', long, help_heading = "Filters")]
|
#[arg(short = 'A', long, help_heading = "Filters")]
|
||||||
pub assignee: Option<String>,
|
pub assignee: Option<String>,
|
||||||
|
|
||||||
|
/// Filter by label (repeatable, AND logic)
|
||||||
#[arg(short = 'l', long, help_heading = "Filters")]
|
#[arg(short = 'l', long, help_heading = "Filters")]
|
||||||
pub label: Option<Vec<String>>,
|
pub label: Option<Vec<String>>,
|
||||||
|
|
||||||
|
/// Filter by milestone title
|
||||||
#[arg(short = 'm', long, help_heading = "Filters")]
|
#[arg(short = 'm', long, help_heading = "Filters")]
|
||||||
pub milestone: Option<String>,
|
pub milestone: Option<String>,
|
||||||
|
|
||||||
|
/// Filter by time (7d, 2w, 1m, or YYYY-MM-DD)
|
||||||
#[arg(long, help_heading = "Filters")]
|
#[arg(long, help_heading = "Filters")]
|
||||||
pub since: Option<String>,
|
pub since: Option<String>,
|
||||||
|
|
||||||
|
/// Filter by due date (before this date, YYYY-MM-DD)
|
||||||
#[arg(long = "due-before", help_heading = "Filters")]
|
#[arg(long = "due-before", help_heading = "Filters")]
|
||||||
pub due_before: Option<String>,
|
pub due_before: Option<String>,
|
||||||
|
|
||||||
|
/// Show only issues with a due date
|
||||||
#[arg(
|
#[arg(
|
||||||
long = "has-due",
|
long = "has-due",
|
||||||
help_heading = "Filters",
|
help_heading = "Filters",
|
||||||
@@ -215,15 +301,18 @@ pub struct IssuesArgs {
|
|||||||
#[arg(long = "no-has-due", hide = true, overrides_with = "has_due")]
|
#[arg(long = "no-has-due", hide = true, overrides_with = "has_due")]
|
||||||
pub no_has_due: bool,
|
pub no_has_due: bool,
|
||||||
|
|
||||||
|
/// Sort field (updated, created, iid)
|
||||||
#[arg(long, value_parser = ["updated", "created", "iid"], default_value = "updated", help_heading = "Sorting")]
|
#[arg(long, value_parser = ["updated", "created", "iid"], default_value = "updated", help_heading = "Sorting")]
|
||||||
pub sort: String,
|
pub sort: String,
|
||||||
|
|
||||||
|
/// Sort ascending (default: descending)
|
||||||
#[arg(long, help_heading = "Sorting", overrides_with = "no_asc")]
|
#[arg(long, help_heading = "Sorting", overrides_with = "no_asc")]
|
||||||
pub asc: bool,
|
pub asc: bool,
|
||||||
|
|
||||||
#[arg(long = "no-asc", hide = true, overrides_with = "asc")]
|
#[arg(long = "no-asc", hide = true, overrides_with = "asc")]
|
||||||
pub no_asc: bool,
|
pub no_asc: bool,
|
||||||
|
|
||||||
|
/// Open first matching item in browser
|
||||||
#[arg(
|
#[arg(
|
||||||
short = 'o',
|
short = 'o',
|
||||||
long,
|
long,
|
||||||
@@ -238,8 +327,10 @@ pub struct IssuesArgs {
|
|||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
pub struct MrsArgs {
|
pub struct MrsArgs {
|
||||||
|
/// MR IID (omit to list, provide to show details)
|
||||||
pub iid: Option<i64>,
|
pub iid: Option<i64>,
|
||||||
|
|
||||||
|
/// Maximum results
|
||||||
#[arg(
|
#[arg(
|
||||||
short = 'n',
|
short = 'n',
|
||||||
long = "limit",
|
long = "limit",
|
||||||
@@ -248,27 +339,39 @@ pub struct MrsArgs {
|
|||||||
)]
|
)]
|
||||||
pub limit: usize,
|
pub limit: usize,
|
||||||
|
|
||||||
|
/// Select output fields (comma-separated: iid,title,state,author,labels,updated)
|
||||||
|
#[arg(long, help_heading = "Output", value_delimiter = ',')]
|
||||||
|
pub fields: Option<Vec<String>>,
|
||||||
|
|
||||||
|
/// Filter by state (opened, merged, closed, locked, all)
|
||||||
#[arg(short = 's', long, help_heading = "Filters")]
|
#[arg(short = 's', long, help_heading = "Filters")]
|
||||||
pub state: Option<String>,
|
pub state: Option<String>,
|
||||||
|
|
||||||
|
/// Filter by project path
|
||||||
#[arg(short = 'p', long, help_heading = "Filters")]
|
#[arg(short = 'p', long, help_heading = "Filters")]
|
||||||
pub project: Option<String>,
|
pub project: Option<String>,
|
||||||
|
|
||||||
|
/// Filter by author username
|
||||||
#[arg(short = 'a', long, help_heading = "Filters")]
|
#[arg(short = 'a', long, help_heading = "Filters")]
|
||||||
pub author: Option<String>,
|
pub author: Option<String>,
|
||||||
|
|
||||||
|
/// Filter by assignee username
|
||||||
#[arg(short = 'A', long, help_heading = "Filters")]
|
#[arg(short = 'A', long, help_heading = "Filters")]
|
||||||
pub assignee: Option<String>,
|
pub assignee: Option<String>,
|
||||||
|
|
||||||
|
/// Filter by reviewer username
|
||||||
#[arg(short = 'r', long, help_heading = "Filters")]
|
#[arg(short = 'r', long, help_heading = "Filters")]
|
||||||
pub reviewer: Option<String>,
|
pub reviewer: Option<String>,
|
||||||
|
|
||||||
|
/// Filter by label (repeatable, AND logic)
|
||||||
#[arg(short = 'l', long, help_heading = "Filters")]
|
#[arg(short = 'l', long, help_heading = "Filters")]
|
||||||
pub label: Option<Vec<String>>,
|
pub label: Option<Vec<String>>,
|
||||||
|
|
||||||
|
/// Filter by time (7d, 2w, 1m, or YYYY-MM-DD)
|
||||||
#[arg(long, help_heading = "Filters")]
|
#[arg(long, help_heading = "Filters")]
|
||||||
pub since: Option<String>,
|
pub since: Option<String>,
|
||||||
|
|
||||||
|
/// Show only draft MRs
|
||||||
#[arg(
|
#[arg(
|
||||||
short = 'd',
|
short = 'd',
|
||||||
long,
|
long,
|
||||||
@@ -277,6 +380,7 @@ pub struct MrsArgs {
|
|||||||
)]
|
)]
|
||||||
pub draft: bool,
|
pub draft: bool,
|
||||||
|
|
||||||
|
/// Exclude draft MRs
|
||||||
#[arg(
|
#[arg(
|
||||||
short = 'D',
|
short = 'D',
|
||||||
long = "no-draft",
|
long = "no-draft",
|
||||||
@@ -285,21 +389,26 @@ pub struct MrsArgs {
|
|||||||
)]
|
)]
|
||||||
pub no_draft: bool,
|
pub no_draft: bool,
|
||||||
|
|
||||||
|
/// Filter by target branch
|
||||||
#[arg(long, help_heading = "Filters")]
|
#[arg(long, help_heading = "Filters")]
|
||||||
pub target: Option<String>,
|
pub target: Option<String>,
|
||||||
|
|
||||||
|
/// Filter by source branch
|
||||||
#[arg(long, help_heading = "Filters")]
|
#[arg(long, help_heading = "Filters")]
|
||||||
pub source: Option<String>,
|
pub source: Option<String>,
|
||||||
|
|
||||||
|
/// Sort field (updated, created, iid)
|
||||||
#[arg(long, value_parser = ["updated", "created", "iid"], default_value = "updated", help_heading = "Sorting")]
|
#[arg(long, value_parser = ["updated", "created", "iid"], default_value = "updated", help_heading = "Sorting")]
|
||||||
pub sort: String,
|
pub sort: String,
|
||||||
|
|
||||||
|
/// Sort ascending (default: descending)
|
||||||
#[arg(long, help_heading = "Sorting", overrides_with = "no_asc")]
|
#[arg(long, help_heading = "Sorting", overrides_with = "no_asc")]
|
||||||
pub asc: bool,
|
pub asc: bool,
|
||||||
|
|
||||||
#[arg(long = "no-asc", hide = true, overrides_with = "asc")]
|
#[arg(long = "no-asc", hide = true, overrides_with = "asc")]
|
||||||
pub no_asc: bool,
|
pub no_asc: bool,
|
||||||
|
|
||||||
|
/// Open first matching item in browser
|
||||||
#[arg(
|
#[arg(
|
||||||
short = 'o',
|
short = 'o',
|
||||||
long,
|
long,
|
||||||
@@ -314,65 +423,95 @@ pub struct MrsArgs {
|
|||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
pub struct IngestArgs {
|
pub struct IngestArgs {
|
||||||
|
/// Entity to ingest (issues, mrs). Omit to ingest everything
|
||||||
#[arg(value_parser = ["issues", "mrs"])]
|
#[arg(value_parser = ["issues", "mrs"])]
|
||||||
pub entity: Option<String>,
|
pub entity: Option<String>,
|
||||||
|
|
||||||
|
/// Filter to single project
|
||||||
#[arg(short = 'p', long)]
|
#[arg(short = 'p', long)]
|
||||||
pub project: Option<String>,
|
pub project: Option<String>,
|
||||||
|
|
||||||
|
/// Override stale sync lock
|
||||||
#[arg(short = 'f', long, overrides_with = "no_force")]
|
#[arg(short = 'f', long, overrides_with = "no_force")]
|
||||||
pub force: bool,
|
pub force: bool,
|
||||||
|
|
||||||
#[arg(long = "no-force", hide = true, overrides_with = "force")]
|
#[arg(long = "no-force", hide = true, overrides_with = "force")]
|
||||||
pub no_force: bool,
|
pub no_force: bool,
|
||||||
|
|
||||||
|
/// Full re-sync: reset cursors and fetch all data from scratch
|
||||||
#[arg(long, overrides_with = "no_full")]
|
#[arg(long, overrides_with = "no_full")]
|
||||||
pub full: bool,
|
pub full: bool,
|
||||||
|
|
||||||
#[arg(long = "no-full", hide = true, overrides_with = "full")]
|
#[arg(long = "no-full", hide = true, overrides_with = "full")]
|
||||||
pub no_full: bool,
|
pub no_full: bool,
|
||||||
|
|
||||||
|
/// Preview what would be synced without making changes
|
||||||
|
#[arg(long, overrides_with = "no_dry_run")]
|
||||||
|
pub dry_run: bool,
|
||||||
|
|
||||||
|
#[arg(long = "no-dry-run", hide = true, overrides_with = "dry_run")]
|
||||||
|
pub no_dry_run: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
pub struct StatsArgs {
|
pub struct StatsArgs {
|
||||||
|
/// Run integrity checks
|
||||||
#[arg(long, overrides_with = "no_check")]
|
#[arg(long, overrides_with = "no_check")]
|
||||||
pub check: bool,
|
pub check: bool,
|
||||||
|
|
||||||
#[arg(long = "no-check", hide = true, overrides_with = "check")]
|
#[arg(long = "no-check", hide = true, overrides_with = "check")]
|
||||||
pub no_check: bool,
|
pub no_check: bool,
|
||||||
|
|
||||||
|
/// Repair integrity issues (auto-enables --check)
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub repair: bool,
|
pub repair: bool,
|
||||||
|
|
||||||
|
/// Preview what would be repaired without making changes (requires --repair)
|
||||||
|
#[arg(long, overrides_with = "no_dry_run")]
|
||||||
|
pub dry_run: bool,
|
||||||
|
|
||||||
|
#[arg(long = "no-dry-run", hide = true, overrides_with = "dry_run")]
|
||||||
|
pub no_dry_run: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
pub struct SearchArgs {
|
pub struct SearchArgs {
|
||||||
|
/// Search query string
|
||||||
pub query: String,
|
pub query: String,
|
||||||
|
|
||||||
|
/// Search mode (lexical, hybrid, semantic)
|
||||||
#[arg(long, default_value = "hybrid", value_parser = ["lexical", "hybrid", "semantic"], help_heading = "Output")]
|
#[arg(long, default_value = "hybrid", value_parser = ["lexical", "hybrid", "semantic"], help_heading = "Output")]
|
||||||
pub mode: String,
|
pub mode: String,
|
||||||
|
|
||||||
|
/// Filter by source type (issue, mr, discussion)
|
||||||
#[arg(long = "type", value_name = "TYPE", value_parser = ["issue", "mr", "discussion"], help_heading = "Filters")]
|
#[arg(long = "type", value_name = "TYPE", value_parser = ["issue", "mr", "discussion"], help_heading = "Filters")]
|
||||||
pub source_type: Option<String>,
|
pub source_type: Option<String>,
|
||||||
|
|
||||||
|
/// Filter by author username
|
||||||
#[arg(long, help_heading = "Filters")]
|
#[arg(long, help_heading = "Filters")]
|
||||||
pub author: Option<String>,
|
pub author: Option<String>,
|
||||||
|
|
||||||
|
/// Filter by project path
|
||||||
#[arg(short = 'p', long, help_heading = "Filters")]
|
#[arg(short = 'p', long, help_heading = "Filters")]
|
||||||
pub project: Option<String>,
|
pub project: Option<String>,
|
||||||
|
|
||||||
|
/// Filter by label (repeatable, AND logic)
|
||||||
#[arg(long, action = clap::ArgAction::Append, help_heading = "Filters")]
|
#[arg(long, action = clap::ArgAction::Append, help_heading = "Filters")]
|
||||||
pub label: Vec<String>,
|
pub label: Vec<String>,
|
||||||
|
|
||||||
|
/// Filter by file path (trailing / for prefix match)
|
||||||
#[arg(long, help_heading = "Filters")]
|
#[arg(long, help_heading = "Filters")]
|
||||||
pub path: Option<String>,
|
pub path: Option<String>,
|
||||||
|
|
||||||
|
/// Filter by created after (7d, 2w, or YYYY-MM-DD)
|
||||||
#[arg(long, help_heading = "Filters")]
|
#[arg(long, help_heading = "Filters")]
|
||||||
pub after: Option<String>,
|
pub after: Option<String>,
|
||||||
|
|
||||||
|
/// Filter by updated after (7d, 2w, or YYYY-MM-DD)
|
||||||
#[arg(long = "updated-after", help_heading = "Filters")]
|
#[arg(long = "updated-after", help_heading = "Filters")]
|
||||||
pub updated_after: Option<String>,
|
pub updated_after: Option<String>,
|
||||||
|
|
||||||
|
/// Maximum results (default 20, max 100)
|
||||||
#[arg(
|
#[arg(
|
||||||
short = 'n',
|
short = 'n',
|
||||||
long = "limit",
|
long = "limit",
|
||||||
@@ -381,57 +520,75 @@ pub struct SearchArgs {
|
|||||||
)]
|
)]
|
||||||
pub limit: usize,
|
pub limit: usize,
|
||||||
|
|
||||||
|
/// Show ranking explanation per result
|
||||||
#[arg(long, help_heading = "Output", overrides_with = "no_explain")]
|
#[arg(long, help_heading = "Output", overrides_with = "no_explain")]
|
||||||
pub explain: bool,
|
pub explain: bool,
|
||||||
|
|
||||||
#[arg(long = "no-explain", hide = true, overrides_with = "explain")]
|
#[arg(long = "no-explain", hide = true, overrides_with = "explain")]
|
||||||
pub no_explain: bool,
|
pub no_explain: bool,
|
||||||
|
|
||||||
|
/// FTS query mode: safe (default) or raw
|
||||||
#[arg(long = "fts-mode", default_value = "safe", value_parser = ["safe", "raw"], help_heading = "Output")]
|
#[arg(long = "fts-mode", default_value = "safe", value_parser = ["safe", "raw"], help_heading = "Output")]
|
||||||
pub fts_mode: String,
|
pub fts_mode: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
pub struct GenerateDocsArgs {
|
pub struct GenerateDocsArgs {
|
||||||
|
/// Full rebuild: seed all entities into dirty queue, then drain
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub full: bool,
|
pub full: bool,
|
||||||
|
|
||||||
|
/// Filter to single project
|
||||||
#[arg(short = 'p', long)]
|
#[arg(short = 'p', long)]
|
||||||
pub project: Option<String>,
|
pub project: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
pub struct SyncArgs {
|
pub struct SyncArgs {
|
||||||
|
/// Reset cursors, fetch everything
|
||||||
#[arg(long, overrides_with = "no_full")]
|
#[arg(long, overrides_with = "no_full")]
|
||||||
pub full: bool,
|
pub full: bool,
|
||||||
|
|
||||||
#[arg(long = "no-full", hide = true, overrides_with = "full")]
|
#[arg(long = "no-full", hide = true, overrides_with = "full")]
|
||||||
pub no_full: bool,
|
pub no_full: bool,
|
||||||
|
|
||||||
|
/// Override stale lock
|
||||||
#[arg(long, overrides_with = "no_force")]
|
#[arg(long, overrides_with = "no_force")]
|
||||||
pub force: bool,
|
pub force: bool,
|
||||||
|
|
||||||
#[arg(long = "no-force", hide = true, overrides_with = "force")]
|
#[arg(long = "no-force", hide = true, overrides_with = "force")]
|
||||||
pub no_force: bool,
|
pub no_force: bool,
|
||||||
|
|
||||||
|
/// Skip embedding step
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub no_embed: bool,
|
pub no_embed: bool,
|
||||||
|
|
||||||
|
/// Skip document regeneration
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub no_docs: bool,
|
pub no_docs: bool,
|
||||||
|
|
||||||
|
/// Skip resource event fetching (overrides config)
|
||||||
#[arg(long = "no-events")]
|
#[arg(long = "no-events")]
|
||||||
pub no_events: bool,
|
pub no_events: bool,
|
||||||
|
|
||||||
|
/// Preview what would be synced without making changes
|
||||||
|
#[arg(long, overrides_with = "no_dry_run")]
|
||||||
|
pub dry_run: bool,
|
||||||
|
|
||||||
|
#[arg(long = "no-dry-run", hide = true, overrides_with = "dry_run")]
|
||||||
|
pub no_dry_run: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
pub struct EmbedArgs {
|
pub struct EmbedArgs {
|
||||||
|
/// Re-embed all documents (clears existing embeddings first)
|
||||||
#[arg(long, overrides_with = "no_full")]
|
#[arg(long, overrides_with = "no_full")]
|
||||||
pub full: bool,
|
pub full: bool,
|
||||||
|
|
||||||
#[arg(long = "no-full", hide = true, overrides_with = "full")]
|
#[arg(long = "no-full", hide = true, overrides_with = "full")]
|
||||||
pub no_full: bool,
|
pub no_full: bool,
|
||||||
|
|
||||||
|
/// Retry previously failed embeddings
|
||||||
#[arg(long, overrides_with = "no_retry_failed")]
|
#[arg(long, overrides_with = "no_retry_failed")]
|
||||||
pub retry_failed: bool,
|
pub retry_failed: bool,
|
||||||
|
|
||||||
@@ -441,9 +598,11 @@ pub struct EmbedArgs {
|
|||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
pub struct CountArgs {
|
pub struct CountArgs {
|
||||||
|
/// Entity type to count (issues, mrs, discussions, notes, events)
|
||||||
#[arg(value_parser = ["issues", "mrs", "discussions", "notes", "events"])]
|
#[arg(value_parser = ["issues", "mrs", "discussions", "notes", "events"])]
|
||||||
pub entity: String,
|
pub entity: String,
|
||||||
|
|
||||||
|
/// Parent type filter: issue or mr (for discussions/notes)
|
||||||
#[arg(short = 'f', long = "for", value_parser = ["issue", "mr"])]
|
#[arg(short = 'f', long = "for", value_parser = ["issue", "mr"])]
|
||||||
pub for_entity: Option<String>,
|
pub for_entity: Option<String>,
|
||||||
}
|
}
|
||||||
|
|||||||
395
src/main.rs
395
src/main.rs
@@ -2,6 +2,7 @@ use clap::Parser;
|
|||||||
use console::style;
|
use console::style;
|
||||||
use dialoguer::{Confirm, Input};
|
use dialoguer::{Confirm, Input};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use strsim::jaro_winkler;
|
||||||
use tracing_subscriber::Layer;
|
use tracing_subscriber::Layer;
|
||||||
use tracing_subscriber::layer::SubscriberExt;
|
use tracing_subscriber::layer::SubscriberExt;
|
||||||
use tracing_subscriber::util::SubscriberInitExt;
|
use tracing_subscriber::util::SubscriberInitExt;
|
||||||
@@ -10,13 +11,14 @@ use lore::Config;
|
|||||||
use lore::cli::commands::{
|
use lore::cli::commands::{
|
||||||
IngestDisplay, InitInputs, InitOptions, InitResult, ListFilters, MrListFilters,
|
IngestDisplay, InitInputs, InitOptions, InitResult, ListFilters, MrListFilters,
|
||||||
SearchCliFilters, SyncOptions, open_issue_in_browser, open_mr_in_browser, print_count,
|
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_count_json, print_doctor_results, print_dry_run_preview, print_dry_run_preview_json,
|
||||||
print_event_count_json, print_generate_docs, print_generate_docs_json, print_ingest_summary,
|
print_embed, print_embed_json, print_event_count, print_event_count_json, print_generate_docs,
|
||||||
print_ingest_summary_json, print_list_issues, print_list_issues_json, print_list_mrs,
|
print_generate_docs_json, print_ingest_summary, print_ingest_summary_json, print_list_issues,
|
||||||
print_list_mrs_json, print_search_results, print_search_results_json, print_show_issue,
|
print_list_issues_json, print_list_mrs, print_list_mrs_json, print_search_results,
|
||||||
print_show_issue_json, print_show_mr, print_show_mr_json, print_stats, print_stats_json,
|
print_search_results_json, print_show_issue, print_show_issue_json, print_show_mr,
|
||||||
print_sync, print_sync_json, print_sync_status, print_sync_status_json, run_auth_test,
|
print_show_mr_json, print_stats, print_stats_json, print_sync, print_sync_json,
|
||||||
run_count, run_count_events, run_doctor, run_embed, run_generate_docs, run_ingest, run_init,
|
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_list_issues, run_list_mrs, run_search, run_show_issue, run_show_mr, run_stats, run_sync,
|
||||||
run_sync_status,
|
run_sync_status,
|
||||||
};
|
};
|
||||||
@@ -40,7 +42,15 @@ async fn main() {
|
|||||||
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
|
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 robot_mode = cli.is_robot_mode();
|
||||||
|
|
||||||
let logging_config = lore::Config::load(cli.config.as_deref())
|
let logging_config = lore::Config::load(cli.config.as_deref())
|
||||||
@@ -127,15 +137,29 @@ async fn main() {
|
|||||||
let quiet = cli.quiet;
|
let quiet = cli.quiet;
|
||||||
|
|
||||||
let result = match cli.command {
|
let result = match cli.command {
|
||||||
Commands::Issues(args) => handle_issues(cli.config.as_deref(), args, robot_mode),
|
// Phase 2: Handle no-args case - in robot mode, output robot-docs; otherwise show help
|
||||||
Commands::Mrs(args) => handle_mrs(cli.config.as_deref(), args, robot_mode),
|
None => {
|
||||||
Commands::Search(args) => handle_search(cli.config.as_deref(), args, robot_mode).await,
|
if robot_mode {
|
||||||
Commands::Stats(args) => handle_stats(cli.config.as_deref(), args, robot_mode).await,
|
handle_robot_docs(robot_mode)
|
||||||
Commands::Embed(args) => handle_embed(cli.config.as_deref(), args, robot_mode).await,
|
} else {
|
||||||
Commands::Sync(args) => {
|
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
|
handle_sync_cmd(cli.config.as_deref(), args, robot_mode, &metrics_layer).await
|
||||||
}
|
}
|
||||||
Commands::Ingest(args) => {
|
Some(Commands::Ingest(args)) => {
|
||||||
handle_ingest(
|
handle_ingest(
|
||||||
cli.config.as_deref(),
|
cli.config.as_deref(),
|
||||||
args,
|
args,
|
||||||
@@ -145,19 +169,19 @@ async fn main() {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
Commands::Count(args) => handle_count(cli.config.as_deref(), args, robot_mode).await,
|
Some(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,
|
Some(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,
|
Some(Commands::Auth) => handle_auth_test(cli.config.as_deref(), robot_mode).await,
|
||||||
Commands::Doctor => handle_doctor(cli.config.as_deref(), robot_mode).await,
|
Some(Commands::Doctor) => handle_doctor(cli.config.as_deref(), robot_mode).await,
|
||||||
Commands::Version => handle_version(robot_mode),
|
Some(Commands::Version) => handle_version(robot_mode),
|
||||||
Commands::Completions { shell } => handle_completions(&shell),
|
Some(Commands::Completions { shell }) => handle_completions(&shell),
|
||||||
Commands::Init {
|
Some(Commands::Init {
|
||||||
force,
|
force,
|
||||||
non_interactive,
|
non_interactive,
|
||||||
gitlab_url,
|
gitlab_url,
|
||||||
token_env_var,
|
token_env_var,
|
||||||
projects,
|
projects,
|
||||||
} => {
|
}) => {
|
||||||
handle_init(
|
handle_init(
|
||||||
cli.config.as_deref(),
|
cli.config.as_deref(),
|
||||||
force,
|
force,
|
||||||
@@ -169,16 +193,16 @@ async fn main() {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
Commands::GenerateDocs(args) => {
|
Some(Commands::GenerateDocs(args)) => {
|
||||||
handle_generate_docs(cli.config.as_deref(), args, robot_mode).await
|
handle_generate_docs(cli.config.as_deref(), args, robot_mode).await
|
||||||
}
|
}
|
||||||
Commands::Backup => handle_backup(robot_mode),
|
Some(Commands::Backup) => handle_backup(robot_mode),
|
||||||
Commands::Reset { yes: _ } => handle_reset(robot_mode),
|
Some(Commands::Reset { yes: _ }) => handle_reset(robot_mode),
|
||||||
Commands::Migrate => handle_migrate(cli.config.as_deref(), robot_mode).await,
|
Some(Commands::Migrate) => handle_migrate(cli.config.as_deref(), robot_mode).await,
|
||||||
Commands::Health => handle_health(cli.config.as_deref(), robot_mode).await,
|
Some(Commands::Health) => handle_health(cli.config.as_deref(), robot_mode).await,
|
||||||
Commands::RobotDocs => handle_robot_docs(robot_mode),
|
Some(Commands::RobotDocs) => handle_robot_docs(robot_mode),
|
||||||
|
|
||||||
Commands::List {
|
Some(Commands::List {
|
||||||
entity,
|
entity,
|
||||||
limit,
|
limit,
|
||||||
project,
|
project,
|
||||||
@@ -198,7 +222,7 @@ async fn main() {
|
|||||||
reviewer,
|
reviewer,
|
||||||
target_branch,
|
target_branch,
|
||||||
source_branch,
|
source_branch,
|
||||||
} => {
|
}) => {
|
||||||
if !robot_mode {
|
if !robot_mode {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"{}",
|
"{}",
|
||||||
@@ -231,11 +255,11 @@ async fn main() {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
Commands::Show {
|
Some(Commands::Show {
|
||||||
entity,
|
entity,
|
||||||
iid,
|
iid,
|
||||||
project,
|
project,
|
||||||
} => {
|
}) => {
|
||||||
if !robot_mode {
|
if !robot_mode {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"{}",
|
"{}",
|
||||||
@@ -255,7 +279,7 @@ async fn main() {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
Commands::AuthTest => {
|
Some(Commands::AuthTest) => {
|
||||||
if !robot_mode {
|
if !robot_mode {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"{}",
|
"{}",
|
||||||
@@ -264,7 +288,7 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
handle_auth_test(cli.config.as_deref(), robot_mode).await
|
handle_auth_test(cli.config.as_deref(), robot_mode).await
|
||||||
}
|
}
|
||||||
Commands::SyncStatus => {
|
Some(Commands::SyncStatus) => {
|
||||||
if !robot_mode {
|
if !robot_mode {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"{}",
|
"{}",
|
||||||
@@ -338,11 +362,143 @@ fn handle_error(e: Box<dyn std::error::Error>, robot_mode: bool) -> ! {
|
|||||||
std::process::exit(1);
|
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(
|
fn handle_issues(
|
||||||
config_override: Option<&str>,
|
config_override: Option<&str>,
|
||||||
args: IssuesArgs,
|
args: IssuesArgs,
|
||||||
robot_mode: bool,
|
robot_mode: bool,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> 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 config = Config::load(config_override)?;
|
||||||
let asc = args.asc && !args.no_asc;
|
let asc = args.asc && !args.no_asc;
|
||||||
let has_due = args.has_due && !args.no_has_due;
|
let has_due = args.has_due && !args.no_has_due;
|
||||||
@@ -391,6 +547,14 @@ fn handle_mrs(
|
|||||||
args: MrsArgs,
|
args: MrsArgs,
|
||||||
robot_mode: bool,
|
robot_mode: bool,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> 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 config = Config::load(config_override)?;
|
||||||
let asc = args.asc && !args.no_asc;
|
let asc = args.asc && !args.no_asc;
|
||||||
let open = args.open && !args.no_open;
|
let open = args.open && !args.no_open;
|
||||||
@@ -442,16 +606,47 @@ async fn handle_ingest(
|
|||||||
quiet: bool,
|
quiet: bool,
|
||||||
metrics: &MetricsLayer,
|
metrics: &MetricsLayer,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let dry_run = args.dry_run && !args.no_dry_run;
|
||||||
let config = Config::load(config_override)?;
|
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 {
|
let display = if robot_mode || quiet {
|
||||||
IngestDisplay::silent()
|
IngestDisplay::silent()
|
||||||
} else {
|
} else {
|
||||||
IngestDisplay::interactive()
|
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 entity_label = args.entity.as_deref().unwrap_or("all");
|
||||||
let command = format!("ingest:{entity_label}");
|
let command = format!("ingest:{entity_label}");
|
||||||
let db_path = get_db_path(config.storage.db_path.as_deref());
|
let db_path = get_db_path(config.storage.db_path.as_deref());
|
||||||
@@ -469,6 +664,7 @@ async fn handle_ingest(
|
|||||||
args.project.as_deref(),
|
args.project.as_deref(),
|
||||||
force,
|
force,
|
||||||
full,
|
full,
|
||||||
|
false,
|
||||||
display,
|
display,
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
@@ -495,6 +691,7 @@ async fn handle_ingest(
|
|||||||
args.project.as_deref(),
|
args.project.as_deref(),
|
||||||
force,
|
force,
|
||||||
full,
|
full,
|
||||||
|
false,
|
||||||
display,
|
display,
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
@@ -506,6 +703,7 @@ async fn handle_ingest(
|
|||||||
args.project.as_deref(),
|
args.project.as_deref(),
|
||||||
force,
|
force,
|
||||||
full,
|
full,
|
||||||
|
false,
|
||||||
display,
|
display,
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
@@ -592,6 +790,35 @@ fn print_combined_ingest_json(
|
|||||||
println!("{}", serde_json::to_string(&output).unwrap());
|
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(
|
async fn handle_count(
|
||||||
config_override: Option<&str>,
|
config_override: Option<&str>,
|
||||||
args: CountArgs,
|
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(
|
async fn handle_doctor(
|
||||||
config_override: Option<&str>,
|
config_override: Option<&str>,
|
||||||
robot_mode: bool,
|
robot_mode: bool,
|
||||||
@@ -928,7 +1167,14 @@ async fn handle_doctor(
|
|||||||
let result = run_doctor(config_override).await;
|
let result = run_doctor(config_override).await;
|
||||||
|
|
||||||
if robot_mode {
|
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 {
|
} else {
|
||||||
print_doctor_results(&result);
|
print_doctor_results(&result);
|
||||||
}
|
}
|
||||||
@@ -1133,9 +1379,10 @@ async fn handle_stats(
|
|||||||
args: StatsArgs,
|
args: StatsArgs,
|
||||||
robot_mode: bool,
|
robot_mode: bool,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let dry_run = args.dry_run && !args.no_dry_run;
|
||||||
let config = Config::load(config_override)?;
|
let config = Config::load(config_override)?;
|
||||||
let check = (args.check && !args.no_check) || args.repair;
|
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 {
|
if robot_mode {
|
||||||
print_stats_json(&result);
|
print_stats_json(&result);
|
||||||
} else {
|
} else {
|
||||||
@@ -1219,6 +1466,8 @@ async fn handle_sync_cmd(
|
|||||||
robot_mode: bool,
|
robot_mode: bool,
|
||||||
metrics: &MetricsLayer,
|
metrics: &MetricsLayer,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let dry_run = args.dry_run && !args.no_dry_run;
|
||||||
|
|
||||||
let mut config = Config::load(config_override)?;
|
let mut config = Config::load(config_override)?;
|
||||||
if args.no_events {
|
if args.no_events {
|
||||||
config.sync.fetch_resource_events = false;
|
config.sync.fetch_resource_events = false;
|
||||||
@@ -1230,8 +1479,15 @@ async fn handle_sync_cmd(
|
|||||||
no_docs: args.no_docs,
|
no_docs: args.no_docs,
|
||||||
no_events: args.no_events,
|
no_events: args.no_events,
|
||||||
robot_mode,
|
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 db_path = get_db_path(config.storage.db_path.as_deref());
|
||||||
let recorder_conn = create_connection(&db_path)?;
|
let recorder_conn = create_connection(&db_path)?;
|
||||||
let run_id = uuid::Uuid::new_v4().simple().to_string();
|
let run_id = uuid::Uuid::new_v4().simple().to_string();
|
||||||
@@ -1371,7 +1627,11 @@ struct RobotDocsData {
|
|||||||
description: String,
|
description: String,
|
||||||
activation: RobotDocsActivation,
|
activation: RobotDocsActivation,
|
||||||
commands: serde_json::Value,
|
commands: serde_json::Value,
|
||||||
|
/// Deprecated command aliases (old -> new)
|
||||||
|
aliases: serde_json::Value,
|
||||||
exit_codes: serde_json::Value,
|
exit_codes: serde_json::Value,
|
||||||
|
/// Error codes emitted by clap parse failures
|
||||||
|
clap_error_codes: serde_json::Value,
|
||||||
error_format: String,
|
error_format: String,
|
||||||
workflows: serde_json::Value,
|
workflows: serde_json::Value,
|
||||||
}
|
}
|
||||||
@@ -1410,37 +1670,37 @@ fn handle_robot_docs(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>>
|
|||||||
},
|
},
|
||||||
"ingest": {
|
"ingest": {
|
||||||
"description": "Sync data from GitLab",
|
"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"
|
"example": "lore --robot ingest issues --project group/repo"
|
||||||
},
|
},
|
||||||
"sync": {
|
"sync": {
|
||||||
"description": "Full sync pipeline: ingest -> generate-docs -> embed",
|
"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"
|
"example": "lore --robot sync"
|
||||||
},
|
},
|
||||||
"issues": {
|
"issues": {
|
||||||
"description": "List or show 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"
|
"example": "lore --robot issues --state opened --limit 10"
|
||||||
},
|
},
|
||||||
"mrs": {
|
"mrs": {
|
||||||
"description": "List or show merge requests",
|
"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"
|
"example": "lore --robot mrs --state opened"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"description": "Search indexed documents (lexical, hybrid, semantic)",
|
"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"
|
"example": "lore --robot search 'authentication bug' --mode hybrid --limit 10"
|
||||||
},
|
},
|
||||||
"count": {
|
"count": {
|
||||||
"description": "Count entities in local database",
|
"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"
|
"example": "lore --robot count issues"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"description": "Show document and index statistics",
|
"description": "Show document and index statistics",
|
||||||
"flags": ["--check", "--repair"],
|
"flags": ["--check", "--no-check", "--repair", "--dry-run", "--no-dry-run"],
|
||||||
"example": "lore --robot stats"
|
"example": "lore --robot stats"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
@@ -1450,12 +1710,12 @@ fn handle_robot_docs(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>>
|
|||||||
},
|
},
|
||||||
"generate-docs": {
|
"generate-docs": {
|
||||||
"description": "Generate searchable documents from ingested data",
|
"description": "Generate searchable documents from ingested data",
|
||||||
"flags": ["--full", "--project <path>"],
|
"flags": ["--full", "-p/--project <path>"],
|
||||||
"example": "lore --robot generate-docs --full"
|
"example": "lore --robot generate-docs --full"
|
||||||
},
|
},
|
||||||
"embed": {
|
"embed": {
|
||||||
"description": "Generate vector embeddings for documents via Ollama",
|
"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"
|
"example": "lore --robot embed"
|
||||||
},
|
},
|
||||||
"migrate": {
|
"migrate": {
|
||||||
@@ -1468,6 +1728,11 @@ fn handle_robot_docs(robot_mode: bool) -> Result<(), Box<dyn std::error::Error>>
|
|||||||
"flags": [],
|
"flags": [],
|
||||||
"example": "lore --robot version"
|
"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": {
|
"robot-docs": {
|
||||||
"description": "This command (agent self-discovery manifest)",
|
"description": "This command (agent self-discovery manifest)",
|
||||||
"flags": [],
|
"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 {
|
let output = RobotDocsOutput {
|
||||||
ok: true,
|
ok: true,
|
||||||
data: RobotDocsData {
|
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(),
|
auto: "Non-TTY stdout".to_string(),
|
||||||
},
|
},
|
||||||
commands,
|
commands,
|
||||||
|
aliases,
|
||||||
exit_codes,
|
exit_codes,
|
||||||
|
clap_error_codes,
|
||||||
error_format: "stderr JSON: {\"error\":{\"code\":\"...\",\"message\":\"...\",\"suggestion\":\"...\"}}".to_string(),
|
error_format: "stderr JSON: {\"error\":{\"code\":\"...\",\"message\":\"...\",\"suggestion\":\"...\"}}".to_string(),
|
||||||
workflows,
|
workflows,
|
||||||
},
|
},
|
||||||
@@ -1639,14 +1930,14 @@ async fn handle_show_compat(
|
|||||||
entity: &str,
|
entity: &str,
|
||||||
iid: i64,
|
iid: i64,
|
||||||
project_filter: Option<&str>,
|
project_filter: Option<&str>,
|
||||||
json: bool,
|
robot_mode: bool,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let config = Config::load(config_override)?;
|
let config = Config::load(config_override)?;
|
||||||
|
|
||||||
match entity {
|
match entity {
|
||||||
"issue" => {
|
"issue" => {
|
||||||
let result = run_show_issue(&config, iid, project_filter)?;
|
let result = run_show_issue(&config, iid, project_filter)?;
|
||||||
if json {
|
if robot_mode {
|
||||||
print_show_issue_json(&result);
|
print_show_issue_json(&result);
|
||||||
} else {
|
} else {
|
||||||
print_show_issue(&result);
|
print_show_issue(&result);
|
||||||
@@ -1655,7 +1946,7 @@ async fn handle_show_compat(
|
|||||||
}
|
}
|
||||||
"mr" => {
|
"mr" => {
|
||||||
let result = run_show_mr(&config, iid, project_filter)?;
|
let result = run_show_mr(&config, iid, project_filter)?;
|
||||||
if json {
|
if robot_mode {
|
||||||
print_show_mr_json(&result);
|
print_show_mr_json(&result);
|
||||||
} else {
|
} else {
|
||||||
print_show_mr(&result);
|
print_show_mr(&result);
|
||||||
|
|||||||
Reference in New Issue
Block a user