use std::path::PathBuf;
use clap::{Args, Parser, Subcommand, ValueEnum};
#[derive(Debug, Parser)]
#[command(
name = "rag-rat",
version,
about = "Local repo-intelligence index, graph, history, and memory — CLI + MCP server.",
propagate_version = true
)]
pub(crate) struct Cli {
#[arg(long, global = true, default_value = "rag-rat.toml")]
pub config: String,
#[command(subcommand)]
pub command: Command,
}
#[derive(Debug, Subcommand)]
pub(crate) enum Command {
Init(InitArgs),
#[command(hide = true)]
ClaudeHook,
Index(IndexArgs),
Doctor,
Migrate(MigrateArgs),
Query(QueryArgs),
Brief(BriefArgs),
Clusters(ClustersArgs),
Mcp,
Memory(MemoryArgs),
Github(GithubArgs),
Hooks(HooksArgs),
Maintenance(MaintenanceArgs),
Models(ModelsArgs),
Reconcile(ReconcileArgs),
Gc,
Eval(EvalArgs),
DumpConfig,
}
#[derive(Debug, Args)]
pub(crate) struct InitArgs {
#[arg(long)]
pub dry_run: bool,
#[arg(long, short = 'y')]
pub yes: bool,
#[arg(long)]
pub force: bool,
}
#[derive(Debug, Args)]
pub(crate) struct IndexArgs {
#[arg(long)]
pub full: bool,
#[arg(long)]
pub discover: bool,
#[arg(long)]
pub changed: bool,
#[arg(long)]
pub watch: bool,
}
#[derive(Debug, Args)]
pub(crate) struct MigrateArgs {
#[arg(long)]
pub check: bool,
}
#[derive(Debug, Args)]
pub(crate) struct QueryArgs {
#[arg(long)]
pub explain: bool,
#[arg(required = true, num_args = 1.., value_name = "QUERY")]
pub query: Vec<String>,
}
#[derive(Debug, Args)]
pub(crate) struct BriefArgs {
#[arg(long)]
pub mode: Option<String>,
#[arg(long)]
pub limit: Option<u32>,
#[arg(long)]
pub include_generated: bool,
#[arg(long)]
pub no_memories: bool,
}
#[derive(Debug, Args)]
pub(crate) struct ClustersArgs {
#[arg(long)]
pub limit: Option<u32>,
#[arg(long)]
pub min_cluster_size: Option<u32>,
#[arg(long)]
pub include_generated: bool,
#[arg(long)]
pub no_memories: bool,
}
#[derive(Debug, Args)]
pub(crate) struct MaintenanceArgs {
#[arg(long)]
pub trigger: Option<String>,
#[arg(long)]
pub max_seconds: Option<u64>,
#[arg(long)]
pub branch_checkout: Option<String>,
#[arg(long)]
pub old_head: Option<String>,
#[arg(long)]
pub new_head: Option<String>,
}
#[derive(Debug, Args)]
pub(crate) struct ReconcileArgs {
#[arg(long)]
pub plan: bool,
#[arg(long)]
pub json: bool,
#[arg(long)]
pub limit: Option<u32>,
#[arg(long)]
pub batch_size: Option<u32>,
#[arg(long)]
pub force: bool,
#[arg(long)]
pub until_clean: bool,
#[arg(long)]
pub changed_first: bool,
#[arg(long)]
pub max_seconds: Option<u64>,
#[arg(long)]
pub max_embedding_chars: Option<usize>,
}
#[derive(Debug, Args)]
pub(crate) struct EvalArgs {
#[arg(long)]
pub queries: Option<PathBuf>,
#[arg(long)]
pub expected: Option<PathBuf>,
#[arg(long)]
pub update_baseline: bool,
#[arg(long)]
pub json: bool,
}
#[derive(Debug, Args)]
pub(crate) struct MemoryArgs {
#[command(subcommand)]
pub command: MemoryCommand,
}
#[derive(Debug, Subcommand)]
pub(crate) enum MemoryCommand {
List {
#[arg(long)]
kind: Option<String>,
},
Show { memory_id: String },
Doctor {
#[arg(long)]
json: bool,
},
Rebind {
memory_id: String,
#[arg(long)]
symbol: Option<String>,
#[arg(long)]
path: Option<String>,
#[arg(long)]
chunk: Option<i64>,
},
}
#[derive(Debug, Args)]
pub(crate) struct GithubArgs {
#[command(subcommand)]
pub command: GithubCommand,
}
#[derive(Debug, Subcommand)]
pub(crate) enum GithubCommand {
Sync {
#[arg(long)]
from_refs: bool,
#[arg(long)]
issue: Option<String>,
#[arg(long)]
offline: bool,
},
}
#[derive(Debug, Args)]
pub(crate) struct HooksArgs {
#[arg(value_enum)]
pub action: HookAction,
#[arg(long)]
pub claude: bool,
#[arg(long)]
pub global: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub(crate) enum HookAction {
Install,
Uninstall,
Status,
}
impl HookAction {
pub(crate) fn as_str(self) -> &'static str {
match self {
HookAction::Install => "install",
HookAction::Uninstall => "uninstall",
HookAction::Status => "status",
}
}
}
#[derive(Debug, Args)]
pub(crate) struct ModelsArgs {
#[command(subcommand)]
pub command: Option<ModelsCommand>,
}
#[derive(Debug, Subcommand)]
pub(crate) enum ModelsCommand {
List,
Install { model_id: String },
}
#[cfg(test)]
mod tests {
use clap::CommandFactory;
use super::*;
#[test]
fn cli_definition_is_valid() {
Cli::command().debug_assert();
}
#[test]
fn parses_global_config_after_subcommand() {
let cli = Cli::try_parse_from(["rag-rat", "query", "--config", "x.toml", "foo", "bar"])
.expect("parse");
assert_eq!(cli.config, "x.toml");
match cli.command {
Command::Query(args) => {
assert_eq!(args.query, vec!["foo", "bar"]);
assert!(!args.explain);
},
other => panic!("expected query, got {other:?}"),
}
}
#[test]
fn config_defaults_to_rag_rat_toml() {
let cli = Cli::try_parse_from(["rag-rat", "gc"]).expect("parse");
assert_eq!(cli.config, "rag-rat.toml");
}
#[test]
fn version_flag_short_circuits() {
let err = Cli::try_parse_from(["rag-rat", "--version"]).unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::DisplayVersion);
}
#[test]
fn help_flag_short_circuits() {
let err = Cli::try_parse_from(["rag-rat", "--help"]).unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::DisplayHelp);
}
#[test]
fn nested_memory_rebind_parses() {
let cli = Cli::try_parse_from(["rag-rat", "memory", "rebind", "mem_1", "--symbol", "foo"])
.expect("parse");
match cli.command {
Command::Memory(MemoryArgs {
command: MemoryCommand::Rebind { memory_id, symbol, .. },
}) => {
assert_eq!(memory_id, "mem_1");
assert_eq!(symbol.as_deref(), Some("foo"));
},
other => panic!("expected memory rebind, got {other:?}"),
}
}
#[test]
fn hooks_action_and_flags_parse() {
let cli = Cli::try_parse_from(["rag-rat", "hooks", "install", "--claude", "--global"])
.expect("parse");
match cli.command {
Command::Hooks(args) => {
assert_eq!(args.action, HookAction::Install);
assert!(args.claude && args.global);
},
other => panic!("expected hooks, got {other:?}"),
}
}
}