mod guard;
mod output;
mod probe;
mod resolve;
mod search;
mod setup;
mod show;
mod status;
mod symbol;
mod vectors;
use clap::{Args, Parser, Subcommand};
use std::{
path::{Path, PathBuf},
process::ExitCode,
};
#[derive(Parser)]
#[command(name = "cargo", bin_name = "cargo")]
enum Cargo {
Teksilo(Cli),
}
#[derive(Args)]
#[command(
version,
about = "API lookup, documentation, and agent tooling for Teksilo"
)]
struct Cli {
#[arg(short, long, global = true, conflicts_with = "verbose")]
quiet: bool,
#[arg(short, long, global = true)]
verbose: bool,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Symbol {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Search {
#[arg(required = true)]
query: Vec<String>,
#[arg(long, default_value_t = 8)]
limit: usize,
#[arg(long)]
kind: Option<SearchKind>,
#[arg(long)]
lexical: bool,
#[arg(long)]
json: bool,
},
Show {
#[arg(required_unless_present = "list")]
path: Option<String>,
#[arg(long, value_name = "A-B")]
lines: Option<String>,
#[arg(long, conflicts_with_all = ["lines", "path"])]
list: bool,
},
Init {
#[arg(long = "agent", value_enum)]
agents: Vec<setup::Agent>,
#[arg(short = 'y', long)]
yes: bool,
#[arg(long)]
force: bool,
},
Agent {
#[command(subcommand)]
command: AgentCommand,
},
Probe {
#[command(subcommand)]
command: ProbeCommand,
},
Model {
#[command(subcommand)]
command: ModelCommand,
},
Status {
#[arg(long)]
json: bool,
},
#[command(hide = true)]
BuildVectors {
#[arg(long)]
corpus: Option<String>,
},
}
#[derive(Subcommand)]
enum AgentCommand {
List {
#[arg(long)]
user: bool,
#[arg(long)]
json: bool,
},
Install {
#[arg(required = true, value_enum)]
agents: Vec<setup::Agent>,
#[arg(long)]
user: bool,
#[arg(long)]
force: bool,
},
}
#[derive(Subcommand)]
enum ProbeCommand {
Install {
#[arg(long)]
force: bool,
},
}
#[derive(Subcommand)]
enum ModelCommand {
Fetch,
}
#[derive(Clone, Copy, clap::ValueEnum)]
enum SearchKind {
Guide,
Example,
}
fn main() -> ExitCode {
let Cargo::Teksilo(cli) = Cargo::parse();
let json = matches!(
&cli.command,
Command::Status { json: true }
| Command::Search { json: true, .. }
| Command::Agent {
command: AgentCommand::List { json: true, .. }
}
);
output::set(output::Options {
quiet: cli.quiet,
verbose: cli.verbose,
json,
});
let result = std::env::current_dir()
.map_err(|e| e.to_string())
.and_then(|dir| run(&dir, cli.command));
match result {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
if !e.is_empty() {
eprintln!(
"{}",
if e.starts_with("error:") {
e
} else {
format!("error: {e}")
}
);
}
ExitCode::FAILURE
}
}
}
fn process_result<E: std::fmt::Display>(result: Result<i32, E>) -> Result<(), String> {
match result {
Ok(0) => Ok(()),
Ok(_) => Err(String::new()),
Err(e) => Err(e.to_string()),
}
}
fn project(dir: &Path) -> Result<PathBuf, String> {
setup::find_project_root(dir)
.ok_or_else(|| "no Cargo.toml found\nhelp: run inside a Cargo project".into())
}
fn version(root: &Path) -> Result<String, String> {
let r = resolve::resolve(root).map_err(|e| e.to_string())?;
match guard::check(&r.version) {
guard::Verdict::Refuse { app, tool } => Err(guard::refusal_text(
&app,
&tool,
"this command",
guard::readable_sources(&r).as_deref(),
)),
verdict => {
if let Some(note) = verdict.note() {
output::note(note);
}
Ok(r.version)
}
}
}
fn install_probe(root: &Path, version: &str, force: bool) -> Result<(), String> {
let written = probe::materialise(root, force).map_err(|e| e.to_string())?;
probe::record_provenance(root, version).map_err(|e| e.to_string())?;
if output::verbose() {
eprintln!(
"Probe: {} files in {}",
written.total(),
root.join("scripts/teksilo_probe").display()
);
}
Ok(())
}
fn install_agents(targets: &[setup::Target], force: bool) -> Result<(), String> {
for target in targets {
setup::check_conflict(target, force)?;
}
for target in targets {
let done = setup::apply(target).map_err(|e| e.to_string())?;
if output::verbose() {
eprintln!(
"{}: {} ({}, {} files)",
done.agent,
done.path.display(),
done.change.describe(),
done.files
);
}
}
Ok(())
}
fn success(message: impl std::fmt::Display) {
if !output::quiet() {
println!("{message}");
}
}
fn run(dir: &Path, command: Command) -> Result<(), String> {
match command {
Command::Symbol { args } => process_result(symbol::run(dir, &args)),
Command::Search {
query,
limit,
kind,
lexical,
..
} => {
let mut args = query;
args.extend(["--limit".into(), limit.to_string()]);
if let Some(kind) = kind {
args.extend([
"--kind".into(),
match kind {
SearchKind::Guide => "guide",
SearchKind::Example => "example",
}
.into(),
]);
}
if lexical {
args.push("--lexical".into());
}
process_result(search::run(dir, &args))
}
Command::Show { path, lines, list } => {
process_result(show::run(dir, &show::ShowRequest { path, lines, list }))
}
Command::Status { .. } => {
status::report(dir);
Ok(())
}
Command::Probe {
command: ProbeCommand::Install { force },
} => {
let root = project(dir)?;
let version = version(&root)?;
install_probe(&root, &version, force)?;
success("Installed: probe harness.");
Ok(())
}
Command::Init { agents, yes, force } => {
let root = project(dir)?;
let version = version(&root)?;
let targets = setup::selected_targets(&root, &agents, false)?;
for target in &targets {
setup::check_conflict(target, force)?;
}
if !yes {
println!(
"Project {}\nTeksilo {version}\n\nInstall probe harness",
root.display()
);
for target in &targets {
println!("Install {} instructions", target.agent);
}
if !setup::confirm("\nProceed?", "--yes").map_err(|e| e.to_string())? {
return Ok(());
}
}
install_probe(&root, &version, force)?;
install_agents(&targets, force)?;
let mut names = vec!["probe harness"];
names.extend(targets.iter().map(|t| t.agent));
success(format!("Installed: {}.", names.join(", ")));
Ok(())
}
Command::Agent {
command:
AgentCommand::Install {
agents,
user,
force,
},
} => {
let root = if user {
setup::home_dir().map_err(|e| e.to_string())?
} else {
project(dir)?
};
if !user {
version(&root)?;
}
let targets = setup::selected_targets(&root, &agents, user)?;
install_agents(&targets, force)?;
success(format!(
"Installed: {}.",
targets
.iter()
.map(|t| t.agent)
.collect::<Vec<_>>()
.join(", ")
));
Ok(())
}
Command::Agent {
command: AgentCommand::List { user, .. },
} => {
let root = if user {
setup::home_dir().map_err(|e| e.to_string())?
} else {
setup::find_project_root(dir).unwrap_or_else(|| dir.to_path_buf())
};
status::agents(&root, user);
Ok(())
}
Command::Model {
command: ModelCommand::Fetch,
} => {
if output::verbose() {
eprintln!("Search model: ~{} MB", vectors::ENCODER_DOWNLOAD_MB);
}
let dir = vectors::prefetch_encoder().map_err(|e| e.to_string())?;
if output::verbose() {
eprintln!("Model cache: {}", dir.display());
}
success("Search model ready.");
Ok(())
}
Command::BuildVectors { corpus } => {
let args = corpus
.map(|p| vec!["--corpus".into(), p])
.unwrap_or_default();
process_result(vectors::build(dir, &args))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn explicit_agents_and_output_flags_parse() {
assert!(
Cargo::try_parse_from([
"cargo", "teksilo", "init", "--agent", "codex", "--agent", "claude", "-y"
])
.is_ok()
);
assert!(
Cargo::try_parse_from(["cargo", "teksilo", "agent", "install", "codex", "cursor"])
.is_ok()
);
assert!(Cargo::try_parse_from(["cargo", "teksilo", "search", "button", "--json"]).is_ok());
assert!(
Cargo::try_parse_from(["cargo", "teksilo", "--quiet", "--verbose", "status"]).is_err()
);
assert!(Cargo::try_parse_from(["cargo", "teksilo", "setup"]).is_err());
}
}