use std::io::{Read, Write};
use std::process;
use crate::audit;
use crate::extract;
use crate::hookio;
use crate::initcmd;
use crate::parse;
use super::admin::{cmd_clear, cmd_map, cmd_stats};
use super::answer::{
emit_freshness_warning, emit_zero_hit_note, ZERO_HIT_FIND, ZERO_HIT_IMPACT, ZERO_HIT_TESTS,
};
use super::args::first_positional;
use super::coverage::cmd_tests;
use super::find::cmd_find;
use super::impact::cmd_impact;
use super::import_edges::cmd_import_edges;
use super::read::cmd_read;
use super::refs::cmd_refs;
use super::root::{apply_global_options, current_dir};
fn print_out(out: &str) {
use std::io::Write;
let stdout = std::io::stdout();
let mut lock = stdout.lock();
let _ = lock
.write_all(out.as_bytes())
.and_then(|()| lock.write_all(b"\n"));
}
const HELP: &str = "\
devscout -- fast code index for C# and TypeScript codebases.
usage: devscout <command> [args]
index
init [scope ...] register this repo, install hooks, first map
[--label L] [--no-hooks] [--no-map]
map [scope ...] [--refresh] build the index for the given scopes
clear --older-than <days> drop freshness rows older than N days
clear --session <id> drop one session's freshness rows
query
find <query> [--resources] search the manifest by name or purpose
refs <symbol> references to a symbol [--out --all --no-guess --no-dispatch --pick N --json|--compact]
read <symbol> decl span + inbound refs [--no-guess --no-dispatch --pick N --json|--compact]
impact <file|symbol> blast radius [--hops N --no-guess --no-dispatch --no-imports --pick N --json|--compact]
tests <symbol> tests reaching a symbol [--no-guess --no-dispatch --pick N --json|--compact]
import-edges <file> --repo <id> load a cross-repo edge export for `impact` to read
stats index + cache summary for this repo
plumbing
parse <file.cs> dump the parse tree
spans <file.cs> dump declaration spans
extract-dump <file.cs> dump extraction records
audit --semantic <refs.jsonl> score uses-member edges against a semantic oracle [--units F] [--defs F] [--json] [--assert F]
hook <read|bash> agent hook filters, stdin -> stdout
noop exit 0 (harness probe)
-C <dir> run as if devscout started in <dir>
-h, --help show this help
-V, --version show the version
";
#[allow(
clippy::too_many_lines,
reason = "one flat match over every subcommand; splitting it would scatter the dispatch table across files"
)]
pub fn dispatch(args: Vec<String>) {
let (cwd, args) = match apply_global_options(¤t_dir(), &args) {
Ok(v) => v,
Err(e) => {
print_out(&format!("error: {e}"));
process::exit(1);
}
};
match args.get(1).map(String::as_str) {
Some("noop") => process::exit(0),
Some("help") | Some("--help") | Some("-h") => {
print!("{HELP}");
process::exit(0);
}
Some("--version") | Some("-V") => {
println!("devscout {}", env!("CARGO_PKG_VERSION"));
process::exit(0);
}
Some("parse") => {
let Some(path) = args.get(2) else {
eprintln!("usage: devscout parse <file.cs>");
process::exit(1);
};
parse::run_parse(path);
}
Some("spans") => {
let Some(path) = args.get(2) else {
eprintln!("usage: devscout spans <file.cs>");
process::exit(1);
};
parse::run_spans(path);
}
Some("extract-dump") => {
let Some(path) = args.get(2) else {
eprintln!("usage: devscout extract-dump <file.cs>");
process::exit(1);
};
extract::run_extract_dump(path);
}
Some("audit") => {
let (code, out) = audit::cmd_audit(&cwd, &args[2..]);
print_out(&out);
process::exit(code);
}
Some("hook") => match args.get(2).map(String::as_str) {
Some("read") => run_hook(hookio::run_read),
Some("bash") => run_hook(hookio::run_bash),
_ => {
eprintln!("usage: devscout hook <read|bash>");
process::exit(1);
}
},
Some("refs") => {
let (code, out, note) = cmd_refs(&cwd, &args[2..]);
print_out(&out);
emit_freshness_warning(&cwd);
emit_zero_hit_note(code, note, &cwd, first_positional(&args[2..]));
process::exit(code);
}
Some("read") => {
let (code, out, note) = cmd_read(&cwd, &args[2..]);
print_out(&out);
emit_freshness_warning(&cwd);
emit_zero_hit_note(code, note, &cwd, first_positional(&args[2..]));
process::exit(code);
}
Some("impact") => {
let (code, out) = cmd_impact(&cwd, &args[2..]);
print_out(&out);
emit_freshness_warning(&cwd);
emit_zero_hit_note(code, Some(ZERO_HIT_IMPACT), &cwd, None);
process::exit(code);
}
Some("tests") => {
let (code, out) = cmd_tests(&cwd, &args[2..]);
print_out(&out);
emit_freshness_warning(&cwd);
emit_zero_hit_note(code, Some(ZERO_HIT_TESTS), &cwd, None);
process::exit(code);
}
Some("find") => {
let resources = args[2..].iter().any(|a| a == "--resources");
let query_str = args[2..]
.iter()
.filter(|a| a.as_str() != "--resources")
.cloned()
.collect::<Vec<_>>()
.join(" ");
let (code, out) = cmd_find(&cwd, &query_str, resources);
print_out(&out);
emit_freshness_warning(&cwd);
emit_zero_hit_note(code, Some(ZERO_HIT_FIND), &cwd, Some(query_str.as_str()));
process::exit(code);
}
Some("import-edges") => {
let (code, out) = cmd_import_edges(&cwd, &args[2..]);
print_out(&out);
process::exit(code);
}
Some("map") => {
let (code, out) = cmd_map(&cwd, &args[2..]);
print_out(&out);
process::exit(code);
}
Some("stats") => {
let (code, out) = cmd_stats(&cwd);
print_out(&out);
process::exit(code);
}
Some("clear") => {
let (code, out) = cmd_clear(&cwd, &args[2..]);
print_out(&out);
process::exit(code);
}
Some("init") => {
let (code, out) = initcmd::cmd_init_full(&cwd, &args[2..]);
print_out(&out);
process::exit(code);
}
_ => {
eprintln!("usage: devscout <noop|parse|spans|extract-dump|hook|refs|read|impact|tests|find|import-edges|map|stats|clear|init> [args]");
process::exit(1);
}
}
}
fn run_hook(handler: fn(&[u8]) -> Vec<u8>) {
let mut buf = Vec::new();
let _ = std::io::stdin().read_to_end(&mut buf);
let out = handler(&buf);
if !out.is_empty() {
let _ = std::io::stdout().write_all(&out);
}
process::exit(0);
}