mod index;
mod lang;
mod output;
mod query;
mod language;
mod util;
use clap::{Parser, Subcommand};
use std::env;
use std::fs;
use std::path::PathBuf;
use std::process;
#[derive(Parser)]
#[command(name = "cx", version, about = "Semantic code navigation for AI agents")]
struct Cli {
#[command(subcommand)]
command: Commands,
#[arg(long, global = true)]
root: Option<PathBuf>,
#[arg(long, global = true)]
json: bool,
#[arg(long, global = true)]
limit: Option<usize>,
#[arg(long, global = true, default_value = "0")]
offset: usize,
#[arg(long, global = true, conflicts_with = "limit")]
all: bool,
}
#[derive(Subcommand)]
enum Commands {
#[command(alias = "o")]
Overview {
path: PathBuf,
#[arg(long)]
full: bool,
},
#[command(alias = "s")]
Symbols {
#[arg(long)]
file: Option<PathBuf>,
#[arg(long)]
name: Option<String>,
#[arg(long)]
kind: Option<index::SymbolKind>,
#[arg(long)]
kinds: bool,
},
#[command(alias = "d")]
Definition {
#[arg(long)]
name: String,
#[arg(long)]
from: Option<PathBuf>,
#[arg(long)]
kind: Option<index::SymbolKind>,
#[arg(long, default_value = "200")]
max_lines: usize,
},
#[command(alias = "r")]
References {
#[arg(long)]
name: String,
#[arg(long)]
file: Option<PathBuf>,
#[arg(long)]
unique: bool,
},
Lang {
#[command(subcommand)]
action: LangAction,
},
Skill,
Cache {
#[command(subcommand)]
action: CacheAction,
},
}
#[derive(Subcommand)]
enum LangAction {
Add {
languages: Vec<String>,
},
Remove {
languages: Vec<String>,
},
List,
}
#[derive(Subcommand)]
enum CacheAction {
Path,
Clean,
}
fn resolve_root(project: Option<PathBuf>) -> PathBuf {
match project {
Some(p) => p,
None => {
let cwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
util::git::find_project_root(&cwd)
}
}
}
fn main() {
let config = tree_sitter_language_pack::PackConfig {
cache_dir: Some(lang::grammar_cache_dir()),
..Default::default()
};
if let Err(e) = tree_sitter_language_pack::configure(&config) {
eprintln!("cx: failed to configure grammar cache: {}", e);
}
let cli = Cli::parse();
let root = resolve_root(cli.root);
let resolve_pagination = |default_limit: Option<usize>| -> query::Pagination {
let limit = if cli.all {
None
} else {
Some(cli.limit.unwrap_or_else(|| default_limit.unwrap_or(usize::MAX)))
};
let limit = limit.filter(|&n| n < usize::MAX);
query::Pagination { limit, offset: cli.offset }
};
let exit_code = match cli.command {
Commands::Overview { path, full } => {
let idx = index::Index::load_or_build(&root);
let abs = if path.is_absolute() { path.clone() } else {
env::current_dir().unwrap_or_else(|_| root.clone()).join(&path)
};
if abs.is_dir() {
query::dir_overview(&idx, &path, full, cli.json, &resolve_pagination(None))
} else {
query::symbols(&idx, Some(&path), None, None, cli.json, &resolve_pagination(None))
}
}
Commands::Symbols { file, name, kind, kinds } => {
let idx = index::Index::load_or_build(&root);
if kinds {
query::kind_counts(&idx, file.as_deref(), cli.json)
} else {
query::symbols(&idx, file.as_deref(), name.as_deref(), kind, cli.json, &resolve_pagination(Some(100)))
}
}
Commands::Definition { name, from, kind, max_lines } => {
let idx = index::Index::load_or_build(&root);
let default = if from.is_some() { None } else { Some(3) };
query::definition(&idx, &name, from.as_deref(), kind, max_lines, cli.json, &resolve_pagination(default))
}
Commands::References { name, file, unique } => {
let idx = index::Index::load_or_build(&root);
query::references(&idx, &name, file.as_deref(), unique, cli.json, &resolve_pagination(Some(50)))
}
Commands::Lang { action } => {
match action {
LangAction::Add { languages } => lang::add(&languages),
LangAction::Remove { languages } => lang::remove(&languages),
LangAction::List => lang::list(),
}
}
Commands::Skill => {
print!("{}", include_str!("skill.md"));
0
}
Commands::Cache { action } => {
let path = index::cache_path_for(&root);
match action {
CacheAction::Path => {
println!("{}", path.display());
0
}
CacheAction::Clean => {
if path.exists() {
if let Err(e) = fs::remove_file(&path) {
eprintln!("cx: failed to remove cache: {}", e);
1
} else {
eprintln!("cx: removed {}", path.display());
0
}
} else {
eprintln!("cx: no cached index for this project");
0
}
}
}
}
};
process::exit(exit_code);
}