use crate::error::ClaudeError;
use crate::error::Result;
use std::collections::HashSet;
use std::path::Path;
use std::process::Stdio;
use tokio::io::AsyncReadExt;
use tokio::process::Command;
#[derive(Debug, Clone, Default)]
pub struct CliCapabilities {
pub flags: HashSet<String>,
}
impl CliCapabilities {
pub fn supports(&self, flag: &str) -> bool {
self.flags.contains(flag)
}
pub fn supports_all(&self, flags: &[&str]) -> bool {
flags.iter().all(|f| self.supports(f))
}
pub fn supports_any(&self, flags: &[&str]) -> bool {
flags.iter().any(|f| self.supports(f))
}
}
pub async fn probe_cli(claude_path: &Path) -> Result<CliCapabilities> {
let mut cmd = Command::new(claude_path);
cmd.arg("--help")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = cmd.spawn().map_err(|e| ClaudeError::SpawnError {
command: claude_path.display().to_string(),
args: vec!["--help".into()],
source: e,
})?;
let mut stdout_content = String::new();
if let Some(mut stdout) = child.stdout.take() {
stdout.read_to_string(&mut stdout_content).await.ok();
}
let status = child.wait().await.map_err(|e| ClaudeError::ProbeError {
message: format!("Failed to wait for --help: {}", e),
})?;
if !status.success() {
return Err(ClaudeError::ProbeError {
message: "claude --help exited with non-zero status".into(),
});
}
let flags = parse_flags_from_help(&stdout_content);
Ok(CliCapabilities { flags })
}
fn parse_flags_from_help(help_text: &str) -> HashSet<String> {
let mut flags = HashSet::new();
for line in help_text.lines() {
for token in line.split_whitespace() {
if token.starts_with("--") {
let cleaned = token
.trim_end_matches([',', ';', ')', ']'])
.trim_start_matches('[');
let flag = cleaned.split('=').next().unwrap_or(cleaned);
if !flag.is_empty() && flag.starts_with("--") {
flags.insert(flag.to_string());
}
}
}
}
flags
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_flags_from_help() {
let help_text = r#"
Usage: claude [options] [query]
Options:
--help Show help
--version Show version
--model <model> Model to use
--output-format <format> Output format (text, json, stream-json)
--permission-mode <mode> Permission mode
--dangerously-skip-permissions Skip permission checks
--allow-dangerously-skip-permissions Allow skipping permissions
--mcp-config <path> MCP configuration file
--add-dir <path> Add directory to context
--resume <id> Resume session
--continue Continue last session
--session-id <uuid> Use specific session ID
--fork-session Fork existing session
--tools <tools> Tools to enable
--allowedTools <tools> Allowed tools
--disallowedTools <tools> Disallowed tools
--json-schema <schema> JSON schema for output
--include-partial-messages Include partial messages
--replay-user-messages Replay user messages
--settings <json> Settings JSON
--setting-sources <sources> Setting sources
--plugin-dir <path> Plugin directory
--ide IDE mode
--agents <json> Agents configuration
--debug [filter] Debug mode
--verbose Verbose output
"#;
let flags = parse_flags_from_help(help_text);
assert!(flags.contains("--help"));
assert!(flags.contains("--version"));
assert!(flags.contains("--model"));
assert!(flags.contains("--permission-mode"));
assert!(flags.contains("--dangerously-skip-permissions"));
assert!(flags.contains("--allow-dangerously-skip-permissions"));
assert!(flags.contains("--mcp-config"));
assert!(flags.contains("--add-dir"));
assert!(flags.contains("--resume"));
assert!(flags.contains("--continue"));
assert!(flags.contains("--session-id"));
assert!(flags.contains("--fork-session"));
assert!(flags.contains("--tools"));
assert!(flags.contains("--allowedTools"));
assert!(flags.contains("--disallowedTools"));
assert!(flags.contains("--json-schema"));
assert!(flags.contains("--include-partial-messages"));
assert!(flags.contains("--replay-user-messages"));
assert!(flags.contains("--settings"));
assert!(flags.contains("--plugin-dir"));
assert!(flags.contains("--ide"));
assert!(flags.contains("--agents"));
assert!(flags.contains("--debug"));
assert!(flags.contains("--verbose"));
}
#[test]
fn test_cli_capabilities_supports() {
let mut caps = CliCapabilities::default();
caps.flags.insert("--help".to_string());
caps.flags.insert("--model".to_string());
caps.flags.insert("--verbose".to_string());
assert!(caps.supports("--help"));
assert!(caps.supports("--model"));
assert!(!caps.supports("--nonexistent"));
assert!(caps.supports_all(&["--help", "--model"]));
assert!(!caps.supports_all(&["--help", "--nonexistent"]));
assert!(caps.supports_any(&["--nonexistent", "--model"]));
assert!(!caps.supports_any(&["--nonexistent", "--also-nonexistent"]));
}
}