use std::process::Command;
use tracing::warn;
use super::pipeline::ParserPipeline;
use crate::models::{HelpFormat, ScannedCommand};
pub struct SubcommandDiscovery<'a> {
pipeline: &'a ParserPipeline,
max_depth: u32,
}
impl<'a> SubcommandDiscovery<'a> {
pub fn new(pipeline: &'a ParserPipeline, max_depth: u32) -> Self {
Self {
pipeline,
max_depth,
}
}
pub fn discover(
&self,
tool_name: &str,
parent_command: &[String],
subcommand_names: &[String],
depth: u32,
) -> Vec<ScannedCommand> {
if depth >= self.max_depth {
warn!(
tool = tool_name,
depth = depth,
"Max subcommand depth reached"
);
return Vec::new();
}
let mut commands = Vec::new();
for sub_name in subcommand_names {
let mut full_cmd: Vec<String> = parent_command.to_vec();
full_cmd.push(sub_name.clone());
let help_text = match self.run_help(tool_name, &full_cmd) {
Some(text) => text,
None => continue,
};
let parsed = self.pipeline.parse(&help_text, tool_name, None);
let nested = if !parsed.subcommand_names.is_empty() {
self.discover(tool_name, &full_cmd, &parsed.subcommand_names, depth + 1)
} else {
Vec::new()
};
commands.push(ScannedCommand {
name: sub_name.clone(),
full_command: full_cmd.join(" "),
description: parsed.description,
flags: parsed.flags,
positional_args: parsed.positional_args,
subcommands: nested,
examples: parsed.examples,
help_format: HelpFormat::Unknown,
structured_output: parsed.structured_output,
raw_help: help_text,
});
}
commands
}
pub fn run_help(&self, tool_name: &str, full_cmd: &[String]) -> Option<String> {
let mut args: Vec<&str> = full_cmd[1..].iter().map(|s| s.as_str()).collect();
args.push("--help");
let output = Command::new(tool_name).args(&args).output().ok()?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
if stdout.trim().is_empty() && !stderr.trim().is_empty() {
Some(stderr)
} else if !stdout.trim().is_empty() {
Some(stdout)
} else {
warn!(command = %full_cmd.join(" "), "Empty help output");
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_discovery_max_depth_zero_returns_empty() {
let pipeline = ParserPipeline::new(None);
let discovery = SubcommandDiscovery::new(&pipeline, 0);
let result = discovery.discover("tool", &["tool".into()], &["sub1".into()], 0);
assert!(result.is_empty());
}
#[test]
fn test_discovery_respects_max_depth() {
let pipeline = ParserPipeline::new(None);
let discovery = SubcommandDiscovery::new(&pipeline, 1);
let result = discovery.discover("tool", &["tool".into()], &["sub1".into()], 1);
assert!(result.is_empty());
}
#[test]
fn test_run_help_captures_stdout() {
let pipeline = ParserPipeline::new(None);
let discovery = SubcommandDiscovery::new(&pipeline, 2);
let result = discovery.run_help("echo", &["echo".into(), "hello".into()]);
assert!(result.is_some());
}
#[test]
fn test_run_help_nonexistent_tool() {
let pipeline = ParserPipeline::new(None);
let discovery = SubcommandDiscovery::new(&pipeline, 2);
let result = discovery.run_help("zzz_no_such_tool_xyz", &["zzz_no_such_tool_xyz".into()]);
assert!(result.is_none());
}
}