mod cli;
use anyhow::{Context, Result, anyhow};
use clap::Parser;
use std::io::{self, Write};
use std::path::Path;
use crate::cli::Cli;
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
if cli.cmd.is_empty() {
return Err(anyhow!(
"No command provided. Use: help-probe -- <command> [args...]"
));
}
let program = &cli.cmd[0];
let args = &cli.cmd[1..];
if cli.clear_cache {
use help_probe::cache::{CacheConfig, clear_cache};
use std::path::PathBuf;
let cache_config = CacheConfig {
cache_dir: cli
.cache_dir
.as_ref()
.map(|d| PathBuf::from(d))
.unwrap_or_else(help_probe::cache::default_cache_dir),
enabled: true,
max_age_secs: None,
};
let cleared = clear_cache(Some(program), Some(args), &cache_config)?;
println!("Cleared {} cache entry(ies)", cleared);
return Ok(());
}
let cache_config = if cli.no_cache {
None
} else {
use help_probe::cache::CacheConfig;
use std::path::PathBuf;
Some(CacheConfig {
cache_dir: cli
.cache_dir
.as_ref()
.map(|d| PathBuf::from(d))
.unwrap_or_else(help_probe::cache::default_cache_dir),
enabled: true,
max_age_secs: Some(86400 * 7), })
};
let cfg = help_probe::ProbeConfig {
timeout_secs: cli.timeout_secs,
require_help_flag: false, cache: cache_config,
};
if cli.discover_all {
use help_probe::discover_all_subcommands;
let tree = discover_all_subcommands(program, &cfg, Some(cli.max_depth)).await?;
if cli.json {
let json = serde_json::to_string_pretty(&tree)?;
println!("{json}");
} else {
print_command_tree(&tree);
}
return Ok(());
}
let result = help_probe::probe_command(program, args, &cfg).await?;
if cli.validate {
use help_probe::validation::validate_command;
let validation_args: Vec<String> = cli.cmd[1..]
.iter()
.filter(|a| !matches!(a.as_str(), "-h" | "--help" | "--usage" | "help"))
.cloned()
.collect();
let validation_result = validate_command(&result, program, &validation_args);
if cli.json {
let json = serde_json::to_string_pretty(&validation_result)?;
println!("{json}");
} else {
print_validation_result(&validation_result);
}
if !validation_result.is_valid {
std::process::exit(1);
}
return Ok(());
}
if let Some(lang_str) = &cli.generate_builder {
use help_probe::builder::{Language, generate_command_builder};
let language = Language::from_str(lang_str).ok_or_else(|| {
anyhow::anyhow!(
"Unknown language: {}. Supported: rust, python, javascript, typescript",
lang_str
)
})?;
let builder_code = generate_command_builder(&result, language);
if let Some(output_path) = &cli.output {
write_to_file(output_path, &builder_code, cli.force)?;
} else {
print!("{builder_code}");
}
return Ok(());
}
if let Some(format_str) = &cli.generate_api_docs {
use help_probe::api_docs::{DocFormat, generate_api_docs};
let format = match format_str.to_lowercase().as_str() {
"markdown" | "md" => DocFormat::Markdown,
"html" => DocFormat::Html,
"openapi" | "swagger" => DocFormat::OpenApi,
"jsonschema" | "json-schema" | "schema" => DocFormat::JsonSchema,
_ => {
return Err(anyhow::anyhow!(
"Unknown format: {}. Supported: markdown, html, openapi, jsonschema",
format_str
));
}
};
let docs = generate_api_docs(&result, format);
if let Some(output_path) = &cli.output {
write_to_file(output_path, &docs, cli.force)?;
} else {
print!("{docs}");
}
return Ok(());
}
if let Some(shell_str) = &cli.generate_completion {
use help_probe::completion::{Shell, generate_shell_completion};
let shell = Shell::from_str(shell_str).ok_or_else(|| {
anyhow::anyhow!(
"Unknown shell: {}. Supported: bash, zsh, fish, powershell, nushell",
shell_str
)
})?;
let completion = generate_shell_completion(&result, shell);
if let Some(output_path) = &cli.output {
write_to_file(output_path, &completion, cli.force)?;
} else {
println!("{completion}");
}
return Ok(());
}
if cli.json {
let json = serde_json::to_string_pretty(&result)?;
println!("{json}");
} else {
print_human_readable(&result, cli.verbose);
}
Ok(())
}
fn print_human_readable(result: &help_probe::model::ProbeResult, verbose: bool) {
if result.timed_out {
println!("Command '{}' {:?} timed out.", result.command, result.args);
return;
}
println!("Command: {} {:?}", result.command, result.args);
println!(
"Exit code: {}",
result
.exit_code
.map(|c| c.to_string())
.unwrap_or_else(|| "<unknown>".to_string())
);
println!(
"Help flag detected in invocation: {}",
if result.help_flag_detected {
"yes"
} else {
"no"
}
);
if verbose {
println!("--- raw stdout ---");
if result.raw_stdout.is_empty() {
println!("<empty>");
} else {
println!("{}", result.raw_stdout);
}
println!("--- raw stderr ---");
if result.raw_stderr.is_empty() {
println!("<empty>");
} else {
println!("{}", result.raw_stderr);
}
}
if result.usage_blocks.is_empty() {
println!("No clear usage information found.");
} else {
println!("--- Parsed usage blocks ---");
for (i, usage) in result.usage_blocks.iter().enumerate() {
println!("(block {})", i + 1);
println!("{usage}");
println!("---------------------------");
}
}
if !result.options.is_empty() {
println!("--- Parsed options ---");
for opt in &result.options {
let shorts = if opt.short_flags.is_empty() {
String::new()
} else {
format!("short: {}", opt.short_flags.join(", "))
};
let longs = if opt.long_flags.is_empty() {
String::new()
} else {
format!("long: {}", opt.long_flags.join(", "))
};
let desc = opt.description.as_deref().unwrap_or("<no description>");
let mut metadata = Vec::new();
metadata.push(format!("type: {:?}", opt.option_type));
if opt.takes_argument {
if let Some(arg_name) = &opt.argument_name {
metadata.push(format!("arg: {}", arg_name));
} else {
metadata.push("takes arg".to_string());
}
}
if opt.required {
metadata.push("required".to_string());
}
if let Some(default) = &opt.default_value {
metadata.push(format!("default: {}", default));
}
if !opt.choices.is_empty() {
metadata.push(format!("choices: {}", opt.choices.join(", ")));
}
let meta_str = if metadata.is_empty() {
String::new()
} else {
format!(" [{}]", metadata.join(", "))
};
println!(" {} {} => {}{}", shorts, longs, desc, meta_str);
}
}
if !result.subcommands.is_empty() {
println!("--- Parsed subcommands ---");
for sc in &result.subcommands {
let desc = sc.description.as_deref().unwrap_or("<no description>");
println!(" {} => {}", sc.name, desc);
}
}
if !result.arguments.is_empty() {
println!("--- Parsed arguments ---");
for arg in &result.arguments {
let req_marker = if arg.required {
"<required>"
} else {
"[optional]"
};
let variadic_marker = if arg.variadic { "..." } else { "" };
let type_marker = arg
.arg_type
.as_ref()
.map(|t| format!(" ({:?})", t))
.unwrap_or_default();
let desc = arg.description.as_deref().unwrap_or("<no description>");
println!(
" {}{}{}{} => {}",
arg.name, variadic_marker, req_marker, type_marker, desc
);
}
}
if !result.examples.is_empty() {
println!("--- Extracted examples ---");
for example in &result.examples {
println!(" $ {}", example.command);
if let Some(desc) = &example.description {
println!(" {}", desc);
}
if !example.tags.is_empty() {
println!(" [tags: {}]", example.tags.join(", "));
}
}
}
if !result.environment_variables.is_empty() {
println!("--- Discovered environment variables ---");
for env_var in &result.environment_variables {
println!(" {}", env_var.name);
if let Some(desc) = &env_var.description {
println!(" Description: {}", desc);
}
if let Some(opt) = &env_var.option_mapped {
println!(" Maps to option: {}", opt);
}
if let Some(default) = &env_var.default_value {
println!(" Default: {}", default);
}
}
}
if !result.validation_rules.is_empty() {
println!("--- Validation rules ---");
for rule in &result.validation_rules {
println!(" {}: {:?}", rule.target, rule.rule_type);
if let Some(pattern) = &rule.pattern {
println!(" Pattern: {}", pattern);
}
if let Some(min) = rule.min {
println!(" Min: {}", min);
}
if let Some(max) = rule.max {
println!(" Max: {}", max);
}
if let Some(msg) = &rule.message {
println!(" Message: {}", msg);
}
}
}
if !verbose {
println!("\n(Note: raw stdout/stderr suppressed; use --verbose to see it.)");
}
}
fn print_command_tree(tree: &help_probe::model::CommandTree) {
println!("Command Tree: {}", tree.command);
println!("Total commands discovered: {}", tree.total_commands);
println!("\nRoot Options: {}", tree.options.len());
println!("Root Arguments: {}", tree.arguments.len());
println!("\nSubcommands:");
print_subcommands(&tree.subcommands, 0);
}
fn print_subcommands(subcommands: &[help_probe::model::SubcommandSpec], depth: usize) {
let indent = " ".repeat(depth);
for subcmd in subcommands {
println!("{}{} ({})", indent, subcmd.full_path, subcmd.name);
if let Some(desc) = &subcmd.description {
println!("{} Description: {}", indent, desc);
}
if !subcmd.options.is_empty() {
println!("{} Options: {}", indent, subcmd.options.len());
}
if !subcmd.arguments.is_empty() {
println!("{} Arguments: {}", indent, subcmd.arguments.len());
}
if !subcmd.subcommands.is_empty() {
println!("{} Subcommands:", indent);
print_subcommands(&subcmd.subcommands, depth + 1);
}
}
}
fn print_validation_result(result: &help_probe::validation::ValidationResult) {
if result.is_valid && result.warnings.is_empty() {
println!("✓ Command is valid");
return;
}
if !result.is_valid {
println!("✗ Command validation failed:\n");
for error in &result.errors {
println!(" Error: {}", error.message);
if let Some(target) = &error.target {
println!(" Target: {}", target);
}
}
}
if !result.warnings.is_empty() {
println!("\n⚠ Warnings:\n");
for warning in &result.warnings {
println!(" Warning: {}", warning.message);
if let Some(target) = &warning.target {
println!(" Target: {}", target);
}
}
}
}
fn write_to_file(path: &Path, content: &str, force: bool) -> Result<()> {
use std::fs;
if path.exists() && !force {
print!("File {:?} already exists. Overwrite? [y/N]: ", path);
io::stdout().flush()?;
let mut response = String::new();
io::stdin().read_line(&mut response)?;
let response = response.trim().to_lowercase();
if response != "y" && response != "yes" {
println!("Aborted.");
return Ok(());
}
}
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("Failed to create directory: {:?}", parent))?;
}
fs::write(path, content).with_context(|| format!("Failed to write file: {:?}", path))?;
println!("Written to {:?}", path);
Ok(())
}