Add a new --mentions flag to the `lore me` command that surfaces items where the user is @-mentioned but NOT already assigned, authoring, or reviewing. This fills an important gap in the personal work dashboard: cross-team requests and callouts that don't show up in the standard issue/MR sections. Implementation details: - query_mentioned_in() scans notes for @username patterns, then filters out entities where the user is already an assignee, author, or reviewer - MentionedInItem type captures entity_type (issue/mr), iid, title, state, project path, attention state, and updated timestamp - Attention state computation marks items as needs_attention when there's recent activity from others - Recency cutoff (7 days) prevents surfacing stale mentions - Both human and robot renderers include the new section The robot mode schema adds mentioned_in array with me_mentions field preset for token-efficient output. Test coverage: - mentioned_in_finds_mention_on_unassigned_issue: basic case - mentioned_in_excludes_assigned_issue: no duplicate surfacing - mentioned_in_excludes_author_on_mr: author already sees in authored MRs - mentioned_in_excludes_reviewer_on_mr: reviewer already sees in reviewing - mentioned_in_uses_recency_cutoff: old mentions filtered - mentioned_in_respects_project_filter: scoping works Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1269 lines
42 KiB
Rust
1269 lines
42 KiB
Rust
pub mod autocorrect;
|
|
pub mod commands;
|
|
pub mod progress;
|
|
pub mod render;
|
|
pub mod robot;
|
|
|
|
use clap::{Args, Parser, Subcommand};
|
|
use std::io::IsTerminal;
|
|
|
|
#[derive(Parser)]
|
|
#[command(name = "lore")]
|
|
#[command(version = env!("LORE_VERSION"), about = "Local GitLab data management with semantic search", long_about = None)]
|
|
#[command(subcommand_required = false)]
|
|
#[command(infer_subcommands = true)]
|
|
#[command(after_long_help = "\x1b[1mEnvironment:\x1b[0m
|
|
GITLAB_TOKEN GitLab personal access token (or name set in config)
|
|
LORE_ROBOT Enable robot/JSON mode (non-empty, non-zero value)
|
|
LORE_CONFIG_PATH Override config file location
|
|
NO_COLOR Disable color output (any non-empty value)
|
|
LORE_ICONS Override icon set: nerd, unicode, or ascii
|
|
NERD_FONTS Enable Nerd Font icons when set to a non-empty value")]
|
|
pub struct Cli {
|
|
/// Path to config file
|
|
#[arg(short = 'c', long, global = true, help = "Path to config file")]
|
|
pub config: Option<String>,
|
|
|
|
/// 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,
|
|
|
|
/// JSON output (global shorthand)
|
|
#[arg(
|
|
short = 'J',
|
|
long = "json",
|
|
global = true,
|
|
help = "JSON output (global shorthand)"
|
|
)]
|
|
pub json: bool,
|
|
|
|
/// 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,
|
|
|
|
/// Icon set: nerd (Nerd Fonts), unicode, or ascii
|
|
#[arg(long, global = true, value_parser = ["nerd", "unicode", "ascii"], help = "Icon set: nerd (Nerd Fonts), unicode, or ascii")]
|
|
pub icons: Option<String>,
|
|
|
|
/// Suppress non-essential output
|
|
#[arg(
|
|
short = 'q',
|
|
long,
|
|
global = true,
|
|
overrides_with = "no_quiet",
|
|
help = "Suppress non-essential output"
|
|
)]
|
|
pub quiet: bool,
|
|
|
|
#[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)", overrides_with = "no_verbose")]
|
|
pub verbose: u8,
|
|
|
|
#[arg(
|
|
long = "no-verbose",
|
|
global = true,
|
|
hide = true,
|
|
overrides_with = "verbose"
|
|
)]
|
|
pub no_verbose: bool,
|
|
|
|
/// 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,
|
|
|
|
#[command(subcommand)]
|
|
pub command: Option<Commands>,
|
|
}
|
|
|
|
impl Cli {
|
|
pub fn is_robot_mode(&self) -> bool {
|
|
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.
|
|
/// Also catches common agent typos like `-robot` and `--Robot`.
|
|
pub fn detect_robot_mode_from_env() -> bool {
|
|
let args: Vec<String> = std::env::args().collect();
|
|
args.iter().any(|a| {
|
|
a == "-J"
|
|
|| a.eq_ignore_ascii_case("--robot")
|
|
|| a.eq_ignore_ascii_case("-robot")
|
|
|| a.eq_ignore_ascii_case("--json")
|
|
|| a.eq_ignore_ascii_case("-json")
|
|
}) || std::env::var("LORE_ROBOT")
|
|
.ok()
|
|
.is_some_and(|v| !v.is_empty() && v != "0" && v != "false")
|
|
|| !std::io::stdout().is_terminal()
|
|
}
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
#[allow(clippy::large_enum_variant)]
|
|
pub enum Commands {
|
|
/// List or show issues
|
|
#[command(visible_alias = "issue")]
|
|
Issues(IssuesArgs),
|
|
|
|
/// List or show merge requests
|
|
#[command(
|
|
visible_alias = "mr",
|
|
alias = "merge-requests",
|
|
alias = "merge-request"
|
|
)]
|
|
Mrs(MrsArgs),
|
|
|
|
/// List notes from discussions
|
|
#[command(visible_alias = "note")]
|
|
Notes(NotesArgs),
|
|
|
|
/// Ingest data from GitLab
|
|
Ingest(IngestArgs),
|
|
|
|
/// Count entities in local database
|
|
Count(CountArgs),
|
|
|
|
/// Show sync state
|
|
#[command(
|
|
visible_alias = "st",
|
|
after_help = "\x1b[1mExamples:\x1b[0m
|
|
lore status # Show last sync times per project
|
|
lore --robot status # JSON output for automation"
|
|
)]
|
|
Status,
|
|
|
|
/// Verify GitLab authentication
|
|
#[command(after_help = "\x1b[1mExamples:\x1b[0m
|
|
lore auth # Verify token and show user info
|
|
lore --robot auth # JSON output for automation")]
|
|
Auth,
|
|
|
|
/// Check environment health
|
|
#[command(after_help = "\x1b[1mExamples:\x1b[0m
|
|
lore doctor # Check config, token, database, Ollama
|
|
lore --robot doctor # JSON output for automation")]
|
|
Doctor,
|
|
|
|
/// Show version information
|
|
Version,
|
|
|
|
/// Initialize configuration and database
|
|
#[command(after_help = "\x1b[1mExamples:\x1b[0m
|
|
lore init # Interactive setup
|
|
lore init --refresh # Register projects from existing config
|
|
lore init --force # Overwrite existing config
|
|
lore --robot init --gitlab-url https://gitlab.com \\
|
|
--token-env-var GITLAB_TOKEN --projects group/repo # Non-interactive setup")]
|
|
Init {
|
|
/// Re-read config and register any new projects in the database
|
|
#[arg(long, conflicts_with = "force")]
|
|
refresh: bool,
|
|
|
|
/// Skip overwrite confirmation
|
|
#[arg(short = 'f', long)]
|
|
force: bool,
|
|
|
|
/// Fail if prompts would be shown
|
|
#[arg(long)]
|
|
non_interactive: bool,
|
|
|
|
/// GitLab base URL (required in robot mode)
|
|
#[arg(long)]
|
|
gitlab_url: Option<String>,
|
|
|
|
/// Environment variable name holding GitLab token (required in robot mode)
|
|
#[arg(long)]
|
|
token_env_var: Option<String>,
|
|
|
|
/// Comma-separated project paths (required in robot mode)
|
|
#[arg(long)]
|
|
projects: Option<String>,
|
|
|
|
/// Default project path (used when -p is omitted)
|
|
#[arg(long)]
|
|
default_project: Option<String>,
|
|
},
|
|
|
|
/// Back up local database (not yet implemented)
|
|
#[command(hide = true)]
|
|
Backup,
|
|
|
|
/// Reset local database (not yet implemented)
|
|
#[command(hide = true)]
|
|
Reset {
|
|
/// Skip confirmation prompt
|
|
#[arg(short = 'y', long)]
|
|
yes: bool,
|
|
},
|
|
|
|
/// Search indexed documents
|
|
#[command(visible_alias = "find", alias = "query")]
|
|
Search(SearchArgs),
|
|
|
|
/// Show document and index statistics
|
|
#[command(visible_alias = "stat")]
|
|
Stats(StatsArgs),
|
|
|
|
/// Generate searchable documents from ingested data
|
|
#[command(name = "generate-docs")]
|
|
GenerateDocs(GenerateDocsArgs),
|
|
|
|
/// Generate vector embeddings for documents via Ollama
|
|
Embed(EmbedArgs),
|
|
|
|
/// Run full sync pipeline: ingest -> generate-docs -> embed
|
|
Sync(SyncArgs),
|
|
|
|
/// Run pending database migrations
|
|
#[command(after_help = "\x1b[1mExamples:\x1b[0m
|
|
lore migrate # Apply pending migrations
|
|
lore --robot migrate # JSON output for automation")]
|
|
Migrate,
|
|
|
|
/// Quick health check: config, database, schema version
|
|
#[command(after_help = "\x1b[1mExamples:\x1b[0m
|
|
lore health # Quick pre-flight check (exit 0 = healthy)
|
|
lore --robot health # JSON output for automation")]
|
|
Health,
|
|
|
|
/// Machine-readable command manifest for agent self-discovery
|
|
#[command(name = "robot-docs")]
|
|
RobotDocs {
|
|
/// Omit response_schema from output (~60% smaller)
|
|
#[arg(long)]
|
|
brief: bool,
|
|
},
|
|
|
|
/// 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 {
|
|
/// Shell to generate completions for
|
|
#[arg(value_parser = ["bash", "zsh", "fish", "powershell"])]
|
|
shell: String,
|
|
},
|
|
|
|
/// Show a chronological timeline of events matching a query
|
|
Timeline(TimelineArgs),
|
|
|
|
/// People intelligence: experts, workload, active discussions, overlap
|
|
Who(WhoArgs),
|
|
|
|
/// Personal work dashboard: open issues, authored/reviewing MRs, activity
|
|
Me(MeArgs),
|
|
|
|
/// Show MRs that touched a file, with linked discussions
|
|
#[command(name = "file-history")]
|
|
FileHistory(FileHistoryArgs),
|
|
|
|
/// Trace why code was introduced: file -> MR -> issue -> discussion
|
|
Trace(TraceArgs),
|
|
|
|
/// Detect discussion divergence from original intent
|
|
#[command(after_help = "\x1b[1mExamples:\x1b[0m
|
|
lore drift issues 42 # Check drift on issue #42
|
|
lore drift issues 42 --threshold 0.3 # Custom similarity threshold
|
|
lore --robot drift issues 42 -p group/repo # JSON output, scoped to project")]
|
|
Drift {
|
|
/// Entity type (currently only "issues" supported)
|
|
#[arg(value_parser = ["issues"])]
|
|
entity_type: String,
|
|
|
|
/// Entity IID
|
|
iid: i64,
|
|
|
|
/// Similarity threshold for drift detection (0.0-1.0)
|
|
#[arg(long, default_value = "0.4")]
|
|
threshold: f32,
|
|
|
|
/// Scope to project (fuzzy match)
|
|
#[arg(short, long)]
|
|
project: Option<String>,
|
|
},
|
|
|
|
/// Find semantically related entities via vector search
|
|
#[command(after_help = "\x1b[1mExamples:\x1b[0m
|
|
lore related issues 42 # Find entities related to issue #42
|
|
lore related mrs 99 -p group/repo # Related to MR #99 in specific project
|
|
lore related 'authentication flow' # Find entities matching free text query
|
|
lore --robot related issues 42 -n 5 # JSON output, limit 5 results")]
|
|
Related {
|
|
/// Entity type (issues, mrs) or free text query
|
|
query_or_type: String,
|
|
|
|
/// Entity IID (required when first arg is entity type)
|
|
iid: Option<i64>,
|
|
|
|
/// Maximum results
|
|
#[arg(short = 'n', long, default_value = "10")]
|
|
limit: usize,
|
|
|
|
/// Scope to project (fuzzy match)
|
|
#[arg(short, long)]
|
|
project: Option<String>,
|
|
},
|
|
|
|
/// Manage cron-based automatic syncing
|
|
#[command(after_help = "\x1b[1mExamples:\x1b[0m
|
|
lore cron install # Install cron job (every 8 minutes)
|
|
lore cron install --interval 15 # Custom interval
|
|
lore cron status # Check if cron is installed
|
|
lore cron uninstall # Remove cron job")]
|
|
Cron(CronArgs),
|
|
|
|
/// Manage stored GitLab token
|
|
#[command(after_help = "\x1b[1mExamples:\x1b[0m
|
|
lore token set # Interactive token entry + validation
|
|
lore token set --token glpat-xxx # Non-interactive token storage
|
|
echo glpat-xxx | lore token set # Pipe token from stdin
|
|
lore token show # Show token (masked)
|
|
lore token show --unmask # Show full token")]
|
|
Token(TokenArgs),
|
|
|
|
#[command(hide = true)]
|
|
List {
|
|
#[arg(value_parser = ["issues", "mrs"])]
|
|
entity: String,
|
|
|
|
#[arg(long, default_value = "50")]
|
|
limit: usize,
|
|
#[arg(long)]
|
|
project: Option<String>,
|
|
#[arg(long)]
|
|
state: Option<String>,
|
|
#[arg(long)]
|
|
author: Option<String>,
|
|
#[arg(long)]
|
|
assignee: Option<String>,
|
|
#[arg(long)]
|
|
label: Option<Vec<String>>,
|
|
#[arg(long)]
|
|
milestone: Option<String>,
|
|
#[arg(long)]
|
|
since: Option<String>,
|
|
#[arg(long)]
|
|
due_before: Option<String>,
|
|
#[arg(long)]
|
|
has_due_date: bool,
|
|
#[arg(long, value_parser = ["updated", "created", "iid"], default_value = "updated")]
|
|
sort: String,
|
|
#[arg(long, value_parser = ["desc", "asc"], default_value = "desc")]
|
|
order: String,
|
|
#[arg(long)]
|
|
open: bool,
|
|
#[arg(long, conflicts_with = "no_draft")]
|
|
draft: bool,
|
|
#[arg(long, conflicts_with = "draft")]
|
|
no_draft: bool,
|
|
#[arg(long)]
|
|
reviewer: Option<String>,
|
|
#[arg(long)]
|
|
target_branch: Option<String>,
|
|
#[arg(long)]
|
|
source_branch: Option<String>,
|
|
},
|
|
|
|
#[command(hide = true)]
|
|
Show {
|
|
#[arg(value_parser = ["issue", "mr"])]
|
|
entity: String,
|
|
|
|
iid: i64,
|
|
|
|
#[arg(long)]
|
|
project: Option<String>,
|
|
},
|
|
|
|
#[command(hide = true, name = "auth-test")]
|
|
AuthTest,
|
|
|
|
#[command(hide = true, name = "sync-status")]
|
|
SyncStatus,
|
|
}
|
|
|
|
#[derive(Parser)]
|
|
#[command(after_help = "\x1b[1mExamples:\x1b[0m
|
|
lore issues -n 10 # List 10 most recently updated issues
|
|
lore issues -s opened -l bug # Open issues labeled 'bug'
|
|
lore issues 42 -p group/repo # Show issue #42 in a specific project
|
|
lore issues --since 7d -a jsmith # Issues updated in last 7 days by jsmith")]
|
|
pub struct IssuesArgs {
|
|
/// Issue IID (omit to list, provide to show details)
|
|
pub iid: Option<i64>,
|
|
|
|
/// Maximum results
|
|
#[arg(
|
|
short = 'n',
|
|
long = "limit",
|
|
default_value = "50",
|
|
help_heading = "Output"
|
|
)]
|
|
pub limit: usize,
|
|
|
|
/// Select output fields (comma-separated, or 'minimal' preset: iid,title,state,updated_at_iso)
|
|
#[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", value_parser = ["opened", "closed", "all"])]
|
|
pub state: Option<String>,
|
|
|
|
/// Filter by project path
|
|
#[arg(short = 'p', long, help_heading = "Filters")]
|
|
pub project: Option<String>,
|
|
|
|
/// Filter by author username
|
|
#[arg(short = 'a', long, help_heading = "Filters")]
|
|
pub author: Option<String>,
|
|
|
|
/// Filter by assignee username
|
|
#[arg(short = 'A', long, help_heading = "Filters")]
|
|
pub assignee: Option<String>,
|
|
|
|
/// Filter by label (repeatable, AND logic)
|
|
#[arg(short = 'l', long, help_heading = "Filters")]
|
|
pub label: Option<Vec<String>>,
|
|
|
|
/// Filter by milestone title
|
|
#[arg(short = 'm', long, help_heading = "Filters")]
|
|
pub milestone: Option<String>,
|
|
|
|
/// Filter by work-item status name (repeatable, OR logic)
|
|
#[arg(long, help_heading = "Filters")]
|
|
pub status: Vec<String>,
|
|
|
|
/// Filter by time (7d, 2w, 1m, or YYYY-MM-DD)
|
|
#[arg(long, help_heading = "Filters")]
|
|
pub since: Option<String>,
|
|
|
|
/// Filter by due date (before this date, YYYY-MM-DD)
|
|
#[arg(long = "due-before", help_heading = "Filters")]
|
|
pub due_before: Option<String>,
|
|
|
|
/// Show only issues with a due date
|
|
#[arg(
|
|
long = "has-due",
|
|
help_heading = "Filters",
|
|
overrides_with = "no_has_due"
|
|
)]
|
|
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)
|
|
#[arg(long, value_parser = ["updated", "created", "iid"], default_value = "updated", help_heading = "Sorting")]
|
|
pub sort: String,
|
|
|
|
/// Sort ascending (default: descending)
|
|
#[arg(long, help_heading = "Sorting", overrides_with = "no_asc")]
|
|
pub asc: bool,
|
|
|
|
#[arg(long = "no-asc", hide = true, overrides_with = "asc")]
|
|
pub no_asc: bool,
|
|
|
|
/// Open first matching item in browser
|
|
#[arg(
|
|
short = 'o',
|
|
long,
|
|
help_heading = "Actions",
|
|
overrides_with = "no_open"
|
|
)]
|
|
pub open: bool,
|
|
|
|
#[arg(long = "no-open", hide = true, overrides_with = "open")]
|
|
pub no_open: bool,
|
|
}
|
|
|
|
#[derive(Parser)]
|
|
#[command(after_help = "\x1b[1mExamples:\x1b[0m
|
|
lore mrs -s opened # List open merge requests
|
|
lore mrs -s merged --since 2w # MRs merged in the last 2 weeks
|
|
lore mrs 99 -p group/repo # Show MR !99 in a specific project
|
|
lore mrs -D --reviewer jsmith # Non-draft MRs reviewed by jsmith")]
|
|
pub struct MrsArgs {
|
|
/// MR IID (omit to list, provide to show details)
|
|
pub iid: Option<i64>,
|
|
|
|
/// Maximum results
|
|
#[arg(
|
|
short = 'n',
|
|
long = "limit",
|
|
default_value = "50",
|
|
help_heading = "Output"
|
|
)]
|
|
pub limit: usize,
|
|
|
|
/// Select output fields (comma-separated, or 'minimal' preset: iid,title,state,updated_at_iso)
|
|
#[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", value_parser = ["opened", "merged", "closed", "locked", "all"])]
|
|
pub state: Option<String>,
|
|
|
|
/// Filter by project path
|
|
#[arg(short = 'p', long, help_heading = "Filters")]
|
|
pub project: Option<String>,
|
|
|
|
/// Filter by author username
|
|
#[arg(short = 'a', long, help_heading = "Filters")]
|
|
pub author: Option<String>,
|
|
|
|
/// Filter by assignee username
|
|
#[arg(short = 'A', long, help_heading = "Filters")]
|
|
pub assignee: Option<String>,
|
|
|
|
/// Filter by reviewer username
|
|
#[arg(short = 'r', long, help_heading = "Filters")]
|
|
pub reviewer: Option<String>,
|
|
|
|
/// Filter by label (repeatable, AND logic)
|
|
#[arg(short = 'l', long, help_heading = "Filters")]
|
|
pub label: Option<Vec<String>>,
|
|
|
|
/// Filter by time (7d, 2w, 1m, or YYYY-MM-DD)
|
|
#[arg(long, help_heading = "Filters")]
|
|
pub since: Option<String>,
|
|
|
|
/// Show only draft MRs
|
|
#[arg(
|
|
short = 'd',
|
|
long,
|
|
conflicts_with = "no_draft",
|
|
help_heading = "Filters"
|
|
)]
|
|
pub draft: bool,
|
|
|
|
/// Exclude draft MRs
|
|
#[arg(
|
|
short = 'D',
|
|
long = "no-draft",
|
|
conflicts_with = "draft",
|
|
help_heading = "Filters"
|
|
)]
|
|
pub no_draft: bool,
|
|
|
|
/// Filter by target branch
|
|
#[arg(long, help_heading = "Filters")]
|
|
pub target: Option<String>,
|
|
|
|
/// Filter by source branch
|
|
#[arg(long, help_heading = "Filters")]
|
|
pub source: Option<String>,
|
|
|
|
/// Sort field (updated, created, iid)
|
|
#[arg(long, value_parser = ["updated", "created", "iid"], default_value = "updated", help_heading = "Sorting")]
|
|
pub sort: String,
|
|
|
|
/// Sort ascending (default: descending)
|
|
#[arg(long, help_heading = "Sorting", overrides_with = "no_asc")]
|
|
pub asc: bool,
|
|
|
|
#[arg(long = "no-asc", hide = true, overrides_with = "asc")]
|
|
pub no_asc: bool,
|
|
|
|
/// Open first matching item in browser
|
|
#[arg(
|
|
short = 'o',
|
|
long,
|
|
help_heading = "Actions",
|
|
overrides_with = "no_open"
|
|
)]
|
|
pub open: bool,
|
|
|
|
#[arg(long = "no-open", hide = true, overrides_with = "open")]
|
|
pub no_open: bool,
|
|
}
|
|
|
|
#[derive(Parser)]
|
|
#[command(after_help = "\x1b[1mExamples:\x1b[0m
|
|
lore notes # List 50 most recent notes
|
|
lore notes --author alice --since 7d # Notes by alice in last 7 days
|
|
lore notes --for-issue 42 -p group/repo # Notes on issue #42
|
|
lore notes --path src/ --resolution unresolved # Unresolved diff notes in src/")]
|
|
pub struct NotesArgs {
|
|
/// Maximum results
|
|
#[arg(
|
|
short = 'n',
|
|
long = "limit",
|
|
default_value = "50",
|
|
help_heading = "Output"
|
|
)]
|
|
pub limit: usize,
|
|
|
|
/// Select output fields (comma-separated, or 'minimal' preset: id,author_username,body,created_at_iso)
|
|
#[arg(long, help_heading = "Output", value_delimiter = ',')]
|
|
pub fields: Option<Vec<String>>,
|
|
|
|
/// Filter by author username
|
|
#[arg(short = 'a', long, help_heading = "Filters")]
|
|
pub author: Option<String>,
|
|
|
|
/// Filter by note type (DiffNote, DiscussionNote)
|
|
#[arg(long, help_heading = "Filters")]
|
|
pub note_type: Option<String>,
|
|
|
|
/// Filter by body text (substring match)
|
|
#[arg(long, help_heading = "Filters")]
|
|
pub contains: Option<String>,
|
|
|
|
/// Filter by internal note ID
|
|
#[arg(long, help_heading = "Filters")]
|
|
pub note_id: Option<i64>,
|
|
|
|
/// Filter by GitLab note ID
|
|
#[arg(long, help_heading = "Filters")]
|
|
pub gitlab_note_id: Option<i64>,
|
|
|
|
/// Filter by discussion ID
|
|
#[arg(long, help_heading = "Filters")]
|
|
pub discussion_id: Option<String>,
|
|
|
|
/// Include system notes (excluded by default)
|
|
#[arg(long, help_heading = "Filters")]
|
|
pub include_system: bool,
|
|
|
|
/// Filter to notes on a specific issue IID (requires --project or default_project)
|
|
#[arg(long, conflicts_with = "for_mr", help_heading = "Filters")]
|
|
pub for_issue: Option<i64>,
|
|
|
|
/// Filter to notes on a specific MR IID (requires --project or default_project)
|
|
#[arg(long, conflicts_with = "for_issue", help_heading = "Filters")]
|
|
pub for_mr: Option<i64>,
|
|
|
|
/// Filter by project path
|
|
#[arg(short = 'p', long, help_heading = "Filters")]
|
|
pub project: Option<String>,
|
|
|
|
/// Filter by time (7d, 2w, 1m, or YYYY-MM-DD)
|
|
#[arg(long, help_heading = "Filters")]
|
|
pub since: Option<String>,
|
|
|
|
/// Filter until date (YYYY-MM-DD, inclusive end-of-day)
|
|
#[arg(long, help_heading = "Filters")]
|
|
pub until: Option<String>,
|
|
|
|
/// Filter by file path (exact match or prefix with trailing /)
|
|
#[arg(long, help_heading = "Filters")]
|
|
pub path: Option<String>,
|
|
|
|
/// Filter by resolution status (any, unresolved, resolved)
|
|
#[arg(
|
|
long,
|
|
value_parser = ["any", "unresolved", "resolved"],
|
|
help_heading = "Filters"
|
|
)]
|
|
pub resolution: Option<String>,
|
|
|
|
/// Sort field (created, updated)
|
|
#[arg(
|
|
long,
|
|
value_parser = ["created", "updated"],
|
|
default_value = "created",
|
|
help_heading = "Sorting"
|
|
)]
|
|
pub sort: String,
|
|
|
|
/// Sort ascending (default: descending)
|
|
#[arg(long, help_heading = "Sorting")]
|
|
pub asc: bool,
|
|
|
|
/// Open first matching item in browser
|
|
#[arg(long, help_heading = "Actions")]
|
|
pub open: bool,
|
|
}
|
|
|
|
#[derive(Parser)]
|
|
pub struct IngestArgs {
|
|
/// Entity to ingest (issues, mrs). Omit to ingest everything
|
|
#[arg(value_parser = ["issues", "mrs"])]
|
|
pub entity: Option<String>,
|
|
|
|
/// Filter to single project
|
|
#[arg(short = 'p', long)]
|
|
pub project: Option<String>,
|
|
|
|
/// Override stale sync lock
|
|
#[arg(short = 'f', long, overrides_with = "no_force")]
|
|
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
|
|
#[arg(long, overrides_with = "no_full")]
|
|
pub full: bool,
|
|
|
|
#[arg(long = "no-full", hide = true, overrides_with = "full")]
|
|
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)]
|
|
#[command(after_help = "\x1b[1mExamples:\x1b[0m
|
|
lore stats # Show document and index statistics
|
|
lore stats --check # Run integrity checks
|
|
lore stats --repair --dry-run # Preview what repair would fix
|
|
lore --robot stats # JSON output for automation")]
|
|
pub struct StatsArgs {
|
|
/// Run integrity checks
|
|
#[arg(long, overrides_with = "no_check")]
|
|
pub check: bool,
|
|
|
|
#[arg(long = "no-check", hide = true, overrides_with = "check")]
|
|
pub no_check: bool,
|
|
|
|
/// Repair integrity issues (auto-enables --check)
|
|
#[arg(long)]
|
|
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)]
|
|
#[command(after_help = "\x1b[1mExamples:\x1b[0m
|
|
lore search 'authentication bug' # Hybrid search (default)
|
|
lore search 'deploy' --mode lexical --type mr # Lexical search, MRs only
|
|
lore search 'API rate limit' --since 30d # Recent results only
|
|
lore search 'config' -p group/repo --explain # With ranking explanation")]
|
|
pub struct SearchArgs {
|
|
/// Search query string
|
|
pub query: String,
|
|
|
|
/// Search mode (lexical, hybrid, semantic)
|
|
#[arg(long, default_value = "hybrid", value_parser = ["lexical", "hybrid", "semantic"], help_heading = "Mode")]
|
|
pub mode: String,
|
|
|
|
/// Filter by source type (issue, mr, discussion, note)
|
|
#[arg(long = "type", value_name = "TYPE", value_parser = ["issue", "mr", "discussion", "note"], help_heading = "Filters")]
|
|
pub source_type: Option<String>,
|
|
|
|
/// Filter by author username
|
|
#[arg(long, help_heading = "Filters")]
|
|
pub author: Option<String>,
|
|
|
|
/// Filter by project path
|
|
#[arg(short = 'p', long, help_heading = "Filters")]
|
|
pub project: Option<String>,
|
|
|
|
/// Filter by label (repeatable, AND logic)
|
|
#[arg(long, action = clap::ArgAction::Append, help_heading = "Filters")]
|
|
pub label: Vec<String>,
|
|
|
|
/// Filter by file path (trailing / for prefix match)
|
|
#[arg(long, help_heading = "Filters")]
|
|
pub path: Option<String>,
|
|
|
|
/// Filter by created since (7d, 2w, or YYYY-MM-DD)
|
|
#[arg(long, help_heading = "Filters")]
|
|
pub since: Option<String>,
|
|
|
|
/// Filter by updated since (7d, 2w, or YYYY-MM-DD)
|
|
#[arg(long = "updated-since", help_heading = "Filters")]
|
|
pub updated_since: Option<String>,
|
|
|
|
/// Maximum results (default 20, max 100)
|
|
#[arg(
|
|
short = 'n',
|
|
long = "limit",
|
|
default_value = "20",
|
|
help_heading = "Output"
|
|
)]
|
|
pub limit: usize,
|
|
|
|
/// Select output fields (comma-separated, or 'minimal' preset: document_id,title,source_type,score)
|
|
#[arg(long, help_heading = "Output", value_delimiter = ',')]
|
|
pub fields: Option<Vec<String>>,
|
|
|
|
/// Show ranking explanation per result
|
|
#[arg(long, help_heading = "Output", overrides_with = "no_explain")]
|
|
pub explain: bool,
|
|
|
|
#[arg(long = "no-explain", hide = true, overrides_with = "explain")]
|
|
pub no_explain: bool,
|
|
|
|
/// FTS query mode: safe (default) or raw
|
|
#[arg(long = "fts-mode", default_value = "safe", value_parser = ["safe", "raw"], help_heading = "Mode")]
|
|
pub fts_mode: String,
|
|
}
|
|
|
|
#[derive(Parser)]
|
|
#[command(after_help = "\x1b[1mExamples:\x1b[0m
|
|
lore generate-docs # Generate docs for dirty entities
|
|
lore generate-docs --full # Full rebuild of all documents
|
|
lore generate-docs --full -p group/repo # Full rebuild for one project")]
|
|
pub struct GenerateDocsArgs {
|
|
/// Full rebuild: seed all entities into dirty queue, then drain
|
|
#[arg(long)]
|
|
pub full: bool,
|
|
|
|
/// Filter to single project
|
|
#[arg(short = 'p', long)]
|
|
pub project: Option<String>,
|
|
}
|
|
|
|
#[derive(Parser)]
|
|
#[command(after_help = "\x1b[1mExamples:\x1b[0m
|
|
lore sync # Full pipeline: ingest + docs + embed
|
|
lore sync --no-embed # Skip embedding step
|
|
lore sync --no-status # Skip work-item status enrichment
|
|
lore sync --full --force # Full re-sync, override stale lock
|
|
lore sync --dry-run # Preview what would change
|
|
lore sync --issue 42 -p group/repo # Surgically sync one issue
|
|
lore sync --mr 10 --mr 20 -p g/r # Surgically sync two MRs")]
|
|
pub struct SyncArgs {
|
|
/// Reset cursors, fetch everything
|
|
#[arg(long, overrides_with = "no_full")]
|
|
pub full: bool,
|
|
|
|
#[arg(long = "no-full", hide = true, overrides_with = "full")]
|
|
pub no_full: bool,
|
|
|
|
/// Override stale lock
|
|
#[arg(long, overrides_with = "no_force")]
|
|
pub force: bool,
|
|
|
|
#[arg(long = "no-force", hide = true, overrides_with = "force")]
|
|
pub no_force: bool,
|
|
|
|
/// Skip embedding step
|
|
#[arg(long)]
|
|
pub no_embed: bool,
|
|
|
|
/// Skip document regeneration
|
|
#[arg(long)]
|
|
pub no_docs: bool,
|
|
|
|
/// Skip resource event fetching (overrides config)
|
|
#[arg(long = "no-events")]
|
|
pub no_events: bool,
|
|
|
|
/// Skip MR file change fetching (overrides config)
|
|
#[arg(long = "no-file-changes")]
|
|
pub no_file_changes: bool,
|
|
|
|
/// Skip work-item status enrichment via GraphQL (overrides config)
|
|
#[arg(long = "no-status")]
|
|
pub no_status: 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,
|
|
|
|
/// Show detailed timing breakdown for sync stages
|
|
#[arg(short = 't', long = "timings")]
|
|
pub timings: bool,
|
|
|
|
/// Acquire file lock before syncing (skip if another sync is running)
|
|
#[arg(long)]
|
|
pub lock: bool,
|
|
|
|
/// Surgically sync specific issues by IID (repeatable, must be positive)
|
|
#[arg(long, value_parser = clap::value_parser!(u64).range(1..), action = clap::ArgAction::Append)]
|
|
pub issue: Vec<u64>,
|
|
|
|
/// Surgically sync specific merge requests by IID (repeatable, must be positive)
|
|
#[arg(long, value_parser = clap::value_parser!(u64).range(1..), action = clap::ArgAction::Append)]
|
|
pub mr: Vec<u64>,
|
|
|
|
/// Scope to a single project (required when --issue or --mr is used)
|
|
#[arg(short = 'p', long)]
|
|
pub project: Option<String>,
|
|
|
|
/// Validate remote entities exist without DB writes (preflight only)
|
|
#[arg(long)]
|
|
pub preflight_only: bool,
|
|
}
|
|
|
|
#[derive(Parser)]
|
|
#[command(after_help = "\x1b[1mExamples:\x1b[0m
|
|
lore embed # Embed new/changed documents
|
|
lore embed --full # Re-embed all documents from scratch
|
|
lore embed --retry-failed # Retry previously failed embeddings")]
|
|
pub struct EmbedArgs {
|
|
/// Re-embed all documents (clears existing embeddings first)
|
|
#[arg(long, overrides_with = "no_full")]
|
|
pub full: bool,
|
|
|
|
#[arg(long = "no-full", hide = true, overrides_with = "full")]
|
|
pub no_full: bool,
|
|
|
|
/// Retry previously failed embeddings
|
|
#[arg(long, overrides_with = "no_retry_failed")]
|
|
pub retry_failed: bool,
|
|
|
|
#[arg(long = "no-retry-failed", hide = true, overrides_with = "retry_failed")]
|
|
pub no_retry_failed: bool,
|
|
}
|
|
|
|
#[derive(Parser)]
|
|
#[command(after_help = "\x1b[1mExamples:\x1b[0m
|
|
lore timeline 'deployment' # Search-based seeding
|
|
lore timeline issue:42 # Direct: issue #42 and related entities
|
|
lore timeline i:42 # Shorthand for issue:42
|
|
lore timeline mr:99 # Direct: MR !99 and related entities
|
|
lore timeline 'auth' --since 30d -p group/repo # Scoped to project and time
|
|
lore timeline 'migration' --depth 2 # Deep cross-reference expansion
|
|
lore timeline 'auth' --no-mentions # Only 'closes' and 'related' edges")]
|
|
pub struct TimelineArgs {
|
|
/// Search text or entity reference (issue:N, i:N, mr:N, m:N)
|
|
pub query: String,
|
|
|
|
/// Scope to a specific project (fuzzy match)
|
|
#[arg(short = 'p', long, help_heading = "Filters")]
|
|
pub project: Option<String>,
|
|
|
|
/// Only show events after this date (e.g. "6m", "2w", "2024-01-01")
|
|
#[arg(long, help_heading = "Filters")]
|
|
pub since: Option<String>,
|
|
|
|
/// Cross-reference expansion depth (0 = no expansion)
|
|
#[arg(long, default_value = "1", help_heading = "Expansion")]
|
|
pub depth: u32,
|
|
|
|
/// Skip 'mentioned' edges during expansion (only follow 'closes' and 'related')
|
|
#[arg(long = "no-mentions", help_heading = "Expansion")]
|
|
pub no_mentions: bool,
|
|
|
|
/// Maximum number of events to display
|
|
#[arg(
|
|
short = 'n',
|
|
long = "limit",
|
|
default_value = "100",
|
|
help_heading = "Output"
|
|
)]
|
|
pub limit: usize,
|
|
|
|
/// Select output fields (comma-separated, or 'minimal' preset: timestamp,type,entity_iid,detail)
|
|
#[arg(long, help_heading = "Output", value_delimiter = ',')]
|
|
pub fields: Option<Vec<String>>,
|
|
|
|
/// Maximum seed entities from search
|
|
#[arg(long = "max-seeds", default_value = "10", help_heading = "Expansion")]
|
|
pub max_seeds: usize,
|
|
|
|
/// Maximum expanded entities via cross-references
|
|
#[arg(
|
|
long = "max-entities",
|
|
default_value = "50",
|
|
help_heading = "Expansion"
|
|
)]
|
|
pub max_entities: usize,
|
|
|
|
/// Maximum evidence notes included
|
|
#[arg(
|
|
long = "max-evidence",
|
|
default_value = "10",
|
|
help_heading = "Expansion"
|
|
)]
|
|
pub max_evidence: usize,
|
|
}
|
|
|
|
#[derive(Parser)]
|
|
#[command(after_help = "\x1b[1mExamples:\x1b[0m
|
|
lore who src/features/auth/ # Who knows about this area?
|
|
lore who @asmith # What is asmith working on?
|
|
lore who @asmith --reviews # What review patterns does asmith have?
|
|
lore who --active # What discussions need attention?
|
|
lore who --overlap src/features/auth/ # Who else is touching these files?
|
|
lore who --path README.md # Expert lookup for a root file
|
|
lore who --path Makefile # Expert lookup for a dotless root file")]
|
|
pub struct WhoArgs {
|
|
/// Username or file path (path if contains /)
|
|
pub target: Option<String>,
|
|
|
|
/// Force expert mode for a file/directory path.
|
|
/// Root files (README.md, LICENSE, Makefile) are treated as exact matches.
|
|
/// Use a trailing `/` to force directory-prefix matching.
|
|
#[arg(long, help_heading = "Mode", conflicts_with_all = ["active", "overlap", "reviews"])]
|
|
pub path: Option<String>,
|
|
|
|
/// Show active unresolved discussions
|
|
#[arg(long, help_heading = "Mode", conflicts_with_all = ["target", "overlap", "reviews", "path"])]
|
|
pub active: bool,
|
|
|
|
/// Find users with MRs/notes touching this file path
|
|
#[arg(long, help_heading = "Mode", conflicts_with_all = ["target", "active", "reviews", "path"])]
|
|
pub overlap: Option<String>,
|
|
|
|
/// Show review pattern analysis (requires username target)
|
|
#[arg(long, help_heading = "Mode", requires = "target", conflicts_with_all = ["active", "overlap", "path"])]
|
|
pub reviews: bool,
|
|
|
|
/// Time window (7d, 2w, 6m, YYYY-MM-DD). Default varies by mode.
|
|
#[arg(long, help_heading = "Filters")]
|
|
pub since: Option<String>,
|
|
|
|
/// Scope to a project (supports fuzzy matching)
|
|
#[arg(short = 'p', long, help_heading = "Filters")]
|
|
pub project: Option<String>,
|
|
|
|
/// Maximum results per section (1..=500); omit for unlimited
|
|
#[arg(
|
|
short = 'n',
|
|
long = "limit",
|
|
value_parser = clap::value_parser!(u16).range(1..=500),
|
|
help_heading = "Output"
|
|
)]
|
|
pub limit: Option<u16>,
|
|
|
|
/// Select output fields (comma-separated, or 'minimal' preset; varies by mode)
|
|
#[arg(long, help_heading = "Output", value_delimiter = ',')]
|
|
pub fields: Option<Vec<String>>,
|
|
|
|
/// Show per-MR detail breakdown (expert mode only)
|
|
#[arg(
|
|
long,
|
|
help_heading = "Output",
|
|
overrides_with = "no_detail",
|
|
conflicts_with = "explain_score"
|
|
)]
|
|
pub detail: bool,
|
|
|
|
#[arg(long = "no-detail", hide = true, overrides_with = "detail")]
|
|
pub no_detail: bool,
|
|
|
|
/// Score as if "now" is this date (ISO 8601 or duration like 30d). Expert mode only.
|
|
#[arg(long = "as-of", help_heading = "Scoring")]
|
|
pub as_of: Option<String>,
|
|
|
|
/// Show per-component score breakdown in output. Expert mode only.
|
|
#[arg(long = "explain-score", help_heading = "Scoring")]
|
|
pub explain_score: bool,
|
|
|
|
/// Include bot users in results (normally excluded via scoring.excluded_usernames).
|
|
#[arg(long = "include-bots", help_heading = "Scoring")]
|
|
pub include_bots: bool,
|
|
|
|
/// Include discussions on closed issues and merged/closed MRs
|
|
#[arg(long, help_heading = "Filters")]
|
|
pub include_closed: bool,
|
|
|
|
/// Remove the default time window (query all history). Conflicts with --since.
|
|
#[arg(
|
|
long = "all-history",
|
|
help_heading = "Filters",
|
|
conflicts_with = "since"
|
|
)]
|
|
pub all_history: bool,
|
|
}
|
|
|
|
#[derive(Parser)]
|
|
#[command(after_help = "\x1b[1mExamples:\x1b[0m
|
|
lore me # Full dashboard (default project or all)
|
|
lore me --issues # Issues section only
|
|
lore me --mrs # MRs section only
|
|
lore me --activity # Activity feed only
|
|
lore me --all # All synced projects
|
|
lore me --since 2d # Activity window (default: 30d)
|
|
lore me --project group/repo # Scope to one project
|
|
lore me --user jdoe # Override configured username")]
|
|
pub struct MeArgs {
|
|
/// Show open issues section
|
|
#[arg(long, help_heading = "Sections")]
|
|
pub issues: bool,
|
|
|
|
/// Show authored + reviewing MRs section
|
|
#[arg(long, help_heading = "Sections")]
|
|
pub mrs: bool,
|
|
|
|
/// Show activity feed section
|
|
#[arg(long, help_heading = "Sections")]
|
|
pub activity: bool,
|
|
|
|
/// Show items you're @mentioned in (not assigned/authored/reviewing)
|
|
#[arg(long, help_heading = "Sections")]
|
|
pub mentions: bool,
|
|
|
|
/// Activity window (e.g. 7d, 2w, 30d). Default: 30d. Only affects activity section.
|
|
#[arg(long, help_heading = "Filters")]
|
|
pub since: Option<String>,
|
|
|
|
/// Scope to a project (supports fuzzy matching)
|
|
#[arg(short = 'p', long, help_heading = "Filters", conflicts_with = "all")]
|
|
pub project: Option<String>,
|
|
|
|
/// Show all synced projects (overrides default_project)
|
|
#[arg(long, help_heading = "Filters", conflicts_with = "project")]
|
|
pub all: bool,
|
|
|
|
/// Override configured username
|
|
#[arg(long = "user", help_heading = "Filters")]
|
|
pub user: Option<String>,
|
|
|
|
/// Select output fields (comma-separated, or 'minimal' preset)
|
|
#[arg(long, help_heading = "Output", value_delimiter = ',')]
|
|
pub fields: Option<Vec<String>>,
|
|
|
|
/// Reset the since-last-check cursor (next run shows no new events)
|
|
#[arg(long, help_heading = "Output")]
|
|
pub reset_cursor: bool,
|
|
}
|
|
|
|
impl MeArgs {
|
|
/// Returns true if no section flags were passed (show all sections).
|
|
pub fn show_all_sections(&self) -> bool {
|
|
!self.issues && !self.mrs && !self.activity && !self.mentions
|
|
}
|
|
}
|
|
|
|
#[derive(Parser)]
|
|
#[command(after_help = "\x1b[1mExamples:\x1b[0m
|
|
lore file-history src/main.rs # MRs that touched this file
|
|
lore file-history src/auth/ -p group/repo # Scoped to project
|
|
lore file-history src/foo.rs --discussions # Include DiffNote snippets
|
|
lore file-history src/bar.rs --no-follow-renames # Skip rename chain")]
|
|
pub struct FileHistoryArgs {
|
|
/// File path to trace history for
|
|
pub path: String,
|
|
|
|
/// Scope to a specific project (fuzzy match)
|
|
#[arg(short = 'p', long, help_heading = "Filters")]
|
|
pub project: Option<String>,
|
|
|
|
/// Include discussion snippets from DiffNotes on this file
|
|
#[arg(long, help_heading = "Output")]
|
|
pub discussions: bool,
|
|
|
|
/// Disable rename chain resolution
|
|
#[arg(long = "no-follow-renames", help_heading = "Filters")]
|
|
pub no_follow_renames: bool,
|
|
|
|
/// Only show merged MRs
|
|
#[arg(long, help_heading = "Filters")]
|
|
pub merged: bool,
|
|
|
|
/// Maximum results
|
|
#[arg(
|
|
short = 'n',
|
|
long = "limit",
|
|
default_value = "50",
|
|
help_heading = "Output"
|
|
)]
|
|
pub limit: usize,
|
|
}
|
|
|
|
#[derive(Parser)]
|
|
#[command(after_help = "\x1b[1mExamples:\x1b[0m
|
|
lore trace src/main.rs # Why was this file changed?
|
|
lore trace src/auth/ -p group/repo # Scoped to project
|
|
lore trace src/foo.rs --discussions # Include DiffNote context
|
|
lore trace src/bar.rs:42 # Line hint (Tier 2 warning)")]
|
|
pub struct TraceArgs {
|
|
/// File path to trace (supports :line suffix for future Tier 2)
|
|
pub path: String,
|
|
|
|
/// Scope to a specific project (fuzzy match)
|
|
#[arg(short = 'p', long, help_heading = "Filters")]
|
|
pub project: Option<String>,
|
|
|
|
/// Include DiffNote discussion snippets
|
|
#[arg(long, help_heading = "Output")]
|
|
pub discussions: bool,
|
|
|
|
/// Disable rename chain resolution
|
|
#[arg(long = "no-follow-renames", help_heading = "Filters")]
|
|
pub no_follow_renames: bool,
|
|
|
|
/// Maximum trace chains to display
|
|
#[arg(
|
|
short = 'n',
|
|
long = "limit",
|
|
default_value = "20",
|
|
help_heading = "Output"
|
|
)]
|
|
pub limit: usize,
|
|
}
|
|
|
|
#[derive(Parser)]
|
|
#[command(after_help = "\x1b[1mExamples:\x1b[0m
|
|
lore count issues # Total issues in local database
|
|
lore count notes --for mr # Notes on merge requests only
|
|
lore count discussions --for issue # Discussions on issues only")]
|
|
pub struct CountArgs {
|
|
/// Entity type to count (issues, mrs, discussions, notes, events)
|
|
#[arg(value_parser = ["issues", "mrs", "discussions", "notes", "events"])]
|
|
pub entity: String,
|
|
|
|
/// Parent type filter: issue or mr (for discussions/notes)
|
|
#[arg(short = 'f', long = "for", value_parser = ["issue", "mr"])]
|
|
pub for_entity: Option<String>,
|
|
}
|
|
|
|
#[derive(Parser)]
|
|
pub struct CronArgs {
|
|
#[command(subcommand)]
|
|
pub action: CronAction,
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
pub enum CronAction {
|
|
/// Install cron job for automatic syncing
|
|
Install {
|
|
/// Sync interval in minutes (default: 8)
|
|
#[arg(long, default_value = "8")]
|
|
interval: u32,
|
|
},
|
|
|
|
/// Remove cron job
|
|
Uninstall,
|
|
|
|
/// Show current cron configuration
|
|
Status,
|
|
}
|
|
|
|
#[derive(Args)]
|
|
pub struct TokenArgs {
|
|
#[command(subcommand)]
|
|
pub action: TokenAction,
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
pub enum TokenAction {
|
|
/// Store a GitLab token in the config file
|
|
Set {
|
|
/// Token value (reads from stdin if omitted in non-interactive mode)
|
|
#[arg(long)]
|
|
token: Option<String>,
|
|
},
|
|
|
|
/// Show the current token (masked by default)
|
|
Show {
|
|
/// Show the full unmasked token
|
|
#[arg(long)]
|
|
unmask: bool,
|
|
},
|
|
}
|