mod commands;
mod detect;
pub(crate) use trusty_search::{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)]
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,
},
#[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 = 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,
},
#[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)]
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 = 28)]
Doctor {
#[arg(long)]
fix: bool,
},
#[command(display_order = 29)]
Config {
#[command(subcommand)]
action: commands::config::ConfigAction,
},
#[command(display_order = 30, subcommand_required = true)]
Monitor {
#[command(subcommand)]
target: MonitorTarget,
},
#[command(display_order = 31)]
Completions {
#[arg(value_enum)]
shell: Shell,
},
}
#[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);
}
}
async fn run() -> Result<()> {
dotenvy::from_filename(".env.local").ok();
let cli = Cli::parse();
if !matches!(cli.command, Commands::Start { .. }) {
trusty_common::init_tracing(if cli.verbose { 2 } else { 0 });
}
trusty_common::maybe_disable_color(false);
match cli.command {
Commands::Search {
query,
top_k,
full: _,
intent: _,
no_kg: _,
offset: _,
budget: _,
} => {
commands::search::handle_search(&cli.index, query, top_k).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,
} => {
commands::index::handle_index(path, name, force, exclude, timeout).await?;
}
Commands::Add { file } => {
commands::add::handle_add(&cli.index, file).await?;
}
Commands::Remove { file } => {
commands::remove::handle_remove(&cli.index, file).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,
} => {
commands::start::handle_start(port, foreground, &device, cli.verbose).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::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(())
}