feat(cli): Add --color, --quiet, --no-X negations, completions, and help headings

Global flags:
- --color (auto|always|never) for explicit color control
- --quiet/-q to suppress non-essential output
- Hidden Completions subcommand for bash/zsh/fish/powershell

Flag negation (--no-X) with overrides_with for: has-due, asc, open
(issues/mrs), force/full (ingest/sync), check (stats), explain (search),
retry-failed (embed). Enables scripted flag composition where later flags
override earlier ones.

Validation:
- value_parser on search --mode, --type, --fts-mode for early rejection
- Remove requires="check" from --repair (auto-enabled in handler)

Polish:
- help_heading groups (Filters, Sorting, Output, Actions) on issues,
  mrs, and search args for cleaner --help output
- Hide Backup, Reset, and Completions from --help

Co-Authored-By: Claude (us.anthropic.claude-opus-4-5-20251101-v1:0) <noreply@anthropic.com>
This commit is contained in:
Taylor Eernisse
2026-01-30 16:54:18 -05:00
parent 730ddef339
commit 585b746461

View File

@@ -22,6 +22,14 @@ pub struct Cli {
#[arg(short = 'J', long = "json", global = true)] #[arg(short = 'J', long = "json", global = true)]
pub json: bool, pub json: bool,
/// Color output: auto (default), always, or never
#[arg(long, global = true, value_parser = ["auto", "always", "never"], default_value = "auto")]
pub color: String,
/// Suppress non-essential output
#[arg(short = 'q', long, global = true)]
pub quiet: bool,
#[command(subcommand)] #[command(subcommand)]
pub command: Commands, pub command: Commands,
} }
@@ -84,9 +92,11 @@ pub enum Commands {
}, },
/// Create timestamped database backup /// Create timestamped database backup
#[command(hide = true)]
Backup, Backup,
/// Delete database and reset all state /// Delete database and reset all state
#[command(hide = true)]
Reset { Reset {
/// Skip confirmation prompt /// Skip confirmation prompt
#[arg(short = 'y', long)] #[arg(short = 'y', long)]
@@ -119,6 +129,14 @@ pub enum Commands {
#[command(name = "robot-docs")] #[command(name = "robot-docs")]
RobotDocs, RobotDocs,
/// Generate shell completions
#[command(hide = true)]
Completions {
/// Shell to generate completions for
#[arg(value_parser = ["bash", "zsh", "fish", "powershell"])]
shell: String,
},
// --- Hidden backward-compat aliases --- // --- Hidden backward-compat aliases ---
/// List issues or MRs (deprecated: use 'lore issues' or 'lore mrs') /// List issues or MRs (deprecated: use 'lore issues' or 'lore mrs')
#[command(hide = true)] #[command(hide = true)]
@@ -195,56 +213,65 @@ pub struct IssuesArgs {
pub iid: Option<i64>, pub iid: Option<i64>,
/// Maximum results /// Maximum results
#[arg(short = 'n', long = "limit", default_value = "50")] #[arg(short = 'n', long = "limit", default_value = "50", help_heading = "Output")]
pub limit: usize, pub limit: usize,
/// Filter by state (opened, closed, all) /// Filter by state (opened, closed, all)
#[arg(short = 's', long)] #[arg(short = 's', long, help_heading = "Filters")]
pub state: Option<String>, pub state: Option<String>,
/// Filter by project path /// Filter by project path
#[arg(short = 'p', long)] #[arg(short = 'p', long, help_heading = "Filters")]
pub project: Option<String>, pub project: Option<String>,
/// Filter by author username /// Filter by author username
#[arg(short = 'a', long)] #[arg(short = 'a', long, help_heading = "Filters")]
pub author: Option<String>, pub author: Option<String>,
/// Filter by assignee username /// Filter by assignee username
#[arg(short = 'A', long)] #[arg(short = 'A', long, help_heading = "Filters")]
pub assignee: Option<String>, pub assignee: Option<String>,
/// Filter by label (repeatable, AND logic) /// Filter by label (repeatable, AND logic)
#[arg(short = 'l', long)] #[arg(short = 'l', long, help_heading = "Filters")]
pub label: Option<Vec<String>>, pub label: Option<Vec<String>>,
/// Filter by milestone title /// Filter by milestone title
#[arg(short = 'm', long)] #[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) /// Filter by time (7d, 2w, 1m, or YYYY-MM-DD)
#[arg(long)] #[arg(long, help_heading = "Filters")]
pub since: Option<String>, pub since: Option<String>,
/// Filter by due date (before this date, YYYY-MM-DD) /// Filter by due date (before this date, YYYY-MM-DD)
#[arg(long = "due-before")] #[arg(long = "due-before", help_heading = "Filters")]
pub due_before: Option<String>, pub due_before: Option<String>,
/// Show only issues with a due date /// Show only issues with a due date
#[arg(long = "has-due")] #[arg(long = "has-due", help_heading = "Filters", overrides_with = "no_has_due")]
pub has_due: bool, pub has_due: bool,
#[arg(long = "no-has-due", hide = true, overrides_with = "has_due")]
pub no_has_due: bool,
/// Sort field (updated, created, iid) /// Sort field (updated, created, iid)
#[arg(long, value_parser = ["updated", "created", "iid"], default_value = "updated")] #[arg(long, value_parser = ["updated", "created", "iid"], default_value = "updated", help_heading = "Sorting")]
pub sort: String, pub sort: String,
/// Sort ascending (default: descending) /// Sort ascending (default: descending)
#[arg(long)] #[arg(long, help_heading = "Sorting", overrides_with = "no_asc")]
pub asc: bool, pub asc: bool,
#[arg(long = "no-asc", hide = true, overrides_with = "asc")]
pub no_asc: bool,
/// Open first matching item in browser /// Open first matching item in browser
#[arg(short = 'o', long)] #[arg(short = 'o', long, help_heading = "Actions", overrides_with = "no_open")]
pub open: bool, pub open: bool,
#[arg(long = "no-open", hide = true, overrides_with = "open")]
pub no_open: bool,
} }
/// Arguments for `lore mrs [IID]` /// Arguments for `lore mrs [IID]`
@@ -254,64 +281,70 @@ pub struct MrsArgs {
pub iid: Option<i64>, pub iid: Option<i64>,
/// Maximum results /// Maximum results
#[arg(short = 'n', long = "limit", default_value = "50")] #[arg(short = 'n', long = "limit", default_value = "50", help_heading = "Output")]
pub limit: usize, pub limit: usize,
/// Filter by state (opened, merged, closed, locked, all) /// Filter by state (opened, merged, closed, locked, all)
#[arg(short = 's', long)] #[arg(short = 's', long, help_heading = "Filters")]
pub state: Option<String>, pub state: Option<String>,
/// Filter by project path /// Filter by project path
#[arg(short = 'p', long)] #[arg(short = 'p', long, help_heading = "Filters")]
pub project: Option<String>, pub project: Option<String>,
/// Filter by author username /// Filter by author username
#[arg(short = 'a', long)] #[arg(short = 'a', long, help_heading = "Filters")]
pub author: Option<String>, pub author: Option<String>,
/// Filter by assignee username /// Filter by assignee username
#[arg(short = 'A', long)] #[arg(short = 'A', long, help_heading = "Filters")]
pub assignee: Option<String>, pub assignee: Option<String>,
/// Filter by reviewer username /// Filter by reviewer username
#[arg(short = 'r', long)] #[arg(short = 'r', long, help_heading = "Filters")]
pub reviewer: Option<String>, pub reviewer: Option<String>,
/// Filter by label (repeatable, AND logic) /// Filter by label (repeatable, AND logic)
#[arg(short = 'l', long)] #[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) /// Filter by time (7d, 2w, 1m, or YYYY-MM-DD)
#[arg(long)] #[arg(long, help_heading = "Filters")]
pub since: Option<String>, pub since: Option<String>,
/// Show only draft MRs /// Show only draft MRs
#[arg(short = 'd', long, conflicts_with = "no_draft")] #[arg(short = 'd', long, conflicts_with = "no_draft", help_heading = "Filters")]
pub draft: bool, pub draft: bool,
/// Exclude draft MRs /// Exclude draft MRs
#[arg(short = 'D', long = "no-draft", conflicts_with = "draft")] #[arg(short = 'D', long = "no-draft", conflicts_with = "draft", help_heading = "Filters")]
pub no_draft: bool, pub no_draft: bool,
/// Filter by target branch /// Filter by target branch
#[arg(long)] #[arg(long, help_heading = "Filters")]
pub target: Option<String>, pub target: Option<String>,
/// Filter by source branch /// Filter by source branch
#[arg(long)] #[arg(long, help_heading = "Filters")]
pub source: Option<String>, pub source: Option<String>,
/// Sort field (updated, created, iid) /// Sort field (updated, created, iid)
#[arg(long, value_parser = ["updated", "created", "iid"], default_value = "updated")] #[arg(long, value_parser = ["updated", "created", "iid"], default_value = "updated", help_heading = "Sorting")]
pub sort: String, pub sort: String,
/// Sort ascending (default: descending) /// Sort ascending (default: descending)
#[arg(long)] #[arg(long, help_heading = "Sorting", overrides_with = "no_asc")]
pub asc: bool, pub asc: bool,
#[arg(long = "no-asc", hide = true, overrides_with = "asc")]
pub no_asc: bool,
/// Open first matching item in browser /// Open first matching item in browser
#[arg(short = 'o', long)] #[arg(short = 'o', long, help_heading = "Actions", overrides_with = "no_open")]
pub open: bool, pub open: bool,
#[arg(long = "no-open", hide = true, overrides_with = "open")]
pub no_open: bool,
} }
/// Arguments for `lore ingest [ENTITY]` /// Arguments for `lore ingest [ENTITY]`
@@ -326,23 +359,32 @@ pub struct IngestArgs {
pub project: Option<String>, pub project: Option<String>,
/// Override stale sync lock /// Override stale sync lock
#[arg(short = 'f', long)] #[arg(short = 'f', long, overrides_with = "no_force")]
pub force: bool, pub force: bool,
#[arg(long = "no-force", hide = true, overrides_with = "force")]
pub no_force: bool,
/// Full re-sync: reset cursors and fetch all data from scratch /// Full re-sync: reset cursors and fetch all data from scratch
#[arg(long)] #[arg(long, overrides_with = "no_full")]
pub full: bool, pub full: bool,
#[arg(long = "no-full", hide = true, overrides_with = "full")]
pub no_full: bool,
} }
/// Arguments for `lore stats` /// Arguments for `lore stats`
#[derive(Parser)] #[derive(Parser)]
pub struct StatsArgs { pub struct StatsArgs {
/// Run integrity checks /// Run integrity checks
#[arg(long)] #[arg(long, overrides_with = "no_check")]
pub check: bool, pub check: bool,
/// Repair integrity issues (requires --check) #[arg(long = "no-check", hide = true, overrides_with = "check")]
#[arg(long, requires = "check")] pub no_check: bool,
/// Repair integrity issues (auto-enables --check)
#[arg(long)]
pub repair: bool, pub repair: bool,
} }
@@ -353,47 +395,50 @@ pub struct SearchArgs {
pub query: String, pub query: String,
/// Search mode (lexical, hybrid, semantic) /// Search mode (lexical, hybrid, semantic)
#[arg(long, default_value = "hybrid")] #[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) /// Filter by source type (issue, mr, discussion)
#[arg(long = "type", value_name = "TYPE")] #[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 /// Filter by author username
#[arg(long)] #[arg(long, help_heading = "Filters")]
pub author: Option<String>, pub author: Option<String>,
/// Filter by project path /// Filter by project path
#[arg(short = 'p', long)] #[arg(short = 'p', long, help_heading = "Filters")]
pub project: Option<String>, pub project: Option<String>,
/// Filter by label (repeatable, AND logic) /// Filter by label (repeatable, AND logic)
#[arg(long, action = clap::ArgAction::Append)] #[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) /// Filter by file path (trailing / for prefix match)
#[arg(long)] #[arg(long, help_heading = "Filters")]
pub path: Option<String>, pub path: Option<String>,
/// Filter by created after (7d, 2w, or YYYY-MM-DD) /// Filter by created after (7d, 2w, or YYYY-MM-DD)
#[arg(long)] #[arg(long, help_heading = "Filters")]
pub after: Option<String>, pub after: Option<String>,
/// Filter by updated after (7d, 2w, or YYYY-MM-DD) /// Filter by updated after (7d, 2w, or YYYY-MM-DD)
#[arg(long = "updated-after")] #[arg(long = "updated-after", help_heading = "Filters")]
pub updated_after: Option<String>, pub updated_after: Option<String>,
/// Maximum results (default 20, max 100) /// Maximum results (default 20, max 100)
#[arg(short = 'n', long = "limit", default_value = "20")] #[arg(short = 'n', long = "limit", default_value = "20", help_heading = "Output")]
pub limit: usize, pub limit: usize,
/// Show ranking explanation per result /// Show ranking explanation per result
#[arg(long)] #[arg(long, help_heading = "Output", overrides_with = "no_explain")]
pub explain: bool, pub explain: bool,
#[arg(long = "no-explain", hide = true, overrides_with = "explain")]
pub no_explain: bool,
/// FTS query mode: safe (default) or raw /// FTS query mode: safe (default) or raw
#[arg(long = "fts-mode", default_value = "safe")] #[arg(long = "fts-mode", default_value = "safe", value_parser = ["safe", "raw"], help_heading = "Output")]
pub fts_mode: String, pub fts_mode: String,
} }
@@ -413,13 +458,19 @@ pub struct GenerateDocsArgs {
#[derive(Parser)] #[derive(Parser)]
pub struct SyncArgs { pub struct SyncArgs {
/// Reset cursors, fetch everything /// Reset cursors, fetch everything
#[arg(long)] #[arg(long, overrides_with = "no_full")]
pub full: bool, pub full: bool,
#[arg(long = "no-full", hide = true, overrides_with = "full")]
pub no_full: bool,
/// Override stale lock /// Override stale lock
#[arg(long)] #[arg(long, overrides_with = "no_force")]
pub force: bool, pub force: bool,
#[arg(long = "no-force", hide = true, overrides_with = "force")]
pub no_force: bool,
/// Skip embedding step /// Skip embedding step
#[arg(long)] #[arg(long)]
pub no_embed: bool, pub no_embed: bool,
@@ -433,8 +484,11 @@ pub struct SyncArgs {
#[derive(Parser)] #[derive(Parser)]
pub struct EmbedArgs { pub struct EmbedArgs {
/// Retry previously failed embeddings /// Retry previously failed embeddings
#[arg(long)] #[arg(long, overrides_with = "no_retry_failed")]
pub retry_failed: bool, pub retry_failed: bool,
#[arg(long = "no-retry-failed", hide = true, overrides_with = "retry_failed")]
pub no_retry_failed: bool,
} }
/// Arguments for `lore count <ENTITY>` /// Arguments for `lore count <ENTITY>`