devscout_rs/cli/
dispatch.rs1use std::io::{Read, Write};
12use std::process;
13
14use crate::audit;
15use crate::extract;
16use crate::hookio;
17use crate::initcmd;
18use crate::parse;
19
20use super::admin::{cmd_clear, cmd_map, cmd_stats};
21use super::answer::{
22 emit_freshness_warning, emit_zero_hit_note, ZERO_HIT_FIND, ZERO_HIT_IMPACT, ZERO_HIT_TESTS,
23};
24use super::args::first_positional;
25use super::coverage::cmd_tests;
26use super::find::cmd_find;
27use super::impact::cmd_impact;
28use super::import_edges::cmd_import_edges;
29use super::read::cmd_read;
30use super::refs::cmd_refs;
31use super::root::{apply_global_options, current_dir};
32
33fn print_out(out: &str) {
40 use std::io::Write;
41 let stdout = std::io::stdout();
42 let mut lock = stdout.lock();
43 let _ = lock
44 .write_all(out.as_bytes())
45 .and_then(|()| lock.write_all(b"\n"));
46}
47
48const HELP: &str = "\
52devscout -- fast code index for C# and TypeScript codebases.
53
54usage: devscout <command> [args]
55
56index
57 init [scope ...] register this repo, install hooks, first map
58 [--label L] [--no-hooks] [--no-map]
59 map [scope ...] [--refresh] build the index for the given scopes
60 clear --older-than <days> drop freshness rows older than N days
61 clear --session <id> drop one session's freshness rows
62
63query
64 find <query> [--resources] search the manifest by name or purpose
65 refs <symbol> references to a symbol [--out --all --no-guess --no-dispatch --pick N --json|--compact]
66 read <symbol> decl span + inbound refs [--no-guess --no-dispatch --pick N --json|--compact]
67 impact <file|symbol> blast radius [--hops N --no-guess --no-dispatch --no-imports --pick N --json|--compact]
68 tests <symbol> tests reaching a symbol [--no-guess --no-dispatch --pick N --json|--compact]
69 import-edges <file> --repo <id> load a cross-repo edge export for `impact` to read
70 stats index + cache summary for this repo
71
72plumbing
73 parse <file.cs> dump the parse tree
74 spans <file.cs> dump declaration spans
75 extract-dump <file.cs> dump extraction records
76 audit --semantic <refs.jsonl> score uses-member edges against a semantic oracle [--units F] [--defs F] [--json] [--assert F]
77 hook <read|bash> agent hook filters, stdin -> stdout
78 noop exit 0 (harness probe)
79
80 -C <dir> run as if devscout started in <dir>
81 -h, --help show this help
82 -V, --version show the version
83";
84
85#[allow(
87 clippy::too_many_lines,
88 reason = "one flat match over every subcommand; splitting it would scatter the dispatch table across files"
89)]
90pub fn dispatch(args: Vec<String>) {
91 let (cwd, args) = match apply_global_options(¤t_dir(), &args) {
92 Ok(v) => v,
93 Err(e) => {
94 print_out(&format!("error: {e}"));
95 process::exit(1);
96 }
97 };
98 match args.get(1).map(String::as_str) {
99 Some("noop") => process::exit(0),
100 Some("help") | Some("--help") | Some("-h") => {
101 print!("{HELP}");
102 process::exit(0);
103 }
104 Some("--version") | Some("-V") => {
105 println!("devscout {}", env!("CARGO_PKG_VERSION"));
106 process::exit(0);
107 }
108 Some("parse") => {
109 let Some(path) = args.get(2) else {
110 eprintln!("usage: devscout parse <file.cs>");
111 process::exit(1);
112 };
113 parse::run_parse(path);
114 }
115 Some("spans") => {
116 let Some(path) = args.get(2) else {
117 eprintln!("usage: devscout spans <file.cs>");
118 process::exit(1);
119 };
120 parse::run_spans(path);
121 }
122 Some("extract-dump") => {
123 let Some(path) = args.get(2) else {
124 eprintln!("usage: devscout extract-dump <file.cs>");
125 process::exit(1);
126 };
127 extract::run_extract_dump(path);
128 }
129 Some("audit") => {
130 let (code, out) = audit::cmd_audit(&cwd, &args[2..]);
131 print_out(&out);
132 process::exit(code);
133 }
134 Some("hook") => match args.get(2).map(String::as_str) {
135 Some("read") => run_hook(hookio::run_read),
136 Some("bash") => run_hook(hookio::run_bash),
137 _ => {
138 eprintln!("usage: devscout hook <read|bash>");
139 process::exit(1);
140 }
141 },
142 Some("refs") => {
143 let (code, out, note) = cmd_refs(&cwd, &args[2..]);
144 print_out(&out);
145 emit_freshness_warning(&cwd);
146 emit_zero_hit_note(code, note, &cwd, first_positional(&args[2..]));
147 process::exit(code);
148 }
149 Some("read") => {
150 let (code, out, note) = cmd_read(&cwd, &args[2..]);
151 print_out(&out);
152 emit_freshness_warning(&cwd);
153 emit_zero_hit_note(code, note, &cwd, first_positional(&args[2..]));
154 process::exit(code);
155 }
156 Some("impact") => {
157 let (code, out) = cmd_impact(&cwd, &args[2..]);
158 print_out(&out);
159 emit_freshness_warning(&cwd);
160 emit_zero_hit_note(code, Some(ZERO_HIT_IMPACT), &cwd, None);
161 process::exit(code);
162 }
163 Some("tests") => {
164 let (code, out) = cmd_tests(&cwd, &args[2..]);
165 print_out(&out);
166 emit_freshness_warning(&cwd);
167 emit_zero_hit_note(code, Some(ZERO_HIT_TESTS), &cwd, None);
168 process::exit(code);
169 }
170 Some("find") => {
171 let resources = args[2..].iter().any(|a| a == "--resources");
175 let query_str = args[2..]
176 .iter()
177 .filter(|a| a.as_str() != "--resources")
178 .cloned()
179 .collect::<Vec<_>>()
180 .join(" ");
181 let (code, out) = cmd_find(&cwd, &query_str, resources);
182 print_out(&out);
183 emit_freshness_warning(&cwd);
184 emit_zero_hit_note(code, Some(ZERO_HIT_FIND), &cwd, Some(query_str.as_str()));
185 process::exit(code);
186 }
187 Some("import-edges") => {
188 let (code, out) = cmd_import_edges(&cwd, &args[2..]);
189 print_out(&out);
190 process::exit(code);
191 }
192 Some("map") => {
193 let (code, out) = cmd_map(&cwd, &args[2..]);
194 print_out(&out);
195 process::exit(code);
196 }
197 Some("stats") => {
198 let (code, out) = cmd_stats(&cwd);
199 print_out(&out);
200 process::exit(code);
201 }
202 Some("clear") => {
203 let (code, out) = cmd_clear(&cwd, &args[2..]);
204 print_out(&out);
205 process::exit(code);
206 }
207 Some("init") => {
208 let (code, out) = initcmd::cmd_init_full(&cwd, &args[2..]);
209 print_out(&out);
210 process::exit(code);
211 }
212 _ => {
213 eprintln!("usage: devscout <noop|parse|spans|extract-dump|hook|refs|read|impact|tests|find|import-edges|map|stats|clear|init> [args]");
214 process::exit(1);
215 }
216 }
217}
218
219fn run_hook(handler: fn(&[u8]) -> Vec<u8>) {
226 let mut buf = Vec::new();
227 let _ = std::io::stdin().read_to_end(&mut buf);
228 let out = handler(&buf);
229 if !out.is_empty() {
230 let _ = std::io::stdout().write_all(&out);
231 }
232 process::exit(0);
233}