pub mod api_docs;
pub mod builder;
pub mod cache;
pub mod completion;
pub mod model;
pub mod parser;
pub mod runner;
pub mod validation;
use std::time::Duration;
use anyhow::Result;
use crate::model::ProbeResult;
use crate::parser::{
detect_help_flag, parse_arguments, parse_environment_variables, parse_examples,
parse_options_from_sections, parse_options_from_usage_blocks, parse_subcommands, parse_usages,
parse_validation_rules,
};
use crate::runner::{RunOutcome, run_with_timeout};
#[derive(Clone)]
pub struct ProbeConfig {
pub timeout_secs: u64,
pub require_help_flag: bool,
pub cache: Option<crate::cache::CacheConfig>,
}
const COMMON_HELP_FLAGS: &[&str] = &["--help", "-h", "help", "--usage", "-?", "/?"];
async fn ensure_help_flag(
program: &str,
args: &[String],
timeout: Duration,
) -> (Vec<String>, bool) {
if detect_help_flag(args) {
return (args.to_vec(), true);
}
let test_timeout = Duration::from_secs(1).min(timeout);
for help_flag in COMMON_HELP_FLAGS {
let test_args = {
let mut new_args = args.to_vec();
new_args.push(help_flag.to_string());
new_args
};
if let Ok(RunOutcome::Completed(output)) =
run_with_timeout(program, &test_args, test_timeout).await
{
if output.status.code() == Some(0) {
return (test_args, true);
}
if !output.stdout.is_empty() || !output.stderr.is_empty() {
let combined = format!(
"{}\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
let combined_lower = combined.to_lowercase();
if combined_lower.contains("usage")
|| combined_lower.contains("options")
|| combined_lower.contains("commands")
|| combined_lower.contains("help")
{
return (test_args, true);
}
}
}
}
let mut default_args = args.to_vec();
default_args.push("--help".to_string());
(default_args, false)
}
pub async fn probe_command(
program: &str,
args: &[String],
config: &ProbeConfig,
) -> Result<ProbeResult> {
let timeout = Duration::from_secs(config.timeout_secs);
let (final_args, help_flag_detected) = ensure_help_flag(program, args, timeout).await;
if let Some(cache_config) = &config.cache {
if let Some(cached_result) = crate::cache::read_cache(program, &final_args, cache_config)? {
return Ok(cached_result);
}
}
if config.require_help_flag && !help_flag_detected {
}
let outcome = run_with_timeout(program, &final_args, timeout).await?;
let mut result = ProbeResult {
command: program.to_string(),
args: final_args.clone(),
exit_code: None,
timed_out: false,
help_flag_detected,
usage_blocks: Vec::new(),
options: Vec::new(),
subcommands: Vec::new(),
arguments: Vec::new(),
examples: Vec::new(),
environment_variables: Vec::new(),
validation_rules: Vec::new(),
raw_stdout: String::new(),
raw_stderr: String::new(),
};
match outcome {
RunOutcome::Completed(output) => {
result.exit_code = output.status.code();
result.raw_stdout = String::from_utf8_lossy(&output.stdout).to_string();
result.raw_stderr = String::from_utf8_lossy(&output.stderr).to_string();
result.usage_blocks = parse_usages(&output.stdout, &output.stderr);
let mut options = parse_options_from_usage_blocks(&result.usage_blocks);
let mut section_options =
parse_options_from_sections(&result.raw_stdout, &result.raw_stderr);
options.append(&mut section_options);
options.sort_by(|a, b| {
let a_flags = format!("{:?}{:?}", a.short_flags, a.long_flags);
let b_flags = format!("{:?}{:?}", b.short_flags, b.long_flags);
a_flags.cmp(&b_flags)
});
options.dedup_by(|a, b| a.short_flags == b.short_flags && a.long_flags == b.long_flags);
result.options = options;
result.subcommands = parse_subcommands(&result.raw_stdout, &result.raw_stderr);
result.arguments =
parse_arguments(&result.raw_stdout, &result.raw_stderr, &result.usage_blocks);
result.examples = parse_examples(&result.raw_stdout, &result.raw_stderr);
result.environment_variables = parse_environment_variables(
&result.raw_stdout,
&result.raw_stderr,
&result.options,
);
result.validation_rules = parse_validation_rules(
&result.raw_stdout,
&result.raw_stderr,
&result.options,
&result.arguments,
);
}
RunOutcome::TimedOut => {
result.timed_out = true;
}
}
if let Some(cache_config) = &config.cache {
let _ = crate::cache::write_cache(program, &final_args, &result, cache_config);
}
Ok(result)
}
pub async fn discover_subcommand_hierarchy(
program: &str,
config: &ProbeConfig,
max_depth: usize,
) -> anyhow::Result<Vec<crate::model::SubcommandSpec>> {
discover_subcommands_recursive(
program.to_string(),
Vec::new(),
ProbeConfig {
timeout_secs: config.timeout_secs,
require_help_flag: config.require_help_flag,
cache: config.cache.clone(),
},
max_depth,
0,
)
.await
}
pub async fn discover_all_subcommands(
program: &str,
config: &ProbeConfig,
max_depth: Option<usize>,
) -> anyhow::Result<crate::model::CommandTree> {
let max_depth = max_depth.unwrap_or(5);
let root_result = probe_command(program, &["--help".to_string()], config).await?;
let subcommands = discover_subcommands_recursive(
program.to_string(),
Vec::new(),
ProbeConfig {
timeout_secs: config.timeout_secs,
require_help_flag: config.require_help_flag,
cache: config.cache.clone(),
},
max_depth,
0,
)
.await?;
let total_commands = count_subcommands(&subcommands) + 1;
Ok(crate::model::CommandTree {
command: program.to_string(),
options: root_result.options,
arguments: root_result.arguments,
subcommands,
total_commands,
})
}
fn count_subcommands(subcommands: &[crate::model::SubcommandSpec]) -> usize {
let mut count = subcommands.len();
for subcmd in subcommands {
count += count_subcommands(&subcmd.subcommands);
}
count
}
fn discover_subcommands_recursive(
program: String,
path: Vec<String>,
config: ProbeConfig,
max_depth: usize,
current_depth: usize,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = anyhow::Result<Vec<crate::model::SubcommandSpec>>> + Send>,
> {
Box::pin(async move {
if current_depth >= max_depth {
return Ok(Vec::new());
}
let mut args = path.clone();
args.push("--help".to_string());
let result = probe_command(&program, &args, &config).await?;
let flat_subcommands = parse_subcommands(&result.raw_stdout, &result.raw_stderr);
let mut hierarchical_subcommands = Vec::new();
for flat_sc in flat_subcommands {
let new_path: Vec<String> = path
.iter()
.cloned()
.chain(std::iter::once(flat_sc.name.clone()))
.collect();
let full_path = new_path.join(" ");
let mut subcommand_args = new_path.clone();
subcommand_args.push("--help".to_string());
let subcommand_result = probe_command(&program, &subcommand_args, &config).await?;
let mut subcommand_options =
parse_options_from_usage_blocks(&subcommand_result.usage_blocks);
let mut section_options = parse_options_from_sections(
&subcommand_result.raw_stdout,
&subcommand_result.raw_stderr,
);
subcommand_options.append(&mut section_options);
subcommand_options.sort_by(|a, b| {
let a_flags = format!("{:?}{:?}", a.short_flags, a.long_flags);
let b_flags = format!("{:?}{:?}", b.short_flags, b.long_flags);
a_flags.cmp(&b_flags)
});
subcommand_options
.dedup_by(|a, b| a.short_flags == b.short_flags && a.long_flags == b.long_flags);
let subcommand_arguments = parse_arguments(
&subcommand_result.raw_stdout,
&subcommand_result.raw_stderr,
&subcommand_result.usage_blocks,
);
let nested_subcommands = discover_subcommands_recursive(
program.clone(),
new_path.clone(),
config.clone(),
max_depth,
current_depth + 1,
)
.await?;
hierarchical_subcommands.push(crate::model::SubcommandSpec {
name: flat_sc.name,
description: flat_sc.description,
full_path: full_path.clone(),
parent: path.last().cloned(),
options: subcommand_options,
arguments: subcommand_arguments,
subcommands: nested_subcommands,
});
}
Ok(hierarchical_subcommands)
})
}