mod commands;
mod detect;
pub(crate) use trusty_search::{config, core, mcp, service};
use anyhow::Result;
use clap::{CommandFactory, Parser, Subcommand, ValueEnum};
use clap_complete::{generate, Shell};
use colored::Colorize;
use commands::convert::ConvertTarget;
use commands::service::ServiceAction;
use std::io;
#[derive(Parser)]
#[command(
name = "trusty-search",
version,
author,
propagate_version = true,
subcommand_required = true,
arg_required_else_help = true
)]
struct Cli {
#[arg(short = 'i', long, global = true, env = "TRUSTY_INDEX")]
index: Option<String>,
#[arg(long, global = true)]
json: bool,
#[arg(short, long, global = true)]
verbose: bool,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
#[command(alias = "s", display_order = 1)]
Search {
query: String,
#[arg(short = 'k', long, default_value = "10")]
top_k: usize,
#[arg(short, long)]
full: bool,
#[arg(long, value_enum)]
intent: Option<IntentArg>,
#[arg(long)]
no_kg: bool,
#[arg(long, default_value = "0")]
offset: usize,
#[arg(long, default_value = "8000")]
budget: u32,
},
#[command(alias = "w", display_order = 2)]
Watch {
path: Option<std::path::PathBuf>,
},
#[command(alias = "st", display_order = 3)]
Status,
#[command(
alias = "idx",
display_order = 4,
args_conflicts_with_subcommands = true
)]
Index {
path: Option<std::path::PathBuf>,
#[arg(short, long)]
name: Option<String>,
#[arg(short, long)]
force: bool,
#[arg(long)]
exclude: Vec<String>,
#[arg(long, default_value_t = 600)]
timeout: u64,
#[arg(long)]
lexical_only: bool,
#[arg(long)]
no_kg: bool,
#[command(subcommand)]
action: Option<IndexAction>,
},
#[command(alias = "i", display_order = 4)]
Init {
path: Option<std::path::PathBuf>,
#[arg(short, long)]
name: Option<String>,
#[arg(long)]
exclude: Vec<String>,
},
#[command(display_order = 5)]
Add {
file: std::path::PathBuf,
},
#[command(alias = "rm", display_order = 6)]
Remove {
file: std::path::PathBuf,
},
#[command(display_order = 13)]
Cleanup {
#[arg(short = 'y', long)]
yes: bool,
#[arg(long)]
dry_run: bool,
},
#[command(display_order = 7)]
Reindex {
path: Option<std::path::PathBuf>,
#[arg(long, default_value_t = 600)]
timeout: u64,
},
#[command(alias = "ls", display_order = 10)]
List,
#[command(alias = "q", display_order = 11)]
Query {
query: String,
#[arg(long, default_value = "*")]
indexes: String,
#[arg(short = 'k', long, default_value = "10")]
top_k: usize,
#[arg(short, long)]
full: bool,
},
#[command(display_order = 12)]
Health,
#[command(display_order = 20)]
Start {
#[arg(long, default_value_t = trusty_search::service::DEFAULT_PORT)]
port: u16,
#[arg(long, default_value_t = false)]
foreground: bool,
#[arg(long, value_parser = ["auto", "cpu", "gpu"], default_value = "auto")]
device: String,
#[arg(long, env = "TRUSTY_DATA_DIR")]
data_dir: Option<std::path::PathBuf>,
#[arg(long, env = "TRUSTY_NO_AUTO_DISCOVER")]
no_auto_discover: bool,
},
#[command(display_order = 21)]
Stop,
#[command(display_order = 22)]
Serve {
#[arg(long, default_value_t = false)]
with_http: bool,
#[arg(long, hide = true, default_value_t = false)]
no_http: bool,
#[arg(long, default_value_t = 0)]
port: u16,
#[arg(long)]
http: Option<String>,
},
#[command(display_order = 24)]
Service {
#[command(subcommand)]
action: ServiceAction,
},
#[command(display_order = 23, aliases = ["dash", "ui"])]
Dashboard,
#[command(display_order = 25)]
Convert {
#[arg(value_name = "TARGET")]
target: ConvertTarget,
#[arg(long)]
dry_run: bool,
#[arg(long, default_value = "4")]
concurrency: usize,
},
#[command(display_order = 26)]
Migrate {
#[arg(value_name = "FROM")]
target: commands::migrate::MigrateTarget,
#[arg(long)]
dry_run: bool,
#[arg(long, conflicts_with = "indexes_only")]
mcp_only: bool,
#[arg(long, conflicts_with = "mcp_only")]
indexes_only: bool,
},
#[command(display_order = 27, name = "migrate-storage")]
MigrateStorage {
#[arg(long)]
dry_run: bool,
},
#[command(display_order = 27, name = "prune-orphans")]
PruneOrphans {
#[arg(long)]
dry_run: bool,
#[arg(long)]
yes: bool,
},
#[command(display_order = 28)]
Setup,
#[command(display_order = 28)]
Integrate {
#[arg(value_name = "IDE")]
target: commands::integrate::IntegrateTarget,
#[arg(long)]
dry_run: bool,
#[arg(long, conflicts_with = "project_only")]
global_only: bool,
#[arg(long, conflicts_with = "global_only")]
project_only: bool,
#[arg(long)]
no_rules: bool,
},
#[command(display_order = 29)]
Doctor {
#[arg(long)]
fix: bool,
},
#[command(display_order = 30)]
Config {
#[command(subcommand)]
action: commands::config::ConfigAction,
},
#[command(display_order = 31, subcommand_required = true)]
Monitor {
#[command(subcommand)]
target: MonitorTarget,
},
#[command(display_order = 32)]
Completions {
#[arg(value_enum)]
shell: Shell,
},
}
#[derive(Subcommand)]
enum IndexAction {
Remove {
path: Option<std::path::PathBuf>,
},
}
#[derive(Subcommand)]
enum MonitorTarget {
Web,
Tui,
Status {
#[arg(long)]
json: bool,
},
Indexes {
id: Option<String>,
#[arg(long)]
json: bool,
},
}
#[derive(Debug, Clone, ValueEnum)]
enum IntentArg {
Definition,
Usage,
Conceptual,
Bugdebt,
Unknown,
}
#[tokio::main]
async fn main() {
if let Err(e) = run().await {
let msg = format!("{:#}", e);
if !msg.is_empty() {
eprintln!("{} {}", "✗".red(), msg);
}
std::process::exit(1);
}
}
static HELP: std::sync::LazyLock<trusty_common::help::HelpConfig> =
std::sync::LazyLock::new(|| {
trusty_common::help::load_help(include_str!("../help.yaml"))
.expect("trusty-search help.yaml is bundled and valid")
});
async fn run() -> Result<()> {
dotenvy::from_filename(".env.local").ok();
let argv: Vec<String> = std::env::args().collect();
let cli = match Cli::try_parse() {
Ok(cli) => cli,
Err(e) => {
e.print().ok();
if matches!(
e.kind(),
clap::error::ErrorKind::InvalidSubcommand | clap::error::ErrorKind::UnknownArgument
) {
trusty_common::help::print_suggestion_hint(&argv, &HELP);
}
std::process::exit(e.exit_code());
}
};
if !matches!(cli.command, Commands::Start { .. }) {
trusty_common::init_tracing(if cli.verbose { 2 } else { 0 });
}
trusty_common::maybe_disable_color(false);
let is_mcp_serve = matches!(cli.command, Commands::Serve { .. });
let is_daemon_start = matches!(cli.command, Commands::Start { .. });
if !is_mcp_serve && !is_daemon_start {
if let Some(info) = trusty_common::update::check_throttled(
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION"),
)
.await
{
eprintln!("{}", trusty_common::update::notice(&info));
}
}
match cli.command {
Commands::Search {
query,
top_k,
full,
intent: _,
no_kg: _,
offset: _,
budget: _,
} => {
commands::search::handle_search(&cli.index, cli.json, query, top_k, full).await?;
}
Commands::Watch { path } => {
commands::watch::handle_watch(&cli.index, path).await?;
}
Commands::Status => {
commands::status::handle_status(cli.json).await?;
}
Commands::Init {
path,
name,
exclude,
} => {
commands::init::handle_init(path, name, exclude).await?;
}
Commands::Index {
path,
name,
force,
exclude,
timeout,
lexical_only,
no_kg,
action,
} => match action {
Some(IndexAction::Remove { path: rm_path }) => {
commands::index_remove::handle_index_remove(rm_path).await?;
}
None => {
commands::index::handle_index(
path,
name,
force,
exclude,
timeout,
lexical_only,
no_kg,
)
.await?;
}
},
Commands::Add { file } => {
commands::add::handle_add(&cli.index, file).await?;
}
Commands::Remove { file } => {
commands::remove::handle_remove(&cli.index, file).await?;
}
Commands::Cleanup { yes, dry_run } => {
commands::cleanup::handle_cleanup(yes, dry_run).await?;
}
Commands::Reindex { path, timeout } => {
commands::reindex::handle_reindex(&cli.index, path, timeout).await?;
}
Commands::List => {
commands::list::handle_list(cli.json).await?;
}
Commands::Query {
query,
indexes,
top_k,
full,
} => {
commands::query::handle_query(&cli.index, cli.json, query, indexes, top_k, full)
.await?;
}
Commands::Health => {
commands::status::handle_status(cli.json).await?;
}
Commands::Start {
port,
foreground,
device,
data_dir,
no_auto_discover,
} => {
commands::start::handle_start(
port,
foreground,
&device,
data_dir.as_deref(),
cli.verbose,
no_auto_discover,
)
.await?;
}
Commands::Stop => {
commands::stop::handle_stop().await?;
}
Commands::Serve {
with_http,
no_http: _, port,
http,
} => {
commands::serve::handle_serve(with_http, port, http).await?;
}
Commands::Service { action } => {
commands::service::handle_service(&action)?;
}
Commands::Dashboard => {
commands::dashboard::handle_dashboard().await?;
}
Commands::Convert {
target,
dry_run,
concurrency,
} => {
commands::convert::handle_convert(target, dry_run, concurrency).await?;
}
Commands::Migrate {
target,
dry_run,
mcp_only,
indexes_only,
} => {
commands::migrate::handle_migrate(target, dry_run, mcp_only, indexes_only).await?;
}
Commands::MigrateStorage { dry_run } => {
commands::migrate_storage::handle_migrate_storage(dry_run)?;
}
Commands::PruneOrphans { dry_run, yes } => {
commands::prune_orphans::handle_prune_orphans(dry_run, yes)?;
}
Commands::Setup => {
commands::setup::handle_setup()?;
}
Commands::Integrate {
target,
dry_run,
global_only,
project_only,
no_rules,
} => {
commands::integrate::handle_integrate(
target,
dry_run,
global_only,
project_only,
no_rules,
)
.await?;
}
Commands::Doctor { fix } => {
commands::doctor::handle_doctor(fix).await?;
}
Commands::Config { action } => {
commands::config::handle_config(action).await?;
}
Commands::Monitor { target } => match target {
MonitorTarget::Web => {
let url = match trusty_common::read_daemon_addr("trusty-search") {
Ok(Some(addr)) => format!("{addr}/ui"),
_ => format!(
"http://127.0.0.1:{}/ui",
trusty_search::service::DEFAULT_PORT
),
};
println!("{url}");
open::that(&url).ok();
}
MonitorTarget::Tui => {
trusty_common::monitor::search_tui::run().await?;
}
MonitorTarget::Status { json } => {
commands::monitor::handle_status(json).await?;
}
MonitorTarget::Indexes { id, json } => {
commands::monitor::handle_indexes(id, json).await?;
}
},
Commands::Completions { shell } => {
let mut cmd = Cli::command();
let name = cmd.get_name().to_string();
generate(shell, &mut cmd, name, &mut io::stdout());
}
}
Ok(())
}