mod jev;
mod mcp;
mod search;
mod types;
use lexopt::prelude::*;
use std::process;
use std::time::Instant;
fn print_help() {
println!(
r#"jev-scout 0.1.0 - Zero-hallucination open-source repo and crate scout
USAGE:
jev-scout [OPTIONS] <QUERY>
ARGS:
<QUERY> Natural language description of what you are searching for
OPTIONS:
-e, --ecosystem <NAME> Target ecosystem: 'all' (default), 'github', or 'crates'
-n, --limit <NUM> Maximum results to show (default: 5)
-j, --json Output machine-readable JSON to stdout
--mcp Run as a stdio Model Context Protocol (MCP) server
-h, --help Print help information
-v, --version Print version information
EXAMPLES:
jev-scout "fast sqlite tui in rust"
jev-scout "headless browser without chromium" --ecosystem rust
jev-scout "token efficient grep for coding agents" --json
"#
);
}
fn main() {
let mut query: Option<String> = None;
let mut ecosystem = "all".to_string();
let mut limit = 5usize;
let mut json_mode = false;
let mut mcp_mode = false;
let mut parser = lexopt::Parser::from_env();
while let Some(arg) = parser.next().unwrap_or_else(|e| {
eprintln!("Error: {}", e);
process::exit(1);
}) {
match arg {
Short('e') | Long("ecosystem") => {
ecosystem = parser.value().unwrap().string().unwrap();
}
Short('n') | Long("limit") => {
limit = parser.value().unwrap().parse().unwrap_or(5);
}
Short('j') | Long("json") => {
json_mode = true;
}
Long("mcp") => {
mcp_mode = true;
}
Short('h') | Long("help") => {
print_help();
process::exit(0);
}
Short('v') | Long("version") => {
println!("jev-scout 0.1.0");
process::exit(0);
}
Value(val) => {
query = Some(val.string().unwrap_or_default());
}
_ => {
eprintln!("Error: unexpected argument {:?}", arg);
print_help();
process::exit(1);
}
}
}
let api_key = match std::env::var("TYPESAFE_API_KEY") {
Ok(k) if !k.trim().is_empty() => k.trim().to_string(),
_ => {
eprintln!("Error: TYPESAFE_API_KEY environment variable is not set.");
eprintln!("Please get an API key from https://typesafe.ai and export it:");
eprintln!(" export TYPESAFE_API_KEY=\"your_key\"");
process::exit(1);
}
};
if mcp_mode {
if let Err(e) = mcp::run_mcp_server(&api_key) {
eprintln!("MCP Server Error: {}", e);
process::exit(1);
}
return;
}
let query_str = match query {
Some(q) if !q.trim().is_empty() => q,
_ => {
print_help();
process::exit(1);
}
};
let start_time = Instant::now();
if !json_mode {
println!("🔍 Scouting repositories for: \"{}\"", query_str);
}
let candidates = search::search_candidates(&query_str, &ecosystem, 8);
if candidates.is_empty() {
if json_mode {
println!("[]");
} else {
println!("No candidates found matching query.");
}
return;
}
let evaluated = match jev::evaluate_candidates(&query_str, candidates, &api_key) {
Ok(res) => res,
Err(err) => {
eprintln!("Error: {}", err);
process::exit(1);
}
};
let elapsed = start_time.elapsed();
let top_results: Vec<_> = evaluated.into_iter().take(limit).collect();
if json_mode {
println!("{}", serde_json::to_string_pretty(&top_results).unwrap());
return;
}
println!(
"Found {} candidates evaluated in {:.0}ms via TypeSafe Jev:\n",
top_results.len(),
elapsed.as_millis()
);
for (rank, item) in top_results.iter().enumerate() {
let best_tag = if item.is_best_match {
" \x1b[32;1m[BEST MATCH]\x1b[0m"
} else {
""
};
println!(
"\x1b[1m#{}\x1b[0m \x1b[36;1m{}\x1b[0m{}",
rank + 1,
item.candidate.name,
best_tag
);
println!(
" \x1b[33m⭐ {}\x1b[0m | \x1b[35m{}\x1b[0m | Updated: {}",
format_num(item.candidate.stars),
item.candidate.license,
item.candidate.updated_at.chars().take(10).collect::<String>()
);
println!(" {}", item.candidate.description);
println!(
" Fit: \x1b[32m{:.1}/4.0\x1b[0m (Conf: {:.2}) | Active: \x1b[34m{:.0}%\x1b[0m",
item.fit_score,
item.confidence,
item.is_modern * 100.0
);
println!(" URL: \x1b[4m{}\x1b[0m", item.candidate.url);
println!(" \x1b[90mCommand:\x1b[0m {}\n", item.candidate.install_cmd);
}
}
fn format_num(n: u64) -> String {
if n >= 1_000_000 {
format!("{:.1}M", n as f64 / 1_000_000.0)
} else if n >= 1_000 {
format!("{:.1}k", n as f64 / 1_000.0)
} else {
n.to_string()
}
}