1pub mod api_docs;
2pub mod builder;
3pub mod cache;
4pub mod completion;
5pub mod model;
6pub mod parser;
7pub mod runner;
8pub mod validation;
9
10use std::time::Duration;
11
12use anyhow::Result;
13
14use crate::model::ProbeResult;
15use crate::parser::{
16 detect_help_flag, parse_arguments, parse_environment_variables, parse_examples,
17 parse_options_from_sections, parse_options_from_usage_blocks, parse_subcommands, parse_usages,
18 parse_validation_rules,
19};
20use crate::runner::{RunOutcome, run_with_timeout};
21
22#[derive(Clone)]
24pub struct ProbeConfig {
25 pub timeout_secs: u64,
27 pub require_help_flag: bool,
31 pub cache: Option<crate::cache::CacheConfig>,
33}
34
35const COMMON_HELP_FLAGS: &[&str] = &["--help", "-h", "help", "--usage", "-?", "/?"];
37
38async fn ensure_help_flag(
41 program: &str,
42 args: &[String],
43 timeout: Duration,
44) -> (Vec<String>, bool) {
45 if detect_help_flag(args) {
47 return (args.to_vec(), true);
48 }
49
50 let test_timeout = Duration::from_secs(1).min(timeout);
52
53 for help_flag in COMMON_HELP_FLAGS {
55 let test_args = {
56 let mut new_args = args.to_vec();
57 new_args.push(help_flag.to_string());
58 new_args
59 };
60
61 if let Ok(RunOutcome::Completed(output)) =
63 run_with_timeout(program, &test_args, test_timeout).await
64 {
65 if output.status.code() == Some(0) {
67 return (test_args, true);
68 }
69 if !output.stdout.is_empty() || !output.stderr.is_empty() {
72 let combined = format!(
74 "{}\n{}",
75 String::from_utf8_lossy(&output.stdout),
76 String::from_utf8_lossy(&output.stderr)
77 );
78 let combined_lower = combined.to_lowercase();
79 if combined_lower.contains("usage")
80 || combined_lower.contains("options")
81 || combined_lower.contains("commands")
82 || combined_lower.contains("help")
83 {
84 return (test_args, true);
85 }
86 }
87 }
88 }
89
90 let mut default_args = args.to_vec();
92 default_args.push("--help".to_string());
93 (default_args, false)
94}
95
96pub async fn probe_command(
101 program: &str,
102 args: &[String],
103 config: &ProbeConfig,
104) -> Result<ProbeResult> {
105 let timeout = Duration::from_secs(config.timeout_secs);
106
107 let (final_args, help_flag_detected) = ensure_help_flag(program, args, timeout).await;
109
110 if let Some(cache_config) = &config.cache {
112 if let Some(cached_result) = crate::cache::read_cache(program, &final_args, cache_config)? {
113 return Ok(cached_result);
114 }
115 }
116
117 if config.require_help_flag && !help_flag_detected {
118 }
121
122 let outcome = run_with_timeout(program, &final_args, timeout).await?;
123
124 let mut result = ProbeResult {
125 command: program.to_string(),
126 args: final_args.clone(),
127 exit_code: None,
128 timed_out: false,
129 help_flag_detected,
130 usage_blocks: Vec::new(),
131 options: Vec::new(),
132 subcommands: Vec::new(),
133 arguments: Vec::new(),
134 examples: Vec::new(),
135 environment_variables: Vec::new(),
136 validation_rules: Vec::new(),
137 raw_stdout: String::new(),
138 raw_stderr: String::new(),
139 };
140
141 match outcome {
142 RunOutcome::Completed(output) => {
143 result.exit_code = output.status.code();
144 result.raw_stdout = String::from_utf8_lossy(&output.stdout).to_string();
145 result.raw_stderr = String::from_utf8_lossy(&output.stderr).to_string();
146
147 result.usage_blocks = parse_usages(&output.stdout, &output.stderr);
148
149 let mut options = parse_options_from_usage_blocks(&result.usage_blocks);
151 let mut section_options =
152 parse_options_from_sections(&result.raw_stdout, &result.raw_stderr);
153 options.append(&mut section_options);
154
155 options.sort_by(|a, b| {
157 let a_flags = format!("{:?}{:?}", a.short_flags, a.long_flags);
158 let b_flags = format!("{:?}{:?}", b.short_flags, b.long_flags);
159 a_flags.cmp(&b_flags)
160 });
161 options.dedup_by(|a, b| a.short_flags == b.short_flags && a.long_flags == b.long_flags);
162
163 result.options = options;
164 result.subcommands = parse_subcommands(&result.raw_stdout, &result.raw_stderr);
165 result.arguments =
166 parse_arguments(&result.raw_stdout, &result.raw_stderr, &result.usage_blocks);
167 result.examples = parse_examples(&result.raw_stdout, &result.raw_stderr);
168 result.environment_variables = parse_environment_variables(
169 &result.raw_stdout,
170 &result.raw_stderr,
171 &result.options,
172 );
173 result.validation_rules = parse_validation_rules(
174 &result.raw_stdout,
175 &result.raw_stderr,
176 &result.options,
177 &result.arguments,
178 );
179 }
180 RunOutcome::TimedOut => {
181 result.timed_out = true;
182 }
183 }
184
185 if let Some(cache_config) = &config.cache {
187 let _ = crate::cache::write_cache(program, &final_args, &result, cache_config);
188 }
189
190 Ok(result)
191}
192
193pub async fn discover_subcommand_hierarchy(
205 program: &str,
206 config: &ProbeConfig,
207 max_depth: usize,
208) -> anyhow::Result<Vec<crate::model::SubcommandSpec>> {
209 discover_subcommands_recursive(
210 program.to_string(),
211 Vec::new(),
212 ProbeConfig {
213 timeout_secs: config.timeout_secs,
214 require_help_flag: config.require_help_flag,
215 cache: config.cache.clone(),
216 },
217 max_depth,
218 0,
219 )
220 .await
221}
222
223pub async fn discover_all_subcommands(
235 program: &str,
236 config: &ProbeConfig,
237 max_depth: Option<usize>,
238) -> anyhow::Result<crate::model::CommandTree> {
239 let max_depth = max_depth.unwrap_or(5);
240
241 let root_result = probe_command(program, &["--help".to_string()], config).await?;
243
244 let subcommands = discover_subcommands_recursive(
246 program.to_string(),
247 Vec::new(),
248 ProbeConfig {
249 timeout_secs: config.timeout_secs,
250 require_help_flag: config.require_help_flag,
251 cache: config.cache.clone(),
252 },
253 max_depth,
254 0,
255 )
256 .await?;
257
258 let total_commands = count_subcommands(&subcommands) + 1; Ok(crate::model::CommandTree {
262 command: program.to_string(),
263 options: root_result.options,
264 arguments: root_result.arguments,
265 subcommands,
266 total_commands,
267 })
268}
269
270fn count_subcommands(subcommands: &[crate::model::SubcommandSpec]) -> usize {
272 let mut count = subcommands.len();
273 for subcmd in subcommands {
274 count += count_subcommands(&subcmd.subcommands);
275 }
276 count
277}
278
279fn discover_subcommands_recursive(
281 program: String,
282 path: Vec<String>,
283 config: ProbeConfig,
284 max_depth: usize,
285 current_depth: usize,
286) -> std::pin::Pin<
287 Box<dyn std::future::Future<Output = anyhow::Result<Vec<crate::model::SubcommandSpec>>> + Send>,
288> {
289 Box::pin(async move {
290 if current_depth >= max_depth {
291 return Ok(Vec::new());
292 }
293
294 let mut args = path.clone();
296 args.push("--help".to_string());
297
298 let result = probe_command(&program, &args, &config).await?;
300
301 let flat_subcommands = parse_subcommands(&result.raw_stdout, &result.raw_stderr);
303
304 let mut hierarchical_subcommands = Vec::new();
305
306 for flat_sc in flat_subcommands {
307 let new_path: Vec<String> = path
308 .iter()
309 .cloned()
310 .chain(std::iter::once(flat_sc.name.clone()))
311 .collect();
312 let full_path = new_path.join(" ");
313
314 let mut subcommand_args = new_path.clone();
316 subcommand_args.push("--help".to_string());
317 let subcommand_result = probe_command(&program, &subcommand_args, &config).await?;
318
319 let mut subcommand_options =
321 parse_options_from_usage_blocks(&subcommand_result.usage_blocks);
322 let mut section_options = parse_options_from_sections(
323 &subcommand_result.raw_stdout,
324 &subcommand_result.raw_stderr,
325 );
326 subcommand_options.append(&mut section_options);
327
328 subcommand_options.sort_by(|a, b| {
330 let a_flags = format!("{:?}{:?}", a.short_flags, a.long_flags);
331 let b_flags = format!("{:?}{:?}", b.short_flags, b.long_flags);
332 a_flags.cmp(&b_flags)
333 });
334 subcommand_options
335 .dedup_by(|a, b| a.short_flags == b.short_flags && a.long_flags == b.long_flags);
336
337 let subcommand_arguments = parse_arguments(
338 &subcommand_result.raw_stdout,
339 &subcommand_result.raw_stderr,
340 &subcommand_result.usage_blocks,
341 );
342
343 let nested_subcommands = discover_subcommands_recursive(
345 program.clone(),
346 new_path.clone(),
347 config.clone(),
348 max_depth,
349 current_depth + 1,
350 )
351 .await?;
352
353 hierarchical_subcommands.push(crate::model::SubcommandSpec {
354 name: flat_sc.name,
355 description: flat_sc.description,
356 full_path: full_path.clone(),
357 parent: path.last().cloned(),
358 options: subcommand_options,
359 arguments: subcommand_arguments,
360 subcommands: nested_subcommands,
361 });
362 }
363
364 Ok(hierarchical_subcommands)
365 })
366}