mod guard;
mod probe;
mod resolve;
mod search;
mod setup;
mod show;
mod status;
mod symbol;
mod vectors;
use std::path::Path;
use std::process::ExitCode;
use clap::{Args, Parser, Subcommand};
#[derive(Parser)]
#[command(name = "cargo", bin_name = "cargo")]
enum Cargo {
Teksilo(Cli),
}
#[derive(Args)]
#[command(
version,
about = "Agent tooling for teksilo apps",
long_about = "Agent tooling for apps that depend on the teksilo GUI framework.\n\n\
Run from inside your app's crate or workspace — every answer is \
resolved from its Cargo.lock, so it always matches the teksilo you \
actually depend on.",
subcommand_required = true,
arg_required_else_help = true
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Symbol {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
Search {
query: Vec<String>,
#[arg(long, default_value_t = 8, value_name = "N")]
limit: usize,
#[arg(long, value_name = "KIND")]
kind: Option<SearchKind>,
#[arg(long)]
lexical: bool,
},
Show {
#[arg(value_name = "PATH", required_unless_present = "list")]
path: Option<String>,
#[arg(long, value_name = "A-B")]
lines: Option<String>,
#[arg(long, conflicts_with = "lines")]
list: bool,
},
Probe {
#[arg(long)]
force: bool,
},
Setup {
#[arg(long)]
force: bool,
#[arg(short = 'y', long)]
yes: bool,
#[arg(long)]
user: bool,
#[arg(long)]
no_model: bool,
},
Status,
Version,
#[command(hide = true)]
BuildVectors {
#[arg(long, value_name = "PATH")]
corpus: Option<String>,
},
}
#[derive(Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
enum SearchKind {
Guide,
Example,
}
fn main() -> ExitCode {
let Cargo::Teksilo(cli) = Cargo::parse();
let dir = match std::env::current_dir() {
Ok(d) => d,
Err(e) => {
eprintln!("error: cannot read the current directory: {e}");
return ExitCode::FAILURE;
}
};
match cli.command {
Command::Status => {
status::report(&dir);
ExitCode::SUCCESS
}
Command::Version => cmd_version(&dir),
Command::Symbol { args } => cmd_symbol(&dir, &args),
Command::Search {
query,
limit,
kind,
lexical,
} => {
let mut argv: Vec<String> = query;
argv.push("--limit".into());
argv.push(limit.to_string());
if let Some(k) = kind {
argv.push("--kind".into());
argv.push(match k {
SearchKind::Guide => "guide".into(),
SearchKind::Example => "example".into(),
});
}
if lexical {
argv.push("--lexical".into());
}
cmd_search(&dir, &argv)
}
Command::Show { path, lines, list } => {
cmd_show(&dir, &show::ShowRequest { path, lines, list })
}
Command::Probe { force } => cmd_probe(&dir, force),
Command::Setup {
force,
yes,
user,
no_model,
} => cmd_setup(&dir, force, yes, user, no_model),
Command::BuildVectors { corpus } => {
let mut argv = Vec::new();
if let Some(path) = corpus {
argv.push("--corpus".into());
argv.push(path);
}
cmd_build_vectors(&dir, &argv)
}
}
}
fn cmd_version(dir: &Path) -> ExitCode {
println!("cargo-teksilo {}", guard::TOOL_VERSION);
match resolve::resolve(dir) {
Ok(r) => {
println!("app resolved teksilo {}", r.version);
for c in r.crates.values() {
println!(" {:<28} {} {}", c.name, c.version, c.dir.display());
}
match guard::check(&r.version) {
guard::Verdict::Ok => println!("versions match"),
guard::Verdict::Degraded { app, tool } => {
println!("{}", guard::degraded_text(&app, &tool))
}
guard::Verdict::Refuse { app, tool } => println!(
"\n{}",
guard::refusal_text(
&app,
&tool,
"symbol lookup",
guard::readable_sources(&r).as_deref(),
)
),
}
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("{e}");
ExitCode::FAILURE
}
}
}
fn cmd_symbol(dir: &Path, args: &[String]) -> ExitCode {
match symbol::run(dir, args) {
Ok(0) => ExitCode::SUCCESS,
Ok(_) => ExitCode::FAILURE,
Err(e) => {
eprintln!("{e}");
ExitCode::FAILURE
}
}
}
fn cmd_search(dir: &Path, args: &[String]) -> ExitCode {
match search::run(dir, args) {
Ok(0) => ExitCode::SUCCESS,
Ok(_) => ExitCode::FAILURE,
Err(e) => {
eprintln!("{e}");
ExitCode::FAILURE
}
}
}
fn cmd_show(dir: &Path, request: &show::ShowRequest) -> ExitCode {
match show::run(dir, request) {
Ok(0) => ExitCode::SUCCESS,
Ok(_) => ExitCode::FAILURE,
Err(e) => {
eprintln!("{e}");
ExitCode::FAILURE
}
}
}
fn cmd_build_vectors(dir: &Path, args: &[String]) -> ExitCode {
match vectors::build(dir, args) {
Ok(0) => ExitCode::SUCCESS,
Ok(_) => ExitCode::FAILURE,
Err(e) => {
eprintln!("error: {e}");
ExitCode::FAILURE
}
}
}
fn cmd_probe(dir: &Path, force: bool) -> ExitCode {
let version = match resolved_for_probe(dir) {
Ok(v) => v,
Err(ProbeBlock::Refused(message) | ProbeBlock::Unresolved(message)) => {
eprintln!("{message}");
return ExitCode::FAILURE;
}
};
match write_probe(dir, &version, force) {
Ok(summary) => println!("{summary}"),
Err(message) => {
eprintln!("{message}");
return ExitCode::FAILURE;
}
}
println!("\nNext: read scripts/teksilo_probe/ and copy the example closest to your case.");
ExitCode::SUCCESS
}
enum ProbeBlock {
Refused(String),
Unresolved(String),
}
fn resolved_for_probe(project: &Path) -> Result<String, ProbeBlock> {
let resolution =
resolve::resolve(project).map_err(|e| ProbeBlock::Unresolved(e.to_string()))?;
match guard::check(&resolution.version) {
guard::Verdict::Refuse { app, tool } => Err(ProbeBlock::Refused(guard::refusal_text(
&app,
&tool,
"the probe harness",
guard::readable_sources(&resolution).as_deref(),
))),
verdict => {
if let Some(note) = verdict.note() {
eprintln!("{note}");
}
Ok(resolution.version)
}
}
}
fn write_probe(project: &Path, version: &str, force: bool) -> Result<String, String> {
if let Some(recorded) = probe::recorded_provenance(project)
&& recorded != version
{
println!(
"note: the harness here was written for teksilo {recorded}, this app now \
resolves {version}. Rewriting it."
);
}
let written = probe::materialise(project, force).map_err(|e| e.to_string())?;
probe::record_provenance(project, version).map_err(|e| e.to_string())?;
Ok(format!(
"probe harness: {} files in scripts/teksilo_probe/ \
({} created, {} updated, {} unchanged) for teksilo {version}",
written.total(),
written.created.len(),
written.updated.len(),
written.unchanged.len(),
))
}
fn cmd_setup(dir: &Path, force: bool, yes: bool, user: bool, no_model: bool) -> ExitCode {
let project = setup::find_project_root(dir);
let scope = if user {
setup::Scope::User
} else if project.is_some() {
setup::Scope::Project
} else {
println!(
"No Cargo.toml in {} or any directory above it, so there is no project here\n\
to set up. The agent instructions can still be installed for your user account.",
dir.display()
);
if yes {
eprintln!(
"\nerror: -y suppresses a confirmation, not this choice. Pass --user to install\n\
under $HOME, or run this from inside your app."
);
return ExitCode::FAILURE;
}
match setup::confirm("\nInstall for this user instead, under $HOME?", "--user") {
Ok(true) => setup::Scope::User,
Ok(false) => {
println!("Nothing written.");
return ExitCode::SUCCESS;
}
Err(e) => {
eprintln!("error: {e}");
return ExitCode::FAILURE;
}
}
};
let root = match scope {
setup::Scope::Project => project.clone().expect("project scope implies a manifest"),
setup::Scope::User => match setup::home_dir() {
Ok(home) => home,
Err(e) => {
eprintln!("error: {e}");
return ExitCode::FAILURE;
}
},
};
let targets = match scope {
setup::Scope::Project => setup::project_targets(&root),
setup::Scope::User => setup::user_targets(&root),
};
let mut probe_version = None;
let probe_skipped = match &project {
None => Some("no Cargo.toml here — the harness is project code".to_string()),
Some(p) => match resolved_for_probe(p) {
Ok(version) => {
probe_version = Some(version);
None
}
Err(ProbeBlock::Refused(message)) => {
eprintln!("{message}");
return ExitCode::FAILURE;
}
Err(ProbeBlock::Unresolved(message)) => Some(message),
},
};
let fetch_model =
!no_model && !vectors::encoder_is_cached() && vectors::encoder_cache_dir().is_some();
match scope {
setup::Scope::Project => println!("Plan — project {}", root.display()),
setup::Scope::User => {
println!("Plan — user account {}", root.display());
if let Some(p) = &project {
println!(" project {} (harness only)", p.display());
}
}
}
println!();
if let (Some(p), Some(version)) = (&project, &probe_version) {
println!(
" {:<18} {:<40} for teksilo {version}",
"probe harness",
relative(&p.join("scripts/teksilo_probe"), p, true)
);
}
for target in &targets {
println!(
" {:<18} {:<40} {}",
target.agent,
relative(&target.path, &root, target.form == setup::Form::Skill),
target.form.describe()
);
}
if fetch_model {
println!(
" {:<18} {:<40} download ~{} MB, once",
"search encoder",
vectors::encoder_cache_dir().unwrap_or_default().display(),
vectors::ENCODER_DOWNLOAD_MB
);
}
if targets.is_empty() && probe_version.is_none() && !fetch_model {
println!(" (nothing to do)");
}
if let Some(reason) = &probe_skipped {
println!("\nThe probe harness is skipped: {reason}");
}
println!("\nNothing outside those paths is written.");
if !yes {
match setup::confirm("Proceed?", "-y / --yes") {
Ok(true) => {}
Ok(false) => {
println!("Nothing written.");
return ExitCode::SUCCESS;
}
Err(e) => {
eprintln!("error: {e}");
return ExitCode::FAILURE;
}
}
}
println!();
if let (Some(p), Some(version)) = (&project, &probe_version) {
match write_probe(p, version, force) {
Ok(summary) => println!("{summary}"),
Err(message) => {
eprintln!("{message}");
return ExitCode::FAILURE;
}
}
}
for target in &targets {
match setup::apply(target) {
Ok(done) => println!(
"{:<18} {:<40} {} ({} file{})",
done.agent,
relative(&done.path, &root, target.form == setup::Form::Skill),
done.change.describe(),
done.files,
if done.files == 1 { "" } else { "s" }
),
Err(e) => {
eprintln!("error: {e}");
return ExitCode::FAILURE;
}
}
}
if fetch_model {
println!(
"\nFetching the search encoder (~{} MB, once)…",
vectors::ENCODER_DOWNLOAD_MB
);
match vectors::prefetch_encoder() {
Ok(dir) => println!("search encoder: cached in {}", dir.display()),
Err(e) => println!(
"warning: could not fetch the search encoder: {e}\n\
`cargo teksilo search` still works — it degrades to BM25 (lexical) ranking,\n\
and will retry the download on its next run."
),
}
} else if no_model {
println!("\nsearch encoder: skipped (--no-model). The first `search` will fetch it.");
} else if vectors::encoder_cache_dir().is_none() {
println!(
"\nsearch encoder: skipped — this build has none (compiled with \
`--no-default-features`).\n`search` uses BM25 only."
);
} else {
println!("\nsearch encoder: already cached.");
}
match scope {
setup::Scope::Project => {
let missing = setup::project_undetected(&root);
if !missing.is_empty() {
println!("\nNot configured in this project, so nothing was written for them:");
for (agent, marker) in missing {
println!(" {agent:<18} no {marker}");
}
println!("Create the marker it looks for and re-run to install.");
}
}
setup::Scope::User => {
let missing = setup::user_undetected(&root);
if !missing.is_empty() {
println!("\nNot configured for your user, so nothing was written for them:");
for (agent, dir) in missing {
println!(" {agent:<18} no {}/", dir.display());
}
println!("Run the agent once so it creates that directory, then re-run this.");
}
println!("\n{}", setup::user_scope_note());
}
}
println!(
"\nThese are files on disk. An agent picks them up on its own terms —\n\
some ask you to trust the folder first, and a gitignored instruction\n\
file is skipped silently."
);
println!(
"\nWhat you can run now:\n\
\x20 cargo teksilo symbol <Name> exact public API of a type, at the pinned version\n\
\x20 cargo teksilo search \"<query>\" the guides and worked examples\n\
\x20 cargo teksilo show <path> one of them in full, offline — not from GitHub\n\
\x20 cargo teksilo status what is installed, here and for you\n\
\x20 cargo teksilo probe rewrite the automation harness\n\
\x20 cargo teksilo setup this command"
);
ExitCode::SUCCESS
}
fn relative(path: &Path, root: &Path, directory: bool) -> String {
let shown = path.strip_prefix(root).unwrap_or(path).display();
if directory {
format!("{shown}/")
} else {
shown.to_string()
}
}