#![forbid(unsafe_code)]
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
mod mcp;
mod fix_agent;
mod deterministic_fix;
mod verify;
mod bench;
mod log;
mod reindex;
mod watcher;
mod read_summary;
mod config;
mod init;
mod tee;
mod ux;
mod paths;
mod index_root;
use clap::{Parser, Subcommand, ValueEnum, CommandFactory};
use clap_complete::generate;
use std::io::{Write, IsTerminal};
mod color {
fn on() -> bool { crate::ux::colors_enabled() }
fn no_color() -> bool { !on() }
pub fn green(s: &str) -> String {
if no_color() { s.to_string() } else { format!("\x1b[32m{}\x1b[0m", s) }
}
pub fn red(s: &str) -> String {
if no_color() { s.to_string() } else { format!("\x1b[31m{}\x1b[0m", s) }
}
pub fn yellow(s: &str) -> String {
if no_color() { s.to_string() } else { format!("\x1b[33m{}\x1b[0m", s) }
}
pub fn bold(s: &str) -> String {
if no_color() { s.to_string() } else { format!("\x1b[1m{}\x1b[0m", s) }
}
pub fn dim(s: &str) -> String {
if no_color() { s.to_string() } else { format!("\x1b[2m{}\x1b[0m", s) }
}
#[allow(dead_code)]
pub fn reset(_s: &str) -> String {
if no_color() { String::new() } else { "\x1b[0m".to_string() }
}
#[allow(dead_code)]
pub fn is_enabled() -> bool { !no_color() }
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn release_assets_match_the_published_matrix() {
let expected = [
("linux", "x86_64", "linux", "x86_64", "tar.gz"),
("linux", "aarch64", "linux", "aarch64", "tar.gz"),
("macos", "x86_64", "darwin", "x86_64", "tar.gz"),
("macos", "aarch64", "darwin", "aarch64", "tar.gz"),
("windows", "x86_64", "windows", "x86_64", "zip"),
("windows", "aarch64", "windows", "aarch64", "zip"),
];
for (in_os, in_arch, asset_os, asset_arch, ext) in expected {
let got = release_asset_platform(in_os, in_arch);
assert_eq!(
got,
Some((asset_os, asset_arch, ext)),
"release_asset_platform({}, {}) must match the release matrix",
in_os, in_arch
);
}
assert_eq!(release_asset_platform("freebsd", "x86_64"), None);
assert_eq!(release_asset_platform("linux", "riscv64"), None);
assert_eq!(
release_asset_name("v1.2.3", "windows", "aarch64", "zip"),
"reliary-agent-v1.2.3-windows-aarch64.zip"
);
assert_eq!(
release_asset_name("v1.2.3", "linux", "x86_64", "tar.gz"),
"reliary-agent-v1.2.3-linux-x86_64.tar.gz"
);
let zip_asset = release_asset_name("v1.2.3", "windows", "x86_64", "zip");
let entry = format!("{}.exe", zip_asset.trim_end_matches(".zip"));
assert_eq!(entry, "reliary-agent-v1.2.3-windows-x86_64.exe");
}
#[test]
fn cli_structure_valid() {
let cmd = Cli::command();
let names: Vec<&str> = cmd.get_subcommands().map(|s| s.get_name()).collect();
for expected in &["search", "index", "compress", "risk", "doctor",
"status", "clean", "logs", "config", "init", "uninstall",
"dead", "completions", "man", "update",
"trust", "sift"] {
assert!(names.contains(expected), "Missing subcommand: {}", expected);
}
}
#[test]
fn cli_commands_list_complete() {
let cmd = Cli::command();
let subcmds: Vec<&str> = cmd.get_subcommands().map(|s| s.get_name()).collect();
for cmd_name in CLI_COMMANDS {
assert!(subcmds.contains(cmd_name) || matches!(*cmd_name,
"mcp" | "fix-dir" | "fix-file" | "apply-edit"
| "session-state" | "memory"
), "CLI_COMMANDS lists '{}' but it's not a subcommand", cmd_name);
}
}
#[test]
fn completions_bash_generates_output() {
let mut cmd = Cli::command();
let mut buf = Vec::new();
generate(clap_complete::Shell::Bash, &mut cmd, "reliary", &mut buf);
let output = String::from_utf8_lossy(&buf).to_string();
assert!(output.contains("reliary"), "Bash completions should mention binary name");
assert!(output.contains("completions"), "Bash completions should list 'completions' subcommand");
assert!(output.contains("search"), "Bash completions should list 'search' subcommand");
assert!(output.contains("update"), "Bash completions should list 'update' subcommand");
assert!(output.contains("trust"), "Bash completions should list 'trust' subcommand");
}
#[test]
fn completions_zsh_generates_output() {
let mut cmd = Cli::command();
let mut buf = Vec::new();
generate(clap_complete::Shell::Zsh, &mut cmd, "reliary", &mut buf);
let output = String::from_utf8_lossy(&buf).to_string();
assert!(output.contains("reliary"), "Zsh completions should mention binary name");
}
#[test]
fn completions_fish_generates_output() {
let mut cmd = Cli::command();
let mut buf = Vec::new();
generate(clap_complete::Shell::Fish, &mut cmd, "reliary", &mut buf);
let output = String::from_utf8_lossy(&buf).to_string();
assert!(output.contains("reliary"), "Fish completions should mention binary name");
}
#[test]
fn completions_powershell_generates_output() {
let mut cmd = Cli::command();
let mut buf = Vec::new();
generate(clap_complete::Shell::PowerShell, &mut cmd, "reliary", &mut buf);
let output = String::from_utf8_lossy(&buf).to_string();
assert!(output.contains("reliary"), "PowerShell completions should mention binary name");
}
#[test]
fn completions_elvish_generates_output() {
let mut cmd = Cli::command();
let mut buf = Vec::new();
generate(clap_complete::Shell::Elvish, &mut cmd, "reliary", &mut buf);
let output = String::from_utf8_lossy(&buf).to_string();
assert!(output.contains("reliary"), "Elvish completions should mention binary name");
}
#[test]
fn man_page_generates() {
let cmd = Cli::command();
let man = clap_mangen::Man::new(cmd);
let mut buf = Vec::new();
man.render(&mut buf).expect("Failed to render man page");
let output = String::from_utf8_lossy(&buf).to_string();
assert!(output.contains(".TH reliary"), "Man page should have TH header");
assert!(output.contains("search"), "Man page should document search");
assert!(output.contains("server"), "Man page should document MCP server");
assert!(output.contains("completions"), "Man page should document completions");
assert!(output.contains("update"), "Man page should document update");
assert!(output.contains("trust"), "Man page should document trust");
}
#[test]
fn no_color_env_var() {
std::env::set_var("NO_COLOR", "1");
assert!(color::green("test") == "test", "NO_COLOR should disable green");
assert!(color::red("test") == "test", "NO_COLOR should disable red");
assert!(color::yellow("test") == "test", "NO_COLOR should disable yellow");
assert!(color::bold("test") == "test", "NO_COLOR should disable bold");
assert!(color::dim("test") == "test", "NO_COLOR should disable dim");
assert!(color::reset("").is_empty(), "NO_COLOR should disable reset");
assert!(!color::is_enabled(), "is_enabled should return false with NO_COLOR");
std::env::remove_var("NO_COLOR");
assert!(!color::is_enabled(), "piped stderr must disable color (TTY gate)");
std::env::set_var("CLICOLOR_FORCE", "1");
assert!(color::green("test").contains("\x1b[32m"), "CLICOLOR_FORCE=1 must force ANSI");
assert!(color::is_enabled(), "is_enabled should return true with CLICOLOR_FORCE=1");
std::env::remove_var("CLICOLOR_FORCE");
}
#[test]
fn trust_creates_reliary_dir() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().to_str().unwrap();
do_trust(path);
assert!(tmp.path().join(".reliary").exists(), "trust should create .reliary dir");
}
#[test]
fn trust_idempotent() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().to_str().unwrap();
do_trust(path);
do_trust(path); assert!(tmp.path().join(".reliary").exists(), ".reliary should still exist");
}
#[test]
fn validate_config_rejects_unknown_keys() {
let tmp = tempfile::tempdir().unwrap();
let reliary_dir = tmp.path().join(".reliary");
std::fs::create_dir_all(&reliary_dir).unwrap();
let config_path = reliary_dir.join("config.json");
std::fs::write(&config_path, r#"{"mode": "strict", "badkey": "value"}"#).unwrap();
validate_config(tmp.path().to_str().unwrap());
}
#[test]
fn validate_config_accepts_valid() {
let tmp = tempfile::tempdir().unwrap();
let reliary_dir = tmp.path().join(".reliary");
std::fs::create_dir_all(&reliary_dir).unwrap();
let config_path = reliary_dir.join("config.json");
std::fs::write(&config_path, r#"{"mode": "fast", "features": {"compress": true, "healEdit": false}}"#).unwrap();
validate_config(tmp.path().to_str().unwrap());
}
#[test]
fn validate_config_rejects_invalid_json() {
let tmp = tempfile::tempdir().unwrap();
let reliary_dir = tmp.path().join(".reliary");
std::fs::create_dir_all(&reliary_dir).unwrap();
let config_path = reliary_dir.join("config.json");
std::fs::write(&config_path, "not json {{{").unwrap();
validate_config(tmp.path().to_str().unwrap());
}
#[test]
fn validate_config_rejects_invalid_mode() {
let tmp = tempfile::tempdir().unwrap();
let reliary_dir = tmp.path().join(".reliary");
std::fs::create_dir_all(&reliary_dir).unwrap();
let config_path = reliary_dir.join("config.json");
std::fs::write(&config_path, r#"{"mode": "invalid_mode"}"#).unwrap();
validate_config(tmp.path().to_str().unwrap());
}
#[test]
fn validate_config_rejects_invalid_feature() {
let tmp = tempfile::tempdir().unwrap();
let reliary_dir = tmp.path().join(".reliary");
std::fs::create_dir_all(&reliary_dir).unwrap();
let config_path = reliary_dir.join("config.json");
std::fs::write(&config_path, r#"{"features": {"badFeature": true}}"#).unwrap();
validate_config(tmp.path().to_str().unwrap());
}
#[test]
fn verbose_flag_parsed() {
let cli = Cli::try_parse_from(["reliary", "-vv", "search", "test", "."]);
assert!(cli.is_ok());
assert_eq!(cli.unwrap().verbose, 2);
}
#[test]
fn quiet_flag_parsed() {
let cli = Cli::try_parse_from(["reliary", "-q", "search", "test", "."]);
assert!(cli.is_ok());
assert!(cli.unwrap().quiet);
}
#[test]
fn format_flag_parsed() {
let cli = Cli::try_parse_from(["reliary", "-f", "json", "search", "test", "."]);
assert!(cli.is_ok());
assert_eq!(cli.unwrap().format, "json");
}
#[test]
fn completions_outdir_creates_file() {
let tmp = tempfile::tempdir().unwrap();
let outdir = tmp.path().to_str().unwrap();
let cli = Cli::try_parse_from(["reliary", "completions", "bash", "--outdir", outdir]);
assert!(cli.is_ok());
}
#[test]
fn update_check_flag_parsed() {
let cli = Cli::try_parse_from(["reliary", "update", "--check"]);
assert!(cli.is_ok());
match cli.unwrap().command {
Commands::Update { check } => assert!(check),
_ => panic!("Expected Update command"),
}
}
#[test]
fn trust_path_parsed() {
let cli = Cli::try_parse_from(["reliary", "trust", "/tmp/test"]);
assert!(cli.is_ok());
match cli.unwrap().command {
Commands::Trust { path } => assert_eq!(path, "/tmp/test"),
_ => panic!("Expected Trust command"),
}
}
#[test]
fn trust_default_path() {
let cli = Cli::try_parse_from(["reliary", "trust"]);
assert!(cli.is_ok());
match cli.unwrap().command {
Commands::Trust { path } => assert_eq!(path, "."),
_ => panic!("Expected Trust command with default path"),
}
}
#[test]
fn man_page_has_all_sections() {
let cmd = Cli::command();
let man = clap_mangen::Man::new(cmd);
let mut buf = Vec::new();
man.render(&mut buf).unwrap();
let output = String::from_utf8_lossy(&buf).to_string();
assert!(output.contains("NAME"), "Man page should have NAME section");
assert!(output.contains("DESCRIPTION") || output.contains("SYNOPSIS"), "Man page should have DESCRIPTION or SYNOPSIS");
assert!(output.contains("OPTIONS"), "Man page should have OPTIONS");
}
#[test]
fn completions_includes_global_flags() {
let mut cmd = Cli::command();
let mut buf = Vec::new();
generate(clap_complete::Shell::Bash, &mut cmd, "reliary", &mut buf);
let output = String::from_utf8_lossy(&buf).to_string();
assert!(output.contains("--format") || output.contains("format"), "Completions should include --format flag");
assert!(output.contains("--verbose") || output.contains("verbose"), "Completions should include --verbose flag");
assert!(output.contains("--quiet") || output.contains("quiet"), "Completions should include --quiet flag");
}
#[test]
fn color_module_unit_tests() {
std::env::set_var("NO_COLOR", "1");
assert_eq!(color::green("hello"), "hello");
assert_eq!(color::red("hello"), "hello");
assert_eq!(color::yellow("hello"), "hello");
assert_eq!(color::bold("hello"), "hello");
assert_eq!(color::dim("hello"), "hello");
assert_eq!(color::reset(""), "");
assert!(!color::is_enabled());
std::env::remove_var("NO_COLOR");
assert!(color::green("hello").contains("hello"));
assert!(color::red("hello").contains("hello"));
assert!(!color::is_enabled(), "piped stderr must disable color (TTY gate)");
std::env::set_var("CLICOLOR_FORCE", "1");
assert!(color::is_enabled(), "CLICOLOR_FORCE=1 must re-enable color");
std::env::remove_var("CLICOLOR_FORCE");
}
#[test]
fn index_db_path_format() {
assert_eq!(index_db_path("/tmp/test"), "/tmp/test/.reliary/index.sqlite");
assert_eq!(index_db_path("/tmp/test/"), "/tmp/test/.reliary/index.sqlite");
}
}
fn pipe_to_pager(text: &str) {
if !std::io::stdout().is_terminal() || text.len() < 4096 {
print!("{}", text);
return;
}
let pager = std::env::var("PAGER").unwrap_or_else(|_| "less -RF".to_string());
let parts: Vec<&str> = pager.splitn(2, ' ').collect();
let prog = parts[0];
let args: Vec<&str> = if parts.len() > 1 { parts[1].split(' ').collect() } else { vec![] };
match std::process::Command::new(prog).args(&args).stdin(std::process::Stdio::piped()).spawn() {
Ok(mut child) => {
if let Some(ref mut stdin) = child.stdin {
let _ = stdin.write_all(text.as_bytes());
}
let _ = child.wait();
}
Err(_) => print!("{}", text),
}
}
fn base_identity(base: &str, base_dir: &str) -> String {
let rev = std::process::Command::new("git")
.arg("-C").arg(base_dir)
.arg("rev-parse").arg("HEAD")
.output();
if let Ok(out) = rev {
if out.status.success() {
return String::from_utf8_lossy(&out.stdout).trim().to_string();
}
}
let mut h = <sha2::Sha256 as sha2::Digest>::new();
for e in walkdir::WalkDir::new(base_dir)
.into_iter()
.filter_entry(|e| {
if e.depth() == 0 { return true; }
let n = e.file_name().to_str().unwrap_or("");
!n.starts_with('.') && !reliary_search::ingest::SKIP_DIRS.contains(&n)
})
.filter_map(|e| e.ok())
{
if !e.file_type().is_file() { continue; }
let Ok(m) = e.metadata() else { continue };
let rel = e.path().strip_prefix(base_dir).unwrap_or(e.path());
let mt = m.modified().ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_nanos())
.unwrap_or(0);
<sha2::Sha256 as sha2::Digest>::update(&mut h, rel.to_string_lossy().as_bytes());
<sha2::Sha256 as sha2::Digest>::update(&mut h, m.len().to_le_bytes());
<sha2::Sha256 as sha2::Digest>::update(&mut h, mt.to_le_bytes());
}
let out = <sha2::Sha256 as sha2::Digest>::finalize(h);
format!("{base}:{out:x}")
}
pub fn index_db_path(path: &str) -> String {
format!("{}/.reliary/index.sqlite", path.trim_end_matches('/'))
}
pub fn index_db_path_fallible(path: &str) -> Option<std::path::PathBuf> {
let s = format!("{}/.reliary/index.sqlite", path.trim_end_matches('/'));
let p = std::path::PathBuf::from(s);
if p.exists() { Some(p) } else { None }
}
fn ensure_occurrence_built(db: &rusqlite::Connection) {
let has_any: i64 = db
.query_row("SELECT EXISTS(SELECT 1 FROM occurrence LIMIT 1)", [], |r| r.get(0))
.unwrap_or(1);
if has_any == 0 {
if let Err(e) = reliary_search::lazy_occurrence::build_all_occurrence(db) {
eprintln!("warning: could not build occurrence table: {}", e);
}
}
}
pub fn cli_index_db_path(path: &str) -> Option<String> {
crate::index_root::nearest_index_root(std::path::Path::new(path))
.map(|root| crate::index_root::db_path_for(&root).to_string_lossy().to_string())
}
pub fn cli_index_db_path_or_literal(path: &str) -> String {
cli_index_db_path(path).unwrap_or_else(|| index_db_path(path))
}
pub fn run_index(path: &str) -> bool {
let db_path_str = index_db_path(path);
if let Some(parent) = std::path::Path::new(&db_path_str).parent() {
let _ = std::fs::create_dir_all(parent);
crate::paths::ensure_reliary_ignored(parent);
}
let bak_path = format!("{}.bak", db_path_str);
let _ = std::fs::remove_file(&bak_path); if std::path::Path::new(&db_path_str).exists() {
let _ = std::fs::rename(&db_path_str, &bak_path);
}
match reliary_core::safe_open_db(&db_path_str) {
Ok(db) => {
if reliary_search::schema::create_new_db(&db).is_err() {
let _ = std::fs::rename(&bak_path, &db_path_str);
eprintln!("{} Database schema creation failed (old index restored)", color::red("✗"));
return false;
}
let result = crate::ux::with_spinner(&format!("indexing {}", path), || {
reliary_search::ingest::index_directory(&db, path)
});
match result {
Ok(count) => {
let _ = std::fs::remove_file(&bak_path);
if let Err(e) = reliary_search::schema::bump_index_gen(&db) {
eprintln!("warning: could not update the index generation stamp: {}", e);
}
if let Err(e) = reliary_search::schema::write_indexer_fingerprint(&db) {
eprintln!("warning: could not record the indexer fingerprint: {}", e);
}
eprintln!("{} {} files indexed", color::green("✓"), count);
true
}
Err(e) => {
drop(db);
let _ = std::fs::rename(&bak_path, &db_path_str);
eprintln!("{} Indexing error: {} (old index restored)", color::red("✗"), e);
false
}
}
}
Err(e) => {
let _ = std::fs::rename(&bak_path, &db_path_str);
eprintln!("{} DB create error: {} (old index restored)", color::red("✗"), e);
false
}
}
}
pub fn run_vacuum(path: &str) {
let db_path = match cli_index_db_path(path) {
Some(p) => p,
None => {
eprintln!("{} no index at or above {}", color::red("✗"), path);
std::process::exit(2);
}
};
let before = match std::fs::metadata(&db_path) {
Ok(m) => m.len(),
Err(e) => { eprintln!("{} stat: {}", color::red("✗"), e); std::process::exit(1); }
};
eprintln!("vacuuming {} ({} bytes)...", db_path, before);
match rusqlite::Connection::open(&db_path) {
Ok(db) => {
let _ = db.execute_batch("PRAGMA busy_timeout=5000;");
if let Err(e) = db.execute_batch("VACUUM;") {
eprintln!("{} VACUUM failed: {}", color::red("✗"), e);
std::process::exit(1);
}
}
Err(e) => { eprintln!("{} open: {}", color::red("✗"), e); std::process::exit(1); }
}
let after = std::fs::metadata(&db_path).map(|m| m.len()).unwrap_or(0);
let saved = before.saturating_sub(after);
eprintln!("{} {} → {} bytes ({} saved)",
color::green("✓"), before, after, saved);
}
pub fn run_build_occurrences(path: &str) {
let db_path = match cli_index_db_path(path) {
Some(p) => p,
None => { eprintln!("{} no index at or above {}", color::red("✗"), path); std::process::exit(2); }
};
let db = match rusqlite::Connection::open(&db_path) {
Ok(d) => { let _ = d.execute_batch("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA busy_timeout=5000;"); d }
Err(e) => { eprintln!("{} open: {}", color::red("✗"), e); std::process::exit(1); }
};
let start = std::time::Instant::now();
let unresolved = match reliary_search::lazy_occurrence::count_unresolved_phrases(&db) {
Ok(n) => n,
Err(e) => { eprintln!("{} count: {}", color::red("✗"), e); std::process::exit(1); }
};
eprintln!("building occurrence rows for {} unresolved phrases...", unresolved);
let built = match reliary_search::lazy_occurrence::build_all_occurrence(&db) {
Ok(n) => n,
Err(e) => { eprintln!("{} build: {}", color::red("✗"), e); std::process::exit(1); }
};
eprintln!("{} {} rows in {:?}", color::green("✓"), built, start.elapsed());
}
pub fn run_build_all(path: &str) {
let db_path = match cli_index_db_path(path) {
Some(p) => p,
None => { eprintln!("{} no index at or above {}", color::red("✗"), path); std::process::exit(2); }
};
let db = match rusqlite::Connection::open(&db_path) {
Ok(d) => { let _ = d.execute_batch("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA busy_timeout=5000;"); d }
Err(e) => { eprintln!("{} open: {}", color::red("✗"), e); std::process::exit(1); }
};
let start = std::time::Instant::now();
let file_ids: Vec<i64> = {
let mut stmt = match db.prepare_cached("SELECT id FROM file_map") {
Ok(s) => s,
Err(e) => { eprintln!("{} prepare: {}", color::red("✗"), e); std::process::exit(1); }
};
let rows = match stmt.query_map([], |r| r.get::<_, i64>(0)) {
Ok(r) => r,
Err(e) => { eprintln!("{} query: {}", color::red("✗"), e); std::process::exit(1); }
};
rows.filter_map(|x| x.ok()).collect()
};
eprintln!("building lazy tables for {} files...", file_ids.len());
let _total_blocks = 0usize;
let mut total_blocks = 0usize;
for (i, fid) in file_ids.iter().enumerate() {
total_blocks += reliary_search::lazy_tables::ensure_blocks_for_file(&db, *fid).unwrap_or(0);
if (i + 1) % 5000 == 0 {
eprintln!(" ... {}/{} files", i + 1, file_ids.len());
}
}
let inserted = reliary_search::lazy_occurrence::build_all_occurrence(&db).unwrap_or(0);
let total_occ: i64 = db
.query_row("SELECT COUNT(*) FROM occurrence", [], |r| r.get(0))
.unwrap_or(0);
let _ = total_blocks;
eprintln!("{} blocks={} occurrence={} (+{} new) in {:?}",
color::green("✓"), total_blocks, total_occ, inserted, start.elapsed());
}
pub fn run_reindex_file(file: &str) {
let idx_path = match cli_index_db_path(file) {
Some(p) => p,
None => {
eprintln!("{} no usable .reliary index found for {}", color::yellow("⚠"), file);
std::process::exit(1);
}
};
match reliary_core::safe_read(file) {
Ok(content) => {
let count = crate::reindex::reindex_single_file(&idx_path, file, &content);
let had_content = !content.trim().is_empty();
if count > 0 {
eprintln!("{} reindexed {} tokens from {}", color::green("✓"), count, file);
} else if had_content {
eprintln!("{} reindex failed for {}", color::red("✗"), file);
std::process::exit(1);
} else {
eprintln!("{} {} is empty (nothing to index)", color::yellow("⊙"), file);
}
}
Err(e) => {
eprintln!("{} cannot read {}: {}", color::red("✗"), file, e);
std::process::exit(1);
}
}
}
pub fn run_who_calls(file: &str, identifier: &str) {
let idx_path = match cli_index_db_path(file) {
Some(p) => p,
None => {
eprintln!("{} no usable .reliary index found for {}", color::red("✗"), file);
std::process::exit(1);
}
};
if let Ok(db) = rusqlite::Connection::open(&idx_path) {
let _ = db.execute_batch("PRAGMA synchronous = NORMAL;");
let results = reliary_search::search::search_fts5(&db, identifier, 10);
let file_name = std::path::Path::new(file).file_name()
.and_then(|n| n.to_str()).unwrap_or("");
let refs: Vec<String> = results.iter()
.filter(|r| {
let is_same = r.file == file
|| (!file_name.is_empty() && r.file.ends_with(&format!("/{}", file_name)));
!is_same
})
.map(|r| r.file.clone())
.collect();
println!("{}", serde_json::to_string(&refs).unwrap_or_else(|_| "[]".to_string()));
} else {
eprintln!("{} cannot open index at {}", color::red("✗"), idx_path);
std::process::exit(1);
}
}
fn open_index_or_prompt(path: &str) -> Option<rusqlite::Connection> {
let assume_yes = std::env::var("RELIARY_YES").is_ok();
let db_path = match cli_index_db_path(path) {
Some(p) => p,
None => {
if !assume_yes && !std::io::IsTerminal::is_terminal(&std::io::stdin()) {
eprintln!("{} No project index found and stdin is not a TTY.", color::red("✗"));
eprintln!(" Run `reliary trust {}` to build it, or re-run with `--yes`.", path);
std::process::exit(2);
}
if !assume_yes {
eprint!("{} No project index found. Build it now? [Y/n] ", color::yellow("⚠"));
std::io::stdout().flush().ok(); let mut input = String::new();
std::io::stdin().read_line(&mut input).ok(); if input.trim().to_lowercase() == "n" {
return None;
}
}
let _ = run_index(path);
index_db_path(path)
}
};
let db = rusqlite::Connection::open(&db_path).ok()?;
let _ = db.execute_batch("PRAGMA synchronous=NORMAL;");
if reliary_search::schema::open_existing_db_safe(&db).is_err() {
eprintln!("{} Index schema mismatch or corrupt. Rebuilding...", color::yellow("⚠"));
let _ = run_index(path);
let db = rusqlite::Connection::open(&db_path).ok()?;
let _ = db.execute_batch("PRAGMA synchronous=NORMAL;");
reliary_search::schema::open_existing_db_safe(&db).ok()?;
return Some(db);
}
Some(db)
}
const VERSION: &str = env!("CARGO_PKG_VERSION");
fn build_cli() -> clap::Command {
Cli::command()
}
#[derive(Parser)]
#[command(
name = "reliary",
version = VERSION,
about = "Grammar-free code intelligence CLI and MCP server",
after_help = "\
EXAMPLES:
reliary index . Build search index for current project
reliary search query . Search indexed project
reliary risk src/main.rs Check edit risk before making changes
reliary init Auto-configure agents (Pi, Claude Code, OpenCode, Cline)
reliary doctor System health check
reliary doctor --fix Check and fix issues automatically
reliary completions bash Generate bash completions
reliary man Generate man page
ALIAS:
Short: 'rel' also works for every command, e.g. 'rel search', 'rel doctor'.
Run `reliary init` to install the 'rel' shim into ~/.local/bin.
ENVIRONMENT:
NO_COLOR Disable colored output
RELIARY_MODE Override safety mode (fast/reactive/strict)
RELIARY_FEATURES Toggle features (+compress,-healEdit)
RELIARY_LOG Log level (error/warn/info/debug/trace)"
)]
struct Cli {
#[command(subcommand)]
command: Commands,
#[arg(short, long, default_value = "default", global = true)]
format: String,
#[arg(short, long, action = clap::ArgAction::Count, global = true)]
verbose: u8,
#[arg(short, long, global = true)]
quiet: bool,
#[arg(long, global = true)]
yes: bool,
}
pub const CLI_COMMANDS: &[&str] = &[
"search", "index", "compress", "risk",
"init", "uninstall", "doctor", "status",
"clean", "logs", "config",
"dead", "sift", "wrap", "trust",
"completions", "man", "update",
"fix", "verify", "impact", "test-plan", "diff", "map", "bench",
];
#[derive(Clone, Copy, Debug, ValueEnum)]
enum BenchAction {
Gen,
Verify,
}
#[derive(Subcommand)]
enum Commands {
Search { query: String, #[arg(default_value = ".")] path: String },
Index { path: String },
ReindexFile { file: String },
Vacuum { path: String },
BuildOccurrences { path: String },
BuildAll { path: String },
WhoCalls { file: String, identifier: String },
Compress {
text: Option<String>,
#[arg(long)] gentle: bool,
},
Risk { file: String },
Pack {
path: String,
#[arg(long, default_value = "l2l3")]
format: String,
#[arg(long, default_value = "full")]
strategy: String,
#[arg(long, default_value = "50")]
top_k: usize,
#[arg(long)]
auto: bool,
#[arg(long)]
slice_query: Option<String>,
},
Doctor {
#[arg(long)]
fix: bool,
},
Status {
#[arg(long, default_value = ".")]
path: String,
},
Clean {
#[arg(long)]
global: bool,
#[arg(long)]
all: bool,
},
Logs {
#[arg(long, conflicts_with = "level")]
tail: bool,
#[arg(long)]
level: Option<String>,
},
Sift {
#[arg(long)]
stdin: bool,
#[arg(long)]
llm: bool,
#[arg(long)]
aggressive: bool,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
command: Vec<String>,
},
Wrap {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
command: Vec<String>,
},
CacheStore {
#[arg(long)]
text: Option<String>,
},
CacheRetrieve {
hash: String,
},
CacheStats,
Config {
key: Option<String>,
value: Option<String>,
#[arg(long)] local: bool,
#[arg(long)] root: Option<String>,
},
Init {
#[arg(long)]
dry_run: bool,
},
Uninstall,
Dead { path: String },
Trust {
#[arg(default_value = ".")]
path: String,
},
Update {
#[arg(long)]
check: bool,
},
Fix {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
task: Vec<String>,
#[arg(long, default_value = ".")]
path: String,
#[arg(long, default_value = "12")]
max_iters: usize,
#[arg(long)]
verify: Option<String>,
#[arg(long)]
dry_run: bool,
#[arg(long)]
json: bool,
},
Verify {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
claim: Vec<String>,
#[arg(long, default_value = ".")]
path: String,
#[arg(long, default_value = "1")]
tol: i32,
#[arg(long)]
json: bool,
},
Impact {
name: String,
#[arg(long, default_value = ".")]
path: String,
#[arg(long)]
json: bool,
},
TestPlan {
#[arg(long, value_delimiter = ',')]
files: Vec<String>,
#[arg(long, value_delimiter = ',')]
symbols: Vec<String>,
#[arg(long)]
diff: Option<String>,
#[arg(long, default_value = ".")]
path: String,
#[arg(long)]
json: bool,
},
Diff {
rev1: String,
rev2: String,
#[arg(long, default_value = ".")]
path: String,
#[arg(long)]
json: bool,
},
Map {
#[arg(long, default_value = ".")]
path: String,
#[arg(short, long)]
out: Option<String>,
#[arg(long, default_value = "24")]
max_files: usize,
#[arg(long, default_value = "10")]
max_symbols: usize,
},
Warrant {
#[arg(long, default_value = "HEAD")]
base: String,
#[arg(long, default_value = ".")]
head: String,
#[arg(long)]
contract: String,
#[arg(long)]
cert: Option<String>,
#[arg(long)]
strict_unseen: bool,
#[arg(long)]
no_cache: bool,
},
Bench {
#[arg(value_enum)]
action: BenchAction,
#[arg(long)]
index: Option<String>,
#[arg(long)]
out: Option<String>,
#[arg(long)]
gt: Option<String>,
#[arg(long, default_value = "42")]
seed: u64,
#[arg(long, default_value = "1")]
tol: i32,
#[arg(long, default_value = ".")]
path: String,
},
Completions {
#[arg(value_enum)]
shell: Shell,
#[arg(short, long)]
outdir: Option<String>,
},
Man {
#[arg(short, long)]
outdir: Option<String>,
},
#[command(hide = true)]
Mcp,
#[command(hide = true)]
Memory { query: String },
#[command(hide = true)]
SessionState { file: String },
#[command(hide = true)]
Classify {
file: String,
line: i32,
stem: String,
},
#[command(hide = false)]
ParseExpr {
line: String,
#[arg(default_value = ".")] path: String,
},
}
#[derive(ValueEnum, Clone)]
#[allow(clippy::enum_variant_names)]
enum Shell {
Bash,
Zsh,
Fish,
PowerShell,
Elvish,
}
fn format_config(fmt: &str) -> reliary_core::OutputFormat {
match fmt {
"compact" => reliary_core::OutputFormat::Compact,
"json" => reliary_core::OutputFormat::Json,
_ => reliary_core::OutputFormat::Default,
}
}
fn read_prefix(path: &std::path::Path, n: usize) -> std::io::Result<Vec<u8>> {
use std::io::Read;
let mut f = std::fs::File::open(path)?;
let mut buf = vec![0u8; n];
let mut filled = 0usize;
while filled < n {
match f.read(&mut buf[filled..]) {
Ok(0) => break,
Ok(k) => filled += k,
Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
}
}
buf.truncate(filled);
Ok(buf)
}
fn effective_command(program: &str, args: &[String]) -> (String, Vec<String>) {
if !matches!(program, "bash" | "sh" | "zsh" | "dash") {
return (program.to_string(), args.to_vec());
}
let Some(ci) = args.iter().position(|a| a == "-c") else {
return (program.to_string(), args.to_vec());
};
let Some(script) = args.get(ci + 1) else {
return (program.to_string(), args.to_vec());
};
if script.contains(['&', '|', ';', '>', '<', '`', '$', '\n', '(', ')']) {
return (program.to_string(), args.to_vec());
}
let mut toks = script.split_whitespace();
let Some(first) = toks.next() else {
return (program.to_string(), args.to_vec());
};
let inner: Vec<String> = toks.map(|s| s.to_string()).collect();
(first.to_string(), inner)
}
fn exec_wrap(cmd: &[String]) {
if cmd.is_empty() {
eprintln!("Usage: reliary wrap <command> [args...]");
std::process::exit(1);
}
let program = &cmd[0];
let args = &cmd[1..];
let program_name_v67 = std::path::Path::new(program).file_name()
.and_then(|n| n.to_str()).unwrap_or(program);
let (eff_program, eff_arg_strings): (String, Vec<String>) =
effective_command(program_name_v67, args);
let eff_program_name = std::path::Path::new(&eff_program).file_name()
.and_then(|n| n.to_str()).unwrap_or(&eff_program).to_string();
if matches!(eff_program_name.as_str(), "cat" | "head" | "tail" | "less" | "more" | "bat") {
let file_args: Vec<&String> = eff_arg_strings.iter().filter(|a| !a.starts_with('-')).collect();
if file_args.len() == 1 {
let path = std::path::Path::new(file_args[0].as_str());
if path.is_file() {
if let Ok(sample) = read_prefix(path, 65536) {
let text = String::from_utf8_lossy(&sample);
if reliary_search::lazy_occurrence::is_source_like(&text) {
let output = match std::process::Command::new(program)
.args(args)
.stdin(std::process::Stdio::inherit())
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.status()
{
Ok(s) => s,
Err(e) => {
eprintln!("Error executing '{}': {}", program, e);
std::process::exit(1);
}
};
std::process::exit(output.code().unwrap_or(1));
}
}
}
}
}
use std::io::IsTerminal;
let stdin_cfg = if std::io::stdin().is_terminal() {
std::process::Stdio::inherit()
} else {
std::process::Stdio::null()
};
let output = match std::process::Command::new(program)
.args(args)
.stdin(stdin_cfg)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::inherit())
.output()
{
Ok(o) => o,
Err(e) => {
eprintln!("Error executing '{}': {}", program, e);
std::process::exit(1);
}
};
let raw = String::from_utf8_lossy(&output.stdout);
let exit_code = output.status.code().unwrap_or_else(|| {
#[cfg(unix)]
{ use std::os::unix::process::ExitStatusExt; output.status.signal().map(|s| 128 + s).unwrap_or(1) }
#[cfg(not(unix))]
{ 1 }
});
if raw.is_empty() {
std::process::exit(exit_code);
}
let cache_path = std::path::Path::new(".reliary/cache.sqlite");
std::fs::create_dir_all(".reliary").ok(); crate::paths::ensure_reliary_ignored(std::path::Path::new(".reliary"));
let hash = open_or_create(cache_path)
.ok()
.and_then(|conn| reliary_core::store(&conn, &raw).ok());
let program_name = std::path::Path::new(program).file_name()
.and_then(|n| n.to_str()).unwrap_or(program);
let args_joined = args.join(" ");
let is_pytest = args_joined.contains("pytest") || program_name == "pytest";
let is_cargo = program_name == "cargo" || args_joined.contains(" cargo ");
let is_test_cmd = is_pytest || is_cargo;
let is_test_passing = exit_code == 0 && is_test_cmd;
let compressed = if is_test_passing {
let passed_count = if is_pytest {
raw.lines().rev().take(3).find_map(|l| {
let parts: Vec<&str> = l.split_whitespace().collect();
for (i, p) in parts.iter().enumerate() {
if *p == "passed" && i > 0 {
return parts.get(i - 1).and_then(|n| n.parse::<usize>().ok());
}
}
None
}).unwrap_or(0)
} else {
raw.lines().find_map(|l| {
if l.contains("test result: ok") {
l.split_whitespace().nth(4).and_then(|n| n.parse::<usize>().ok())
} else { None }
}).unwrap_or(0)
};
if passed_count > 0 {
format!("[reliary: {} tests passed]", passed_count)
} else {
reliary_output::compress_unified(&raw)
}
} else {
match program_name {
"cat" | "head" | "tail" | "less" | "more" => sift_file_read(&raw, args),
_ if is_test_cmd => sift_test_output(&raw, if is_pytest { "pytest" } else { "cargo" }, exit_code),
_ => reliary_output::compress_unified(&raw),
}
};
print!("{}", compressed);
if let Some(h) = hash {
eprintln!("\n[reliary-compressed {}]", h);
}
std::process::exit(exit_code);
}
fn open_or_create(path: &std::path::Path) -> Result<rusqlite::Connection, String> {
crate::paths::open_or_create(path)
}
fn dummy_conn() -> rusqlite::Connection {
let conn = rusqlite::Connection::open_in_memory().unwrap();
let _ = conn.execute_batch(
"CREATE TABLE IF NOT EXISTS content_cache (
hash TEXT PRIMARY KEY, original BLOB NOT NULL,
stored_at INTEGER NOT NULL, accessed_at INTEGER NOT NULL
);"
);
conn
}
fn exec_sift(cmd: &[String], stdin_mode: bool) {
if stdin_mode {
let raw = match reliary_core::safe_read_stdin() {
Ok(buf) => buf,
Err(e) => { eprintln!("stdin: {}", e); std::process::exit(1); }
};
if raw.is_empty() { std::process::exit(0); }
let compressed = reliary_output::compress_unified(&raw);
if compressed.len() < raw.len() {
if let Ok(Some(path)) = tee::save_tee(&raw) {
eprintln!("[full output: {}]", path);
}
print!("{}", compressed);
} else {
print!("{}", raw);
}
std::process::exit(0);
}
if cmd.is_empty() {
eprintln!("Usage: reliary sift <command> [args...] | sift --stdin");
std::process::exit(1);
}
let program = &cmd[0];
let args = &cmd[1..];
let expand = std::env::var("RELIARY_SIFT_EXPAND").is_ok_and(|v| v == "1" || v == "true");
let output = match std::process::Command::new(program)
.args(args)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::inherit())
.output()
{
Ok(o) => o,
Err(e) => {
eprintln!("Error executing '{}': {}", program, e);
std::process::exit(1);
}
};
let raw = String::from_utf8_lossy(&output.stdout);
let exit_code = output.status.code().unwrap_or_else(|| {
#[cfg(unix)]
{ use std::os::unix::process::ExitStatusExt; output.status.signal().map(|s| 128 + s).unwrap_or(1) }
#[cfg(not(unix))]
{ 1 }
});
if expand {
print!("{}", raw);
} else {
let program_name = std::path::Path::new(program).file_name()
.and_then(|n| n.to_str()).unwrap_or(program);
let args_joined = args.join(" ");
let is_pytest = args_joined.contains("pytest") || program_name == "pytest";
let is_cargo = program_name == "cargo" || args_joined.contains(" cargo ");
let is_test_cmd = is_pytest || is_cargo;
let compressed = match program_name {
"cat" | "head" | "tail" | "less" | "more" => {
sift_file_read(&raw, args)
}
_ if is_test_cmd => {
sift_test_output(&raw, if is_pytest { "pytest" } else { "cargo" }, exit_code)
}
_ => {
reliary_output::compress_unified(&raw)
}
};
if compressed.len() < raw.len() {
if let Ok(Some(path)) = tee::save_tee(&raw) {
eprintln!("[full output: {}]", path);
}
print!("{}", compressed);
} else {
print!("{}", raw);
}
}
std::process::exit(exit_code);
}
fn sift_file_read(raw: &str, args: &[String]) -> String {
if args.len() == 1 && !raw.is_empty() {
let path = &args[0];
let footer = build_read_footer(path);
let compressed = reliary_output::compress_unified(raw);
if footer.is_empty() {
compressed
} else {
format!("{}\n[reliary: {}]\n", compressed, footer)
}
} else {
reliary_output::compress_unified(raw)
}
}
fn find_open_index(start_path: &str) -> Option<rusqlite::Connection> {
let root = crate::index_root::nearest_index_root(std::path::Path::new(start_path))?;
let candidate = crate::index_root::db_path_for(&root);
rusqlite::Connection::open_with_flags(&candidate, rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY)
.ok()
.inspect(|d| {
let _ = d.execute_batch("PRAGMA synchronous = NORMAL;");
})
}
fn build_read_footer(path: &str) -> String {
let db = match find_open_index(path) {
Some(d) => d,
None => return String::new(),
};
let file_id = match db.query_row(
"SELECT id FROM file_map WHERE file_path = ?1 OR file_path LIKE '%/' || ?1 LIMIT 1",
rusqlite::params![path, path],
|row| row.get::<_, i64>(0),
) {
Ok(id) => id,
Err(_) => return String::new(),
};
let mut identifiers: Vec<String> = Vec::new();
if let Ok(mut stmt) = db.prepare(
"SELECT p.phrase FROM file_phrases fp JOIN phrases p ON fp.phrase_id = p.id WHERE fp.file_id = ?1 LIMIT 30"
) {
if let Ok(rows) = stmt.query_map([file_id], |row| row.get::<_, String>(0)) {
for r in rows.flatten() {
if r.len() >= 5
&& r.chars().next().is_some_and(|c| c.is_alphabetic())
&& !r.chars().any(|c| c.is_ascii_digit())
&& r != "record" && r != "config" && r != "return" && r != "self"
&& r != "kwargs" && r != "args" && r != "None" && r != "True" && r != "False"
&& r != "process_batch" {
identifiers.push(r);
}
}
}
}
if identifiers.is_empty() {
return String::new();
}
let first_id = identifiers.iter().max_by_key(|s| s.len()).unwrap();
let caller_count: usize = db.query_row(
"SELECT COUNT(DISTINCT fp.file_id) FROM file_phrases fp JOIN phrases p ON fp.phrase_id = p.id WHERE p.phrase = ?1",
rusqlite::params![first_id],
|row| row.get::<_, i64>(0),
).map(|c: i64| c.saturating_sub(1).max(0) as usize).unwrap_or(0);
if caller_count == 0 {
return String::new(); }
format!("c:{}→{}", first_id, caller_count)
}
fn sift_test_output(raw: &str, program: &str, exit_code: i32) -> String {
if exit_code == 0 {
let passed_count = if program == "pytest" {
raw.lines().rev().take(3).find_map(|l| {
let parts: Vec<&str> = l.split_whitespace().collect();
for (i, p) in parts.iter().enumerate() {
if *p == "passed" && i > 0 {
return parts.get(i - 1).and_then(|n| n.parse::<usize>().ok());
}
}
None
}).unwrap_or(0)
} else {
raw.lines().find_map(|l| {
if l.contains("test result: ok") {
l.split_whitespace().nth(4).and_then(|n| n.parse::<usize>().ok())
} else { None }
}).unwrap_or(0)
};
if passed_count > 0 {
return format!("[reliary: {} tests passed]\n", passed_count);
}
}
let compressed = reliary_output::compress_unified(raw);
let diagnosis = diagnose_failure(raw, program);
if !diagnosis.is_empty() {
format!("{}\n[reliary: {}]\n", compressed, diagnosis)
} else {
compressed
}
}
fn diagnose_failure(raw: &str, _program: &str) -> String {
let cwd = std::env::current_dir().ok(); let cwd_str = cwd.as_ref().map(|p| p.to_string_lossy().to_string()).unwrap_or_default();
let db = match find_open_index(&cwd_str) {
Some(d) => d,
None => return String::new(),
};
let import_name = extract_import_error(raw);
if let Some(name) = import_name {
let mut locations: Vec<String> = Vec::new();
if let Ok(mut stmt) = db.prepare(
"SELECT DISTINCT f.file_path FROM file_phrases fp JOIN phrases p ON fp.phrase_id = p.id JOIN file_map f ON fp.file_id = f.id WHERE p.phrase = ?1 LIMIT 5"
) {
if let Ok(rows) = stmt.query_map([&name], |row| row.get::<_, String>(0)) {
for r in rows.flatten() {
locations.push(r);
}
}
}
if !locations.is_empty() {
let mut rename_hint = String::new();
if let Ok(mut rename_stmt) = db.prepare(
"SELECT DISTINCT p.phrase FROM file_phrases fp JOIN phrases p ON fp.phrase_id = p.id JOIN file_map f ON fp.file_id = f.id WHERE f.file_path = ?1 AND length(p.phrase) >= ?2 AND length(p.phrase) <= ?3 AND p.phrase != ?4 LIMIT 10"
) {
for file in locations.iter().take(2) {
let nlen = name.len() as i64;
if let Ok(rows) = rename_stmt.query_map(
rusqlite::params![file, nlen - 3, nlen + 3, &name],
|row| row.get::<_, String>(0),
) {
let alts: Vec<String> = rows.flatten().collect();
let best = alts.iter().max_by_key(|a| {
a.chars().zip(name.chars()).take_while(|(x, y)| x == y).count()
});
if let Some(alt) = best {
let lcp = alt.chars().zip(name.chars()).take_while(|(x, y)| x == y).count();
if lcp >= 5 && *alt != name {
rename_hint = format!(" (rename of '{}'?)", alt);
break;
}
}
}
}
}
return format!("missing '{}' → referenced in: {}{}", name, locations.iter().take(3).cloned().collect::<Vec<_>>().join(", "), rename_hint);
} else {
return format!("missing '{}' → not in index", name);
}
}
if let Some(name) = extract_name_error(raw) {
let file: Option<String> = db.query_row(
"SELECT f.file_path FROM phrases p JOIN file_phrases fp ON p.id = fp.phrase_id JOIN file_map f ON fp.file_id = f.id WHERE p.phrase = ?1 LIMIT 1",
rusqlite::params![&name],
|row| row.get::<_, String>(0),
)
.inspect_err(|e| eprintln!("[diagnose] name lookup failed: {}", e))
.ok(); if let Some(file) = file {
return format!("undefined '{}' → defined in: {}", name, file);
}
}
String::new()
}
fn extract_import_error(raw: &str) -> Option<String> {
for line in raw.lines() {
if line.contains("ImportError") && line.contains("cannot import name") {
if let Some(start) = line.find("'") {
if let Some(end) = line[start + 1..].find("'") {
return Some(line[start + 1..start + 1 + end].to_string());
}
}
}
if line.contains("unresolved import") {
if let Some(start) = line.rfind('`') {
if let Some(end) = line[..start].rfind('`') {
let path = &line[end + 1..start];
if let Some(last) = path.split("::").last() {
return Some(last.to_string());
}
}
}
}
}
None
}
fn extract_name_error(raw: &str) -> Option<String> {
for line in raw.lines() {
if line.contains("NameError") && line.contains("not defined") {
if let Some(start) = line.find("'") {
if let Some(end) = line[start + 1..].find("'") {
return Some(line[start + 1..start + 1 + end].to_string());
}
}
}
}
None
}
fn validate_config(workdir: &str) {
let path = config::project_config_path(workdir);
if !path.exists() { return; }
if let Ok(content) = reliary_core::safe_read(path.to_string_lossy().as_ref()) {
if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&content) {
if let Some(obj) = parsed.as_object() {
let valid_features: Vec<&str> = config::FEATURE_DEFAULTS.iter().map(|(k, _)| *k).collect();
for key in obj.keys() {
let ok = matches!(key.as_str(), "mode" | "features" | "apiMode" | "privacyMode" | "apiBaseUrl")
|| key.strip_prefix("features.").map(|f| valid_features.contains(&f)).unwrap_or(false);
if !ok {
eprintln!("{} Unknown config key '{}' in {}", color::yellow("⚠"), key, path.display());
}
}
if let Some(mode) = obj.get("mode") {
if let Some(s) = mode.as_str() {
if !matches!(s, "fast" | "reactive" | "strict") {
eprintln!("{} Invalid mode '{}' — expected fast, reactive, or strict", color::yellow("⚠"), s);
}
}
}
if let Some(features) = obj.get("features") {
if let Some(fobj) = features.as_object() {
for (k, v) in fobj {
if !valid_features.contains(&k.as_str()) {
eprintln!("{} Unknown feature '{}' in {}", color::yellow("⚠"), k, path.display());
}
if !v.is_boolean() {
eprintln!("{} Feature '{}' should be boolean, got {}", color::yellow("⚠"), k, v);
}
}
}
}
}
} else {
eprintln!("{} Config file is not valid JSON: {}", color::yellow("⚠"), path.display());
}
}
}
fn do_trust(path: &str) {
let reliary_dir = std::path::PathBuf::from(path).join(".reliary");
if reliary_dir.exists() {
println!("{} .reliary/ already exists in {}", color::green("✓"), path);
} else {
if let Err(e) = std::fs::create_dir_all(&reliary_dir) {
eprintln!("{} Failed to create .reliary/ in {}: {}", color::red("✗"), path, e);
std::process::exit(1);
}
println!("{} Created .reliary/ in {}", color::green("✓"), path);
}
crate::paths::ensure_reliary_ignored(&reliary_dir);
if !run_index(path) {
eprintln!("{} Failed to trust {}: the index could not be built", color::red("✗"), path);
std::process::exit(1);
}
if std::env::var("RELIARY_EAGER_INDEX").map(|v| v == "1").unwrap_or(false) {
run_build_all(path);
}
validate_config(path);
println!("{} Project trusted: {}", color::green("✓"), path);
}
fn sha256_file(path: &std::path::Path) -> std::io::Result<String> {
use sha2::{Digest, Sha256};
let mut file = std::fs::File::open(path)?;
let mut hasher = Sha256::new();
std::io::copy(&mut file, &mut hasher)?;
Ok(format!("{:x}", hasher.finalize()))
}
const RELEASES_URL: &str = "https://api.github.com/repos/Reliary/reliary-agent/releases/latest";
fn release_asset_platform(os: &str, arch: &str) -> Option<(&'static str, &'static str, &'static str)> {
match (os, arch) {
("linux", "x86_64") => Some(("linux", "x86_64", "tar.gz")),
("linux", "aarch64") => Some(("linux", "aarch64", "tar.gz")),
("macos", "x86_64") => Some(("darwin", "x86_64", "tar.gz")),
("macos", "aarch64") => Some(("darwin", "aarch64", "tar.gz")),
("windows", "x86_64") => Some(("windows", "x86_64", "zip")),
("windows", "aarch64") => Some(("windows", "aarch64", "zip")),
_ => None,
}
}
fn release_asset_name(tag: &str, os: &str, arch: &str, ext: &str) -> String {
format!("reliary-agent-{}-{}-{}.{}", tag, os, arch, ext)
}
#[cfg(windows)]
fn schedule_delete(path: &std::path::Path) {
use std::os::windows::process::CommandExt;
const DETACHED_PROCESS: u32 = 0x0000_0008;
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
let Ok(exe) = std::env::current_exe() else { return };
let _ = std::process::Command::new(exe)
.arg("--internal-unlink")
.arg(path)
.creation_flags(DETACHED_PROCESS | CREATE_NO_WINDOW)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn();
}
#[cfg(not(windows))]
#[allow(dead_code)]
fn schedule_delete(path: &std::path::Path) {
let _ = std::fs::remove_file(path);
}
fn args_after_flag(flag: &str) -> Option<Vec<String>> {
let mut args = std::env::args();
while let Some(a) = args.next() {
if a == flag {
return Some(args.collect());
}
}
None
}
fn unlink_when_unlocked(path: &std::path::Path) -> ! {
let allowed = path
.file_name()
.and_then(|n| n.to_str())
.map(|n| n.ends_with(".old") || n.ends_with(".exe.old"))
.unwrap_or(false);
if !allowed {
eprintln!("[internal-unlink] refusing to delete {} (only self-update .old files)", path.display());
std::process::exit(2);
}
for _ in 0..300 {
match std::fs::remove_file(path) {
Ok(()) => break,
Err(_) => std::thread::sleep(std::time::Duration::from_millis(100)),
}
}
std::process::exit(0);
}
fn do_update(check_only: bool) {
println!("{} Checking for updates...", color::bold(""));
let current = VERSION;
let client = match reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
{
Ok(c) => c,
Err(e) => { eprintln!("{} Could not build HTTP client: {}", color::red("✗"), e); std::process::exit(1); }
};
let release: serde_json::Value = match client
.get(RELEASES_URL)
.header("User-Agent", "reliary")
.send()
{
Ok(resp) => match resp.text() {
Ok(body) => match serde_json::from_str(&body) {
Ok(v) => v,
Err(e) => {
eprintln!("{} Could not parse GitHub response: {}", color::red("✗"), e);
std::process::exit(1);
}
},
Err(e) => {
eprintln!("{} Could not read GitHub response: {}", color::red("✗"), e);
std::process::exit(1);
}
},
Err(e) => {
eprintln!("{} Could not check for updates: {}", color::red("✗"), e);
eprintln!(" Install manually from: https://github.com/Reliary/reliary-agent/releases");
std::process::exit(1);
}
};
let tag = release.get("tag_name").and_then(|v| v.as_str()).unwrap_or("unknown");
let latest = tag.trim_start_matches('v');
if latest == current {
println!("{} Already up to date (v{})", color::green("✓"), current);
return;
}
println!("{} Update available: v{} → v{}", color::yellow("!"), current, latest);
let installs = ux::find_installs();
let mut seen_methods = std::collections::HashSet::new();
for inst in &installs {
if seen_methods.insert(inst.method) {
match inst.method {
"cargo" => println!(" cargo: cargo install reliary-agent --force"),
"brew" => println!(" brew: brew upgrade Reliary/homebrew-tap/reliary-agent"),
"npm" => println!(" npm: npm update -g @reliary/agent"),
_ => {}
}
}
}
if check_only {
println!(" Run 'reliary update' to install");
std::process::exit(2);
}
let Some((os_name, arch_name, archive_ext)) =
release_asset_platform(std::env::consts::OS, std::env::consts::ARCH)
else {
eprintln!("{} No prebuilt binary for {}-{}", color::red("✗"),
std::env::consts::OS, std::env::consts::ARCH);
eprintln!(" Build from source: cargo install reliary-agent");
std::process::exit(1);
};
let asset_name = release_asset_name(tag, os_name, arch_name, archive_ext);
let download_url = format!(
"https://github.com/Reliary/reliary-agent/releases/download/{}/{}",
tag, asset_name
);
let expected_digest = release
.get("assets")
.and_then(|a| a.as_array())
.and_then(|assets| assets.iter().find(|a| a.get("name").and_then(|n| n.as_str()) == Some(asset_name.as_str())))
.and_then(|a| a.get("digest"))
.and_then(|d| d.as_str())
.and_then(|d| d.strip_prefix("sha256:"))
.map(|s| s.to_ascii_lowercase());
let Some(expected_digest) = expected_digest else {
eprintln!("{} Release {} has no {} with a published digest", color::red("✗"), tag, asset_name);
eprintln!(" Install manually from: https://github.com/Reliary/reliary-agent/releases");
std::process::exit(1);
};
let staging = match tempfile::Builder::new().prefix("reliary-update-").tempdir() {
Ok(d) => d,
Err(e) => {
eprintln!("{} Could not create staging dir: {}", color::red("✗"), e);
std::process::exit(1);
}
};
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(staging.path(), std::fs::Permissions::from_mode(0o700));
}
let tmp_dir = staging.keep();
let cleanup = || { let _ = std::fs::remove_dir_all(&tmp_dir); };
println!(" Downloading {}...", asset_name);
let archive = tmp_dir.join(&asset_name);
let dl_ok: Result<(), String> = client
.get(&download_url)
.header("User-Agent", "reliary")
.send()
.map_err(|e| e.to_string())
.and_then(|resp| {
let resp = resp.error_for_status().map_err(|e| e.to_string())?;
let mut out = std::fs::File::create(&archive).map_err(|e| e.to_string())?;
std::io::copy(&mut { resp }, &mut out).map_err(|e| e.to_string())?;
Ok(())
});
if let Err(e) = dl_ok {
eprintln!("{} Download failed: {}", color::red("✗"), e);
cleanup();
std::process::exit(1);
}
match sha256_file(&archive) {
Ok(actual) if actual.eq_ignore_ascii_case(&expected_digest) => {}
Ok(actual) => {
eprintln!("{} Checksum mismatch — refusing to install", color::red("✗"));
eprintln!(" expected sha256:{}", expected_digest);
eprintln!(" actual sha256:{}", actual);
cleanup();
std::process::exit(1);
}
Err(e) => {
eprintln!("{} Could not hash download: {}", color::red("✗"), e);
cleanup();
std::process::exit(1);
}
}
println!(" {} checksum verified", color::green("✓"));
let extract_ok = if archive_ext == "zip" {
#[cfg(windows)]
{
std::process::Command::new("powershell")
.args([
"-NoProfile", "-NonInteractive", "-Command",
&format!(
"Expand-Archive -LiteralPath '{}' -DestinationPath '{}' -Force",
archive.display(),
tmp_dir.display()
),
])
.status()
.is_ok_and(|s| s.success())
}
#[cfg(not(windows))]
{
false
}
} else {
std::process::Command::new("tar")
.args(["--no-absolute-filenames", "--no-same-owner", "-xzf", &archive.to_string_lossy(), "-C", &tmp_dir.to_string_lossy()])
.status()
.is_ok_and(|s| s.success())
};
if !extract_ok {
eprintln!("{} Extract failed", color::red("✗"));
cleanup();
std::process::exit(1);
}
let extracted_name = format!(
"{}{}",
asset_name.trim_end_matches(&format!(".{}", archive_ext)),
if archive_ext == "zip" { ".exe" } else { "" }
);
let extracted_bin = tmp_dir.join(&extracted_name);
if !extracted_bin.is_file() {
eprintln!("{} Unexpected archive layout (no {})", color::red("✗"), extracted_bin.display());
cleanup();
std::process::exit(1);
}
let binary = std::env::current_exe().unwrap_or_default();
let tmp_bin = std::path::PathBuf::from(format!("{}.new", binary.display()));
let copy_ok = std::fs::copy(&extracted_bin, &tmp_bin).is_ok();
#[cfg(unix)]
if copy_ok {
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(&tmp_bin, std::fs::Permissions::from_mode(0o755));
}
let install_ok = if !copy_ok {
false
} else {
#[cfg(windows)]
{
let old = std::path::PathBuf::from(format!("{}.old", binary.display()));
let _ = std::fs::remove_file(&old);
if std::fs::rename(&binary, &old).is_ok() {
if std::fs::rename(&tmp_bin, &binary).is_ok() {
schedule_delete(&old);
true
} else {
let _ = std::fs::rename(&old, &binary);
false
}
} else {
false
}
}
#[cfg(not(windows))]
{
std::fs::rename(&tmp_bin, &binary).is_ok()
}
};
if install_ok {
println!("{} Updated to v{}", color::green("✓"), latest);
} else {
eprintln!("{} Install failed — try manually:", color::red("✗"));
eprintln!(" cargo install reliary-agent --force");
cleanup();
std::process::exit(1);
}
cleanup();
}
fn main() {
log::init();
if let Some(rest) = args_after_flag("--internal-unlink") {
match rest.first() {
Some(p) if !p.is_empty() => unlink_when_unlocked(&std::path::PathBuf::from(p)),
_ => std::process::exit(0),
}
}
let default_threads = if cfg!(target_arch = "aarch64") {
4
} else {
std::thread::available_parallelism().map(|n| n.get().min(8)).unwrap_or(4)
};
let threads: usize = std::env::var("RELIARY_RAYON_THREADS")
.ok().and_then(|v| v.parse().ok()).unwrap_or(default_threads);
let _ = rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.build_global();
let cli = Cli::parse();
let fmt = format_config(&cli.format);
let cfg = reliary_core::FormatConfig::new(fmt);
if cli.yes {
std::env::set_var("RELIARY_YES", "1");
}
match &cli.command {
Commands::Config { .. } | Commands::Init { dry_run: _ } | Commands::Doctor { .. }
| Commands::Status { .. } | Commands::Completions { .. } | Commands::Man { outdir: _ } => {}
_ => validate_config("."),
}
match &cli.command {
Commands::Search { query, path } => {
let Some(db) = open_index_or_prompt(path) else {
eprintln!("{} No index — run `reliary trust {}` to build it.", color::red("✗"), path);
std::process::exit(2);
};
let results = reliary_search::search::search_fts5(&db, query, 10);
let lines: Vec<String> = results.iter()
.map(|r| format!("{:.4} {}", r.score, r.file))
.collect();
if matches!(fmt, reliary_core::OutputFormat::Json) {
println!("{}", cfg.format_output("search results", &lines));
} else if results.is_empty() {
println!("No results found.");
} else {
let output = cfg.format_output("search results", &lines);
pipe_to_pager(&output);
}
}
Commands::Index { path } => {
if !run_index(path) {
std::process::exit(1);
}
}
Commands::ReindexFile { file } => {
run_reindex_file(file);
}
Commands::Vacuum { path } => {
run_vacuum(path);
}
Commands::BuildOccurrences { path } => {
run_build_occurrences(path);
}
Commands::BuildAll { path } => {
run_build_all(path);
}
Commands::WhoCalls { file, identifier } => {
run_who_calls(file, identifier);
}
Commands::Compress { text, gentle } => {
let input_buf: String = match text {
Some(ref t) if !t.is_empty() && t != "---stdin---" => t.clone(),
_ => {
match reliary_core::safe_read_stdin() {
Ok(buf) => buf,
Err(e) => { eprintln!("{} stdin: {}", color::red("✗"), e); std::process::exit(1); }
}
}
};
let input: &str = &input_buf;
if !input.is_empty() {
let result = reliary_compress::compress_reasoning_mode(input, None, *gentle);
if let Some(compressed) = result {
println!("{}", cfg.format_output("compressed", &[compressed]));
} else {
println!("no compression possible");
}
}
}
Commands::Risk { file } => {
let _ = open_index_or_prompt(".");
let content = match reliary_core::safe_read(file) {
Ok(c) => c,
Err(e) => { eprintln!("{} {}", color::red("✗"), e); std::process::exit(1); }
};
let risk_result = reliary_risk::compute_file_risk(file, &content);
let risk_fmt = match fmt { reliary_core::OutputFormat::Json => "json", _ => "default" };
ux::format_risk(file, &format!("{:?}", risk_result), risk_fmt);
}
Commands::Pack { path, format, strategy, top_k, auto, slice_query } => {
if let Some(query) = slice_query {
let pack_format = if format == "full" {
reliary_pack::PackFormat::Full
} else {
reliary_pack::PackFormat::L2L3
};
match reliary_pack::generate_pack(path, pack_format) {
Ok(full_pack) => {
let sliced = reliary_pack::slice_pack_for_query(&full_pack, query, *top_k);
if sliced.is_empty() {
eprintln!("{} no matching entries for query",
color::yellow("⊙"));
} else {
print!("{}", sliced);
}
}
Err(e) => { eprintln!("{} {}", color::red("✗"), e); std::process::exit(1); }
}
} else if *auto {
match reliary_pack::should_inject_pack(path) {
Ok(reliary_pack::GateDecision::Skip) => {
eprintln!("{} pack skipped (codebase too simple or famous)",
color::yellow("⊙"));
}
Ok(reliary_pack::GateDecision::Minimal) => {
eprintln!("{} minimal pack (low-complexity codebase, top-15 hotspot)",
color::yellow("ℹ"));
match reliary_pack::generate_pack_hotspot(path, reliary_pack::PackFormat::L2L3, 15) {
Ok(pack) => { print!("{}", pack); }
Err(e) => { eprintln!("{} {}", color::red("✗"), e); std::process::exit(1); }
}
}
Ok(reliary_pack::GateDecision::Full) => {
eprintln!("{} full pack (high-complexity codebase, top-{} hotspot)",
color::green("✓"), top_k);
match reliary_pack::generate_pack_hotspot(path, reliary_pack::PackFormat::L2L3, *top_k) {
Ok(pack) => { print!("{}", pack); }
Err(e) => { eprintln!("{} {}", color::red("✗"), e); std::process::exit(1); }
}
}
Err(e) => { eprintln!("{} {}", color::red("✗"), e); std::process::exit(1); }
}
} else if strategy == "hotspot" {
match reliary_pack::generate_pack_hotspot(path, reliary_pack::PackFormat::L2L3, *top_k) {
Ok(pack) => { print!("{}", pack); }
Err(e) => { eprintln!("{} {}", color::red("✗"), e); std::process::exit(1); }
}
} else {
let pack_format = if format == "full" {
reliary_pack::PackFormat::Full
} else {
reliary_pack::PackFormat::L2L3
};
match reliary_pack::generate_pack(path, pack_format) {
Ok(pack) => { print!("{}", pack); }
Err(e) => { eprintln!("{} {}", color::red("✗"), e); std::process::exit(1); }
}
}
}
Commands::Doctor { fix } => {
ux::doctor(*fix, match fmt { reliary_core::OutputFormat::Json => "json", _ => "default" });
}
Commands::Status { path } => {
ux::status_at(path, match fmt { reliary_core::OutputFormat::Json => "json", _ => "default" });
}
Commands::Clean { global, all } => {
let assume_yes = cli.yes || std::env::var("RELIARY_YES").is_ok();
let tty = std::io::IsTerminal::is_terminal(&std::io::stdin());
if !*global && !*all && !assume_yes {
if !tty {
eprintln!("{} Refusing to wipe project state without a TTY. Re-run with `--yes`.", color::red("✗"));
std::process::exit(2);
}
eprint!("{} Wipe all project state (.reliary)? [y/N] ", color::yellow("⚠"));
std::io::stdout().flush().ok(); let mut input = String::new();
std::io::stdin().read_line(&mut input).ok(); if input.trim().to_lowercase() != "y" {
println!("{} Cancelled.", color::dim("-"));
return;
}
}
if *all && !assume_yes {
if !tty {
eprintln!("{} Refusing to wipe ALL state without a TTY. Re-run with `--yes`.", color::red("✗"));
std::process::exit(2);
}
eprint!("{} Wipe ALL state (project + global ~/.reliary)? [y/N] ", color::yellow("⚠"));
std::io::stdout().flush().ok(); let mut input = String::new();
std::io::stdin().read_line(&mut input).ok(); if input.trim().to_lowercase() != "y" {
println!("{} Cancelled.", color::dim("-"));
return;
}
}
ux::clean(*global, *all);
}
Commands::Logs { tail, level } => {
ux::logs(*tail, level.clone());
}
Commands::Sift { stdin, llm, aggressive, command } => {
if *llm { std::env::set_var("RELIARY_SIFT_LLM", "1"); }
if *aggressive { std::env::set_var("RELIARY_SIFT_AGGRESSIVE", "1"); }
exec_sift(command, *stdin);
}
Commands::Wrap { command } => {
exec_wrap(command);
}
Commands::CacheStore { text } => {
let input = match text {
Some(ref t) if !t.is_empty() && t != "---stdin---" => t.clone(),
_ => {
match reliary_core::safe_read_stdin() {
Ok(buf) => buf,
Err(e) => { eprintln!("{} stdin: {}", color::red("✗"), e); return; }
}
}
};
let path = std::path::Path::new(".reliary/cache.sqlite");
std::fs::create_dir_all(".reliary").ok(); crate::paths::ensure_reliary_ignored(std::path::Path::new(".reliary"));
match reliary_core::open(path).and_then(|conn| reliary_core::store(&conn, &input)) {
Ok(hash) => {
let _ = reliary_core::evict(&open_or_create(path).unwrap_or_else(|_| dummy_conn()),
reliary_core::default_ttl(), reliary_core::default_max_entries());
println!("{}", hash);
}
Err(e) => { eprintln!("{} cache-store: {}", color::red("✗"), e); std::process::exit(1); }
}
}
Commands::CacheRetrieve { hash } => {
let path = std::path::Path::new(".reliary/cache.sqlite");
match open_or_create(path) {
Ok(conn) => {
match reliary_core::retrieve(&conn, hash) {
Ok(Some(content)) => print!("{}", content),
Ok(None) => match crate::tee::read_tee(hash) {
Ok(content) => print!("{}", content),
Err(_) => { eprintln!("{} not found: {}", color::yellow("⚠"), hash); std::process::exit(1); }
},
Err(e) => { eprintln!("{} cache-retrieve: {}", color::red("✗"), e); std::process::exit(1); }
}
}
Err(_) => match crate::tee::read_tee(hash) {
Ok(content) => print!("{}", content),
Err(_) => { eprintln!("{} not found: {}", color::yellow("⚠"), hash); std::process::exit(1); }
},
}
}
Commands::CacheStats => {
let path = std::path::Path::new(".reliary/cache.sqlite");
match open_or_create(path) {
Ok(conn) => {
match reliary_core::stats(&conn) {
Ok((count, bytes)) => println!("entries: {}\nbytes: {}", count, bytes),
Err(e) => { eprintln!("{} cache-stats: {}", color::red("✗"), e); std::process::exit(1); }
}
}
Err(_) => println!("entries: 0\nbytes: 0"),
}
}
Commands::Config { key, value, local, root } => {
match (key, value) {
(Some(k), Some(v)) => {
if !config::VALID_CONFIG_KEYS.contains(&k.as_str()) {
eprintln!("{} Unknown config key '{}'. Valid keys:", color::yellow("⚠"), k);
for vk in config::VALID_CONFIG_KEYS {
eprintln!(" {}", vk);
}
std::process::exit(1);
}
if k == "mode" && !matches!(v.as_str(), "fast" | "reactive" | "strict") {
eprintln!("{} Invalid mode '{}' — expected fast, reactive, or strict", color::yellow("⚠"), v);
std::process::exit(1);
}
let root_str = root.as_deref();
println!("{}", config::set_config(k, v, *local, root_str));
}
(None, None) => {
let wd = root.as_deref().or(Some("."));
let resolved_mode = config::resolve_mode_with_source(wd);
let resolved_features = config::resolve_features_with_source(wd);
if fmt == reliary_core::OutputFormat::Json {
let mut map = serde_json::Map::new();
map.insert("mode".into(), serde_json::Value::String(resolved_mode.value.as_str().into()));
map.insert("mode_source".into(), serde_json::Value::String(resolved_mode.source.as_str().into()));
let features_obj: Vec<serde_json::Value> = resolved_features.iter().map(|f| {
serde_json::json!({"name": f.name, "enabled": f.enabled, "source": f.source.as_str()})
}).collect();
map.insert("features".into(), serde_json::Value::Array(features_obj));
map.insert("global_config".into(), serde_json::Value::String(config::global_config_path().to_string_lossy().into()));
if let Some(r) = root {
map.insert("project_config".into(), serde_json::Value::String(config::project_config_path(r).to_string_lossy().into()));
}
println!("{}", serde_json::to_string_pretty(&map).unwrap());
} else {
println!("\x1b[1m| Current Config |\x1b[0m");
println!(" \x1b[1mgate mode:\x1b[0m {} \x1b[2m(from: {})\x1b[0m", resolved_mode.value.as_str(), resolved_mode.source.as_str());
let global = config::global_config_path();
println!(" \x1b[2mGlobal:\x1b[0m {}", global.display());
if let Some(r) = root {
let local_path = config::project_config_path(r);
println!(" \x1b[2mLocal: \x1b[0m {}", local_path.display());
}
println!(" \x1b[1mfeatures:\x1b[0m");
for f in &resolved_features {
let icon = if f.enabled { "\x1b[32m+\x1b[0m" } else { "\x1b[2m-\x1b[0m" };
println!(" {} {} \x1b[2m({})\x1b[0m", icon, f.name, f.source.as_str());
}
}
}
_ => {
eprintln!("Usage: reliary config [key] [value]");
eprintln!(" reliary-agent config (show current)");
eprintln!(" reliary-agent config --local mode strict");
}
}
}
Commands::Init { dry_run } => {
init::run(*dry_run);
}
Commands::Uninstall => {
init::uninstall();
}
Commands::Dead { path } => {
let dead_fmt = match fmt { reliary_core::OutputFormat::Json => "json", _ => "default" };
let config = reliary_dead::DeadConfig::default();
let (candidates, entries) = crate::ux::with_spinner("scanning for dead code", || {
let mut files = Vec::new();
let path_buf = std::path::PathBuf::from(path);
let cwd = std::env::current_dir().unwrap_or_default();
if path_buf.is_dir() {
const SKIP_DIRS: &[&str] = &[
"target", "node_modules", "dist", "build", "vendor", ".next",
".cache", "out", "__pycache__", ".venv", "venv", ".gradle",
];
for entry in walkdir::WalkDir::new(&path_buf)
.into_iter()
.filter_entry(|e| {
let name = e.file_name().to_string_lossy();
if e.file_type().is_dir() {
if name.starts_with('.') { return false; }
if SKIP_DIRS.contains(&name.as_ref()) { return false; }
}
true
})
.filter_map(|e| e.ok())
{
let p = entry.path();
if p.is_file() {
if !reliary_search::is_likely_binary(p, 8192) {
if let Ok(content) = reliary_core::safe_read(p.to_string_lossy().as_ref()) {
let display = p.strip_prefix(&cwd).unwrap_or(p);
files.push((display.to_string_lossy().to_string(), content));
}
}
}
}
} else if path_buf.is_file() {
if let Ok(content) = reliary_core::safe_read(path_buf.to_string_lossy().as_ref()) {
let display = path_buf.strip_prefix(&cwd).unwrap_or(&path_buf);
files.push((display.to_string_lossy().to_string(), content));
}
}
let candidates = reliary_dead::analyze_files(&files, &config);
let entries: Vec<String> = candidates.iter().map(|c| {
let conf = match c.confidence { reliary_dead::Confidence::High => "HIGH", reliary_dead::Confidence::Medium => "MED", reliary_dead::Confidence::Low => "LOW" };
format!("{}:{} [{}] {}", c.file, c.line, conf, c.reason)
}).collect();
(candidates, entries)
});
drop(candidates);
ux::format_dead(path, &entries, dead_fmt);
}
Commands::Trust { path } => {
do_trust(path);
}
Commands::Update { check } => {
do_update(*check);
}
Commands::Bench { action, index, out, gt, seed, tol, path } => {
let index_path = index.clone().unwrap_or_else(|| cli_index_db_path_or_literal(path));
match action {
BenchAction::Gen => {
if !std::path::Path::new(&index_path).exists() {
eprintln!("cannot open index at {} → run `reliary trust {}` to build it", index_path, path);
std::process::exit(2);
}
let doc = bench::generate(&index_path, *seed);
let text = serde_json::to_string_pretty(&doc).unwrap_or_default();
match out {
Some(o) => {
if let Err(e) = std::fs::write(o, &text) {
eprintln!("cannot write {}: {}", o, e);
std::process::exit(1);
}
let n = doc.get("questions").and_then(|v| v.as_array()).map(|a| a.len()).unwrap_or(0);
eprintln!("wrote {} ({} questions)", o, n);
}
None => println!("{}", text),
}
}
BenchAction::Verify => {
let Some(results) = out else {
eprintln!("bench verify requires --out <results.jsonl>");
std::process::exit(2);
};
let Some(gt_file) = gt else {
eprintln!("bench verify requires --gt <questions.json>");
std::process::exit(2);
};
let doc = bench::score_results(results, gt_file, *tol);
println!("{}", serde_json::to_string_pretty(&doc).unwrap_or_default());
}
}
}
Commands::Warrant { base, head, contract, cert, strict_unseen, no_cache } => {
let head_root = head.trim_end_matches('/').to_string();
let tmp = std::env::temp_dir().join(format!("reliary-warrant-{}", std::process::id()));
let _ = std::fs::create_dir_all(&tmp);
#[allow(unused_assignments)]
let mut exit_code: i32 = 0;
let mut written: Option<String> = None;
let timing = std::env::var("RELIARY_WARRANT_TIMING").is_ok();
let t_start = std::time::Instant::now();
let mut t_phase = std::time::Instant::now();
let phase = |name: &str, t_phase: &mut std::time::Instant| {
if timing {
eprintln!("[warrant] {name}: {:?}", t_phase.elapsed());
}
*t_phase = std::time::Instant::now();
};
'warrant: {
let contract_text = match std::fs::read_to_string(contract) {
Ok(t) => t,
Err(e) => {
eprintln!("cannot read contract {}: {}", contract, e);
exit_code = 2;
break 'warrant;
}
};
let mut parsed: reliary_search::warrant::Contract =
match serde_json::from_str(&contract_text) {
Ok(c) => c,
Err(e) => {
eprintln!("invalid contract {}: {}", contract, e);
exit_code = 2;
break 'warrant;
}
};
if *strict_unseen {
parsed.strict_unseen = true;
}
let base_dir = if std::path::Path::new(&base).is_dir() {
base.to_string()
} else {
let slot = tmp.join("base");
let out = std::process::Command::new("git")
.arg("-C").arg(&head_root)
.arg("worktree").arg("add").arg("--detach")
.arg("--").arg(&slot).arg(base)
.output();
match out {
Ok(o) if o.status.success() => slot.to_string_lossy().to_string(),
Ok(o) => {
eprintln!("cannot resolve base '{}': {}", base, String::from_utf8_lossy(&o.stderr).trim());
exit_code = 2;
break 'warrant;
}
Err(e) => {
eprintln!("cannot run git: {}", e);
exit_code = 2;
break 'warrant;
}
}
};
let head_db_path = tmp.join("head.sqlite");
let cache_enabled = !*no_cache
&& std::env::var("RELIARY_WARRANT_CACHE").map(|v| v != "0").unwrap_or(true);
let cache_dir = std::path::Path::new(&head_root).join(".reliary/warrant");
let mut exclude: Vec<std::path::PathBuf> = vec![
std::path::PathBuf::from(&contract),
];
if let Some(c) = cert {
exclude.push(std::path::PathBuf::from(c));
}
let mut ignored_rel: std::collections::BTreeSet<String> =
std::collections::BTreeSet::new();
if let Ok(out) = std::process::Command::new("git")
.arg("-C").arg(&head_root)
.arg("ls-files").arg("--others").arg("--ignored")
.arg("--directory")
.arg("--exclude-standard")
.output()
{
if out.status.success() {
for line in String::from_utf8_lossy(&out.stdout).lines() {
let rel = line.trim();
if !rel.is_empty() {
ignored_rel.insert(rel.to_string());
exclude.push(std::path::Path::new(&head_root).join(rel));
}
}
}
}
let t_manifest = std::time::Instant::now();
let head_identity = match reliary_search::warrant::manifest_digest(
std::path::Path::new(&head_root), &exclude) {
Ok(d) => d,
Err(e) => { eprintln!("scan head: {}", e); exit_code = 2; break 'warrant; }
};
phase("head-manifest", &mut t_phase);
let _ = t_manifest;
let base_identity = if base_dir != head_root.trim_end_matches('/') {
base_identity(base, &base_dir)
} else {
head_identity.clone()
};
let key = reliary_search::warrant::cache_key(&base_identity);
let index_defs = reliary_search::segment::segment_enabled();
let mut base_index_defs: Option<Vec<reliary_search::warrant::DefRow>> = None;
let mut head_index_defs: Option<Vec<reliary_search::warrant::DefRow>> = None;
if index_defs {
let base_db = if cache_enabled {
match reliary_search::warrant::open_or_build_cached_index(
std::path::Path::new(&base_dir), &cache_dir, &key, &ignored_rel) {
Ok((db, _hit)) => db,
Err(e) => { eprintln!("index base: {}", e); exit_code = 2; break 'warrant; }
}
} else {
match reliary_search::struct_diff::index_revision_relative(
&base_dir, tmp.join("base.sqlite").to_string_lossy().as_ref()) {
Ok(d) => d,
Err(e) => { eprintln!("index base: {}", e); exit_code = 2; break 'warrant; }
}
};
base_index_defs = match reliary_search::warrant::def_rows(
&base_db, std::path::Path::new(&base_dir)) {
Ok(d) => Some(d),
Err(e) => { eprintln!("defs base: {}", e); exit_code = 2; break 'warrant; }
};
phase("base-index", &mut t_phase);
let head_key = reliary_search::warrant::cache_key(&head_identity);
let (head_db, head_hit) = if cache_enabled {
match reliary_search::warrant::open_or_build_cached_index(
std::path::Path::new(&head_root), &cache_dir, &head_key, &ignored_rel) {
Ok((db, hit)) => (db, hit),
Err(e) => { eprintln!("index head: {}", e); exit_code = 2; break 'warrant; }
}
} else {
match reliary_search::struct_diff::index_revision_relative_excluding(
&head_root, head_db_path.to_string_lossy().as_ref(), &ignored_rel) {
Ok(d) => (d, false),
Err(e) => { eprintln!("index head: {}", e); exit_code = 2; break 'warrant; }
}
};
head_index_defs = match reliary_search::warrant::def_rows(
&head_db, std::path::Path::new(&head_root)) {
Ok(d) => Some(d),
Err(e) => { eprintln!("defs head: {}", e); exit_code = 2; break 'warrant; }
};
if timing {
eprintln!("[warrant] head-index-cache-hit: {head_hit}");
}
phase("head-index", &mut t_phase);
}
if timing {
eprintln!(
"[warrant] defs-source: {}",
if index_defs { "index (segmenter)" } else { "scan" }
);
}
let certificate = match reliary_search::warrant::evaluate(
reliary_search::warrant::WarrantInput {
base_root: std::path::Path::new(&base_dir),
head_root: std::path::Path::new(&head_root),
base_defs: base_index_defs.as_deref(),
head_defs: head_index_defs.as_deref(),
contract: &parsed,
exclude: &exclude,
cache_dir: if cache_enabled { Some(&cache_dir) } else { None },
base_identity: base_identity.clone(),
head_identity: head_identity.clone(),
}) {
Ok(c) => c,
Err(e) => { eprintln!("warrant: {}", e); exit_code = 2; break 'warrant; }
};
phase("evaluate", &mut t_phase);
let text = match reliary_search::warrant::canonical_json(&certificate) {
Ok(t) => t,
Err(e) => { eprintln!("certificate: {}", e); exit_code = 2; break 'warrant; }
};
written = Some(text.clone());
exit_code = match certificate.verdict {
reliary_search::warrant::Verdict::Closed => 0,
reliary_search::warrant::Verdict::Open => 1,
reliary_search::warrant::Verdict::Indeterminate => 3,
};
}
let _ = std::process::Command::new("git")
.arg("-C").arg(&head_root)
.arg("worktree").arg("remove").arg("--force")
.arg(tmp.join("base"))
.output();
let _ = std::fs::remove_file(tmp.join("base.sqlite"));
let _ = std::fs::remove_file(tmp.join("head.sqlite"));
let _ = std::fs::remove_file(tmp.join("base.sqlite-wal"));
let _ = std::fs::remove_file(tmp.join("head.sqlite-wal"));
let _ = std::fs::remove_file(tmp.join("base.sqlite-shm"));
let _ = std::fs::remove_file(tmp.join("head.sqlite-shm"));
let _ = std::fs::remove_dir(&tmp);
reliary_search::warrant::prune_cache(
&std::path::Path::new(&head_root).join(".reliary/warrant"),
4,
512 * 1024 * 1024,
);
if timing {
eprintln!("[warrant] total: {:?}", t_start.elapsed());
}
if let Some(text) = written {
match cert {
Some(path) => {
if let Err(e) = std::fs::write(path, &text) {
eprintln!("cannot write certificate {}: {}", path, e);
std::process::exit(2);
}
}
None => println!("{}", text),
}
}
if exit_code != 0 { std::process::exit(exit_code); }
}
Commands::Map { path, out, max_files, max_symbols } => {
let db_path = cli_index_db_path_or_literal(path);
if !std::path::Path::new(&db_path).exists() {
eprintln!("cannot open index at {} → run `reliary trust {}` to build it", db_path, path);
std::process::exit(2);
}
let db = match rusqlite::Connection::open(&db_path) {
Ok(d) => d,
Err(e) => { eprintln!("cannot open index: {}", e); std::process::exit(2); }
};
if let Err(e) = reliary_search::schema::open_existing_db_safe(&db) {
eprintln!("index at {} is not usable: {}", db_path, e);
std::process::exit(2);
}
let files = reliary_search::map::collect_map(&db, *max_files, *max_symbols);
let title = std::path::Path::new(path)
.canonicalize()
.ok()
.and_then(|p| p.file_name().map(|x| x.to_string_lossy().to_string()))
.unwrap_or_else(|| path.clone());
let svg = reliary_search::map::render_svg(&files, &title);
match out {
Some(o) => {
if let Err(e) = std::fs::write(o, &svg) {
eprintln!("cannot write {}: {}", o, e);
std::process::exit(1);
}
eprintln!("wrote {} ({} files, {} bytes)", o, files.len(), svg.len());
}
None => print!("{}", svg),
}
}
Commands::Diff { rev1, rev2, path, json } => {
let base = path.trim_end_matches('/').to_string();
let tmp = std::env::temp_dir().join(format!("reliary-diff-{}", std::process::id()));
let _ = std::fs::create_dir_all(&tmp);
let mut diff_exit: i32 = 0;
let db_a_path = tmp.join("a.sqlite");
let db_b_path = tmp.join("b.sqlite");
'diff: {
let resolve = |rev: &str, slot: &str| -> Result<String, String> {
let candidate = if std::path::Path::new(rev).is_absolute() {
rev.to_string()
} else {
format!("{}/{}", base, rev)
};
if std::path::Path::new(&candidate).is_dir() {
return Ok(candidate);
}
let slot_path = tmp.join(slot);
let out = std::process::Command::new("git")
.arg("-C").arg(&base)
.arg("worktree").arg("add").arg("--detach")
.arg("--")
.arg(&slot_path).arg(rev)
.output()
.map_err(|e| format!("git: {}", e))?;
if !out.status.success() {
return Err(format!(
"cannot resolve rev '{}': {}",
rev,
String::from_utf8_lossy(&out.stderr).trim()
));
}
Ok(slot_path.to_string_lossy().to_string())
};
let dir_a = match resolve(rev1, "a") {
Ok(d) => d,
Err(e) => { eprintln!("{}", e); diff_exit = 2; break 'diff; }
};
let dir_b = match resolve(rev2, "b") {
Ok(d) => d,
Err(e) => { eprintln!("{}", e); diff_exit = 2; break 'diff; }
};
let db_a = match reliary_search::struct_diff::index_revision(
&dir_a, db_a_path.to_string_lossy().as_ref()) {
Ok(d) => d,
Err(e) => { eprintln!("index {}: {}", rev1, e); diff_exit = 1; break 'diff; }
};
let db_b = match reliary_search::struct_diff::index_revision(
&dir_b, db_b_path.to_string_lossy().as_ref()) {
Ok(d) => d,
Err(e) => { eprintln!("index {}: {}", rev2, e); diff_exit = 1; break 'diff; }
};
let diff = reliary_search::struct_diff::compute_diff(&db_a, &db_b);
if *json {
let sym = |v: &Vec<(String, String, i32)>| -> Vec<serde_json::Value> {
v.iter().map(|(n, f, l)| serde_json::json!({"name": n, "file": f, "line": l})).collect()
};
let edge = |v: &Vec<(String, String)>| -> Vec<serde_json::Value> {
v.iter().map(|(a, b)| serde_json::json!({"caller": a, "callee": b})).collect()
};
println!("{}", serde_json::json!({
"symbols_added": sym(&diff.symbols_added),
"symbols_removed": sym(&diff.symbols_removed),
"edges_added": edge(&diff.edges_added),
"edges_removed": edge(&diff.edges_removed),
}));
} else {
println!("structural diff {}..{}", rev1, rev2);
println!(" symbols added: {}", diff.symbols_added.len());
for (n, f, l) in diff.symbols_added.iter().take(20) {
println!(" + {} ({}:{})", n, f, l);
}
println!(" symbols removed: {}", diff.symbols_removed.len());
for (n, f, l) in diff.symbols_removed.iter().take(20) {
println!(" - {} ({}:{})", n, f, l);
}
println!(" call edges added: {}", diff.edges_added.len());
for (a, b) in diff.edges_added.iter().take(10) {
println!(" + {} -> {}", a, b);
}
println!(" call edges removed: {}", diff.edges_removed.len());
for (a, b) in diff.edges_removed.iter().take(10) {
println!(" - {} -> {}", a, b);
}
}
}
for slot in ["a", "b"] {
let _ = std::process::Command::new("git")
.arg("-C").arg(&base)
.arg("worktree").arg("remove").arg("--force")
.arg(tmp.join(slot))
.output();
}
let _ = std::fs::remove_file(&db_a_path);
let _ = std::fs::remove_file(&db_b_path);
let _ = std::fs::remove_dir(&tmp);
if diff_exit != 0 { std::process::exit(diff_exit); }
}
Commands::TestPlan { files, symbols, path, diff, json } => {
let db_path = cli_index_db_path_or_literal(path);
if !std::path::Path::new(&db_path).exists() {
eprintln!("cannot open index at {} → run `reliary trust {}` to build it", db_path, path);
std::process::exit(2);
}
let db = match rusqlite::Connection::open(&db_path) {
Ok(d) => d,
Err(e) => { eprintln!("cannot open index: {}", e); std::process::exit(2); }
};
if let Err(e) = reliary_search::schema::open_existing_db_safe(&db) {
eprintln!("index at {} is not usable: {}", db_path, e);
std::process::exit(2);
}
let mut eff_files: Vec<String> = files.clone();
if let Some(range) = diff {
let out = std::process::Command::new("git")
.arg("-C").arg(path)
.arg("diff").arg("--name-only").arg(range)
.output();
match out {
Ok(o) if o.status.success() => {
for line in String::from_utf8_lossy(&o.stdout).lines() {
let l = line.trim();
if !l.is_empty() {
eff_files.push(l.to_string());
}
}
}
Ok(o) => {
eprintln!("git diff failed: {}", String::from_utf8_lossy(&o.stderr).trim());
std::process::exit(2);
}
Err(e) => {
eprintln!("cannot run git: {}", e);
std::process::exit(2);
}
}
}
match reliary_search::test_plan::compute_plan(&db, &eff_files, symbols, path) {
Ok(plan) => {
if *json {
let tests: Vec<serde_json::Value> = plan.tests.iter()
.map(|(f, r)| serde_json::json!({"file": f, "reasons": r}))
.collect();
let inline: Vec<serde_json::Value> = plan.inline.iter()
.map(|t| serde_json::json!({
"file": t.file, "line": t.line, "name": t.name}))
.collect();
println!("{}", serde_json::json!({
"tests": tests,
"commands": plan.commands,
"inline": inline,
"inline_skipped": plan.inline_skipped,
}));
} else if plan.tests.is_empty() && plan.inline.is_empty() {
println!("no tests found that exercise the given files/symbols");
for s in &plan.inline_skipped {
eprintln!("warning: {} was not read (not indexed, or not a source file)", s);
}
} else {
if !plan.inline.is_empty() {
println!("inline tests ({}):", plan.inline.len());
for t in &plan.inline {
println!(" {}::{} (line {})", t.file, t.name, t.line);
}
}
if !plan.tests.is_empty() {
println!("test files ({}):", plan.tests.len());
for (f, reasons) in &plan.tests {
println!(" {} [{}]", f, reasons.join(", "));
}
}
if !plan.commands.is_empty() {
println!("\nrun:");
for c in &plan.commands {
println!(" {}", c);
}
}
}
}
Err(e) => {
eprintln!("test-plan failed: {}", e);
std::process::exit(1);
}
}
}
Commands::Impact { name, path, json } => {
let db_path = cli_index_db_path_or_literal(path);
if !std::path::Path::new(&db_path).exists() {
eprintln!("cannot open index at {} → run `reliary trust {}` to build it", db_path, path);
std::process::exit(2);
}
let db = match rusqlite::Connection::open(&db_path) {
Ok(d) => d,
Err(e) => { eprintln!("cannot open index: {}", e); std::process::exit(2); }
};
if let Err(e) = reliary_search::schema::open_existing_db_safe(&db) {
eprintln!("index at {} is not usable: {}", db_path, e);
std::process::exit(2);
}
match reliary_search::impact::compute_impact(&db, name, path) {
Ok(imp) => {
if imp.def_file.is_empty() {
eprintln!("no definition found for \"{}\"", name);
std::process::exit(1);
}
if *json {
let callers: Vec<serde_json::Value> = imp.callers.iter()
.map(|(f, l)| serde_json::json!({"file": f, "line": l}))
.collect();
println!("{}", serde_json::json!({
"symbol": imp.symbol,
"def_file": imp.def_file,
"def_line": imp.def_line,
"callers": callers,
"caller_files": imp.caller_files,
"test_files": imp.test_files,
"risk": imp.risk.label(),
}));
} else {
let def_base = std::path::Path::new(&imp.def_file)
.file_name().map(|x| x.to_string_lossy().to_string())
.unwrap_or_else(|| imp.def_file.clone());
println!("{} is defined at {}:{}", imp.symbol, def_base, imp.def_line);
if imp.callers.is_empty() {
println!("callers: none found in the index (textual references only; may still be used via reflection, DI, or framework wiring — not proof of dead code)");
} else {
let caller_list: Vec<String> = imp.callers.iter().take(12)
.map(|(f, l)| {
let b = std::path::Path::new(f).file_name()
.map(|x| x.to_string_lossy().to_string())
.unwrap_or_else(|| f.clone());
format!("{}:{}", b, l)
})
.collect();
println!("callers ({}): {}", imp.callers.len(), caller_list.join(", "));
println!(" (textual references; a complete call graph would also include reflection/DI/framework wiring)");
}
if !imp.test_files.is_empty() {
let tests: Vec<String> = imp.test_files.iter()
.map(|f| std::path::Path::new(f).file_name()
.map(|x| x.to_string_lossy().to_string())
.unwrap_or_else(|| f.clone()))
.collect();
println!("tests touching it: {}", tests.join(", "));
}
println!("risk: {}", imp.risk.label());
}
}
Err(e) => {
eprintln!("impact failed: {}", e);
std::process::exit(1);
}
}
}
Commands::Verify { claim, path, tol, json } => {
let mut eff_path = path.clone();
let mut eff_tol = *tol;
let mut eff_json = *json;
let mut claim_parts: Vec<String> = Vec::new();
let mut it = claim.iter();
while let Some(t) = it.next() {
match t.as_str() {
"--path" | "-p" => { if let Some(v) = it.next() { eff_path = v.clone(); } }
"--tol" => { if let Some(v) = it.next() { eff_tol = v.parse().unwrap_or(1); } }
"--json" => { eff_json = true; }
_ => claim_parts.push(t.clone()),
}
}
let claim_str = claim_parts.join(" ");
let path = &eff_path;
let tol = &eff_tol;
let json = &eff_json;
let db_path = cli_index_db_path_or_literal(path);
if !std::path::Path::new(&db_path).exists() {
eprintln!("cannot open index at {} → run `reliary trust {}` to build it", db_path, path);
std::process::exit(2);
}
let db = match rusqlite::Connection::open(&db_path) {
Ok(d) => d,
Err(e) => { eprintln!("cannot open index: {}", e); std::process::exit(2); }
};
if let Err(e) = reliary_search::schema::open_existing_db_safe(&db) {
eprintln!("index at {} is not usable: {}", db_path, e);
std::process::exit(2);
}
ensure_occurrence_built(&db);
let summary = verify::verify_text(&db, &claim_str, *tol);
if *json {
let claims: Vec<serde_json::Value> = summary.claims.iter().map(|(c, v)| {
match v {
verify::Verdict::Verified { actual } => serde_json::json!({
"symbol": c.symbol, "file": c.file, "line": c.line,
"verdict": "verified",
"actual": actual.as_ref().map(|(f, l)| format!("{}:{}", f, l)),
}),
verify::Verdict::False { actual } => serde_json::json!({
"symbol": c.symbol, "file": c.file, "line": c.line,
"verdict": "false",
"actual": actual.as_ref().map(|(f, l)| format!("{}:{}", f, l)),
}),
verify::Verdict::UnverifiedStale { reason } => serde_json::json!({
"symbol": c.symbol, "file": c.file, "line": c.line,
"verdict": "unverified",
"reason": reason,
}),
}
}).collect();
println!("{}", serde_json::json!({
"verified": summary.verified,
"falsified": summary.falsified,
"unverified": summary.unverified,
"claims": claims,
}));
} else if summary.claims.is_empty() {
println!("NO CLAIMS — nothing verifiable found in the input.");
} else {
for (c, v) in &summary.claims {
let subject = if c.symbol.is_empty() {
format!("{}:{}", c.file, c.line)
} else {
format!("{} at {}:{}", c.symbol, c.file, c.line)
};
match v {
verify::Verdict::Verified { .. } => println!("VERIFIED {}", subject),
verify::Verdict::False { actual } => {
let actual_str = actual.as_ref()
.map(|(f, l)| format!(" → actual: {}:{}", f, l))
.unwrap_or_else(|| " → not found in source".to_string());
println!("FALSE {}{}", subject, actual_str);
}
verify::Verdict::UnverifiedStale { reason } => {
println!("UNVERIFIED {} → {}", subject, reason);
}
}
}
println!(
"\n{} verified, {} false, {} unverified",
summary.verified, summary.falsified, summary.unverified);
}
let exit = if summary.falsified > 0 {
1
} else if summary.unverified > 0 {
3
} else {
0
};
std::process::exit(exit);
}
Commands::Fix { task, path, max_iters, verify, dry_run, json } => {
let mut eff_path = path.clone();
let mut eff_dry = *dry_run;
let mut eff_json = *json;
let mut eff_iters = *max_iters;
let mut eff_verify: Option<String> = verify.clone();
let mut task_parts: Vec<String> = Vec::new();
let mut it = task.iter();
while let Some(t) = it.next() {
let (flag, inline) = match t.split_once('=') {
Some((f, v)) if f.starts_with("--") => (f, Some(v.to_string())),
_ => (t.as_str(), None),
};
match flag {
"--path" | "-p" => {
if let Some(v) = inline { eff_path = v; }
else if let Some(v) = it.next() { eff_path = v.clone(); }
}
"--max-iters" => {
let v = inline.or_else(|| it.next().cloned());
if let Some(v) = v { eff_iters = v.parse().unwrap_or(eff_iters); }
}
"--verify" => {
eff_verify = inline.or_else(|| it.next().cloned());
}
"--dry-run" => { eff_dry = true; }
"--json" => { eff_json = true; }
_ if !t.starts_with('-') => task_parts.push(t.clone()),
_ => {} }
}
let task_str = task_parts.join(" ");
match deterministic_fix::run_deterministic_opts(&eff_path, &task_str, eff_dry) {
Ok(code) => {
if eff_json {
println!("{{\"deterministic\":true,\"exit\":{},\"dry_run\":{}}}", code, eff_dry);
}
std::process::exit(code);
}
Err(msg) if msg.starts_with("no deterministic recipe") => {
eprintln!("[fix] {} — falling back to LLM agent", msg);
match fix_agent::run(&eff_path, &task_str, eff_iters, eff_dry, eff_json, eff_verify.as_deref()) {
Ok(_code) => {}
Err(e) => {
eprintln!("reliary fix failed: {}", e);
std::process::exit(1);
}
}
}
Err(e) => {
eprintln!("reliary fix failed: {}", e);
std::process::exit(1);
}
}
}
Commands::Completions { shell, outdir } => {
let mut cmd = build_cli();
let sh = match shell {
Shell::Bash => clap_complete::Shell::Bash,
Shell::Zsh => clap_complete::Shell::Zsh,
Shell::Fish => clap_complete::Shell::Fish,
Shell::PowerShell => clap_complete::Shell::PowerShell,
Shell::Elvish => clap_complete::Shell::Elvish,
};
let ext = match shell {
Shell::Bash => "bash",
Shell::Zsh => "zsh",
Shell::Fish => "fish",
Shell::PowerShell => "ps1",
Shell::Elvish => "elvish",
};
let mut buf = Vec::new();
generate(sh, &mut cmd, "reliary", &mut buf);
let output = String::from_utf8_lossy(&buf).to_string();
if let Some(dir) = outdir {
let path = std::path::Path::new(dir);
std::fs::create_dir_all(path).ok(); let file_path = path.join(format!("reliary.{}", ext));
if let Err(e) = reliary_core::atomic_write(file_path.to_string_lossy().as_ref(), &output) {
eprintln!("{} Failed to write completion file: {}", color::red("✗"), e);
std::process::exit(1);
}
println!("{} Generated {} completions → {}", color::green("✓"), ext, file_path.display());
} else {
print!("{}", output);
}
}
Commands::Man { outdir } => {
let cmd = build_cli();
let man = clap_mangen::Man::new(cmd);
if let Some(dir) = outdir {
let path = std::path::Path::new(dir);
std::fs::create_dir_all(path).ok(); let file_path = path.join("reliary.1");
let mut file = match std::fs::File::create(&file_path) {
Ok(f) => f,
Err(e) => {
eprintln!("{} Failed to create man page at {}: {}", color::red("✗"), file_path.display(), e);
std::process::exit(1);
}
};
if let Err(e) = man.render(&mut file) {
eprintln!("{} Failed to render man page: {}", color::red("✗"), e);
std::process::exit(1);
}
println!("{} Generated man page → {}", color::green("✓"), file_path.display());
} else {
let mut buf = Vec::new();
man.render(&mut buf).expect("Failed to render man page");
print!("{}", String::from_utf8_lossy(&buf));
}
}
Commands::Mcp => {
eprintln!("Starting MCP server on stdio");
mcp::serve_stdio();
}
Commands::Memory { query } => {
println!("Memory query: {}", query);
}
Commands::SessionState { file } => {
match reliary_core::parse_session_file(file) {
Ok(state) => {
if state.turn_count < 3 {
println!("early");
} else {
println!("{}", reliary_core::build_state_block(&state, state.turn_count));
}
}
Err(e) => { eprintln!("✗ Session file error: {}", e); std::process::exit(1); }
}
}
Commands::Classify { file, line, stem } => {
let role = if let Some(db) = open_index_or_prompt(".") {
let stem_stemmed = reliary_search::porter_stem(stem);
let stem_lower = stem.to_lowercase();
let mut stmt = match db.prepare_cached(
"SELECT o.tag FROM occurrence o
JOIN phrases p ON o.phrase_id = p.id
JOIN file_map f ON o.file_id = f.id
WHERE (p.phrase = ?1 OR p.phrase = ?2) AND f.file_path = ?3 AND o.line = ?4 LIMIT 1"
) {
Ok(s) => s,
Err(_) => {
eprintln!("error: prepare failed");
return;
}
};
let tag: Option<i64> = stmt.query_row(
rusqlite::params![stem_lower, stem_stemmed, file, *line],
|r| r.get(0)
)
.inspect_err(|e| eprintln!("[parse-expr] occurrence tag lookup failed: {}", e))
.ok(); match tag {
Some(1) | Some(2) | Some(3) => "function_def",
Some(4) => "field_access",
Some(5) => "param",
Some(6) => "local_var",
Some(7) => "import_or_use",
_ => {
let content = std::fs::read_to_string(file).unwrap_or_default();
let line_idx = if *line < 0 { 0usize } else { *line as usize };
let line_text = content.lines().nth(line_idx).unwrap_or("");
reliary_search::type_flow::predict_role_with_stem(line_text, stem)
}
}
} else {
let content = std::fs::read_to_string(file).unwrap_or_default();
let line_idx = if *line < 0 { 0usize } else { *line as usize };
let line_text = content.lines().nth(line_idx).unwrap_or("");
reliary_search::type_flow::predict_role_with_stem(line_text, stem)
};
println!("{}", role);
}
Commands::ParseExpr { line, path } => {
let db_path = index_db_path(path);
let table = if let Ok(db) = rusqlite::Connection::open(&db_path) {
let mut t = reliary_search::op_table::mine_op_table(&db).unwrap_or_else(|_| reliary_search::op_table::OpTable::new());
if t.entries.is_empty() {
t.entries.insert("+".to_string(), reliary_search::op_table::OpEntry { precedence: 5.0, associativity: 'L' });
t.entries.insert("-".to_string(), reliary_search::op_table::OpEntry { precedence: 5.0, associativity: 'L' });
t.entries.insert("*".to_string(), reliary_search::op_table::OpEntry { precedence: 7.0, associativity: 'L' });
t.entries.insert("/".to_string(), reliary_search::op_table::OpEntry { precedence: 7.0, associativity: 'L' });
t.entries.insert("==".to_string(), reliary_search::op_table::OpEntry { precedence: 3.0, associativity: 'L' });
t.entries.insert("!=".to_string(), reliary_search::op_table::OpEntry { precedence: 3.0, associativity: 'L' });
t.entries.insert("<".to_string(), reliary_search::op_table::OpEntry { precedence: 4.0, associativity: 'L' });
t.entries.insert(">".to_string(), reliary_search::op_table::OpEntry { precedence: 4.0, associativity: 'L' });
t.entries.insert("&&".to_string(), reliary_search::op_table::OpEntry { precedence: 2.0, associativity: 'L' });
t.entries.insert("||".to_string(), reliary_search::op_table::OpEntry { precedence: 1.0, associativity: 'L' });
t.postfix = reliary_search::op_table::default_postfix();
}
t
} else {
reliary_search::op_table::OpTable::new()
};
match reliary_search::expr_tree::parse_expression(line, &table) {
Some(tree) => println!("{}", tree.dump(0)),
None => println!("(parse failed)"),
}
}
}
}