use super::command::{Command, CommandArg};
use anyhow::{anyhow, Result};
use once_cell::sync::Lazy;
static VAR_REGEX: Lazy<regex::Regex> =
Lazy::new(|| regex::Regex::new(r"\$\{([^}]+)\}").expect("Invalid regex pattern"));
fn validate_and_split_command(s: &str) -> Result<Vec<&str>> {
let s = s.trim();
if s.is_empty() {
return Err(anyhow!("Empty command string"));
}
let s = s.strip_prefix('/').unwrap_or(s);
let parts: Vec<&str> = s.split_whitespace().collect();
if parts.is_empty() {
return Err(anyhow!("Invalid command format"));
}
Ok(parts)
}
fn is_boolean_flag(key: &str) -> bool {
const BOOLEAN_FLAGS: &[&str] = &[
"verbose", "help", "version", "debug", "quiet", "force", "dry-run",
];
BOOLEAN_FLAGS.contains(&key)
}
fn parse_option(cmd: &mut Command, parts: &[&str], index: usize) -> usize {
let key = parts[index].trim_start_matches("--");
let has_next = index + 1 < parts.len();
let next_is_value = has_next && !parts[index + 1].starts_with("--");
if next_is_value && !is_boolean_flag(key) {
cmd.options
.insert(key.to_string(), serde_json::json!(parts[index + 1]));
index + 2
} else {
cmd.options.insert(key.to_string(), serde_json::json!(true));
index + 1
}
}
pub fn parse_command_string(s: &str) -> Result<Command> {
let parts = validate_and_split_command(s)?;
let mut cmd = Command::new(parts[0]);
parse_command_arguments(&mut cmd, &parts[1..]);
Ok(cmd)
}
fn parse_command_arguments(cmd: &mut Command, parts: &[&str]) {
let mut i = 0;
while i < parts.len() {
let part = parts[i];
if part.starts_with("--") {
i = parse_option(cmd, parts, i);
} else {
cmd.args.push(CommandArg::parse(part));
i += 1;
}
}
}
pub fn expand_variables(cmd: &mut Command, variables: &std::collections::HashMap<String, String>) {
for value in cmd.options.values_mut() {
if let Some(s) = value.as_str() {
*value = serde_json::json!(expand_string(s, variables));
}
}
let mut new_env = std::collections::HashMap::new();
for (key, value) in &cmd.metadata.env {
new_env.insert(key.clone(), expand_string(value, variables));
}
cmd.metadata.env = new_env;
}
fn expand_string(s: &str, variables: &std::collections::HashMap<String, String>) -> String {
let mut result = s.to_string();
for cap in VAR_REGEX.captures_iter(s) {
if let Some(var_name) = cap.get(1) {
if let Some(value) = variables.get(var_name.as_str()) {
result = result.replace(&cap[0], value);
}
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn test_validate_and_split_command() {
assert!(validate_and_split_command("").is_err());
assert!(validate_and_split_command(" ").is_err());
assert!(validate_and_split_command("\t\n").is_err());
let parts = validate_and_split_command("/command").unwrap();
assert_eq!(parts, vec!["command"]);
let parts = validate_and_split_command("cmd arg1 arg2 --opt").unwrap();
assert_eq!(parts, vec!["cmd", "arg1", "arg2", "--opt"]);
}
#[test]
fn test_is_boolean_flag() {
assert!(is_boolean_flag("verbose"));
assert!(is_boolean_flag("help"));
assert!(is_boolean_flag("version"));
assert!(is_boolean_flag("debug"));
assert!(is_boolean_flag("quiet"));
assert!(is_boolean_flag("force"));
assert!(is_boolean_flag("dry-run"));
assert!(!is_boolean_flag("focus"));
assert!(!is_boolean_flag("max-issues"));
assert!(!is_boolean_flag("unknown"));
}
#[test]
fn test_parse_option_boolean_flag() {
let mut cmd = Command::new("test");
let parts = vec!["cmd", "--verbose", "next"];
let next_i = parse_option(&mut cmd, &parts, 1);
assert_eq!(next_i, 2);
assert_eq!(cmd.options.get("verbose"), Some(&serde_json::json!(true)));
}
#[test]
fn test_parse_option_with_value() {
let mut cmd = Command::new("test");
let parts = vec!["cmd", "--focus", "security", "--verbose"];
let next_i = parse_option(&mut cmd, &parts, 1);
assert_eq!(next_i, 3);
assert_eq!(
cmd.options.get("focus"),
Some(&serde_json::json!("security"))
);
}
#[test]
fn test_parse_option_at_end() {
let mut cmd = Command::new("test");
let parts = vec!["cmd", "--verbose"];
let next_i = parse_option(&mut cmd, &parts, 1);
assert_eq!(next_i, 2);
assert_eq!(cmd.options.get("verbose"), Some(&serde_json::json!(true)));
}
#[test]
fn test_parse_option_before_another_option() {
let mut cmd = Command::new("test");
let parts = vec!["cmd", "--focus", "--verbose"];
let next_i = parse_option(&mut cmd, &parts, 1);
assert_eq!(next_i, 2);
assert_eq!(cmd.options.get("focus"), Some(&serde_json::json!(true)));
}
#[test]
fn test_parse_simple_command() {
let cmd = parse_command_string("prodigy-code-review").unwrap();
assert_eq!(cmd.name, "prodigy-code-review");
assert!(cmd.args.is_empty());
assert!(cmd.options.is_empty());
}
#[test]
fn test_parse_command_with_slash() {
let cmd = parse_command_string("/prodigy-lint").unwrap();
assert_eq!(cmd.name, "prodigy-lint");
}
#[test]
fn test_parse_command_with_args() {
let cmd = parse_command_string("prodigy-implement-spec iteration-123").unwrap();
assert_eq!(cmd.name, "prodigy-implement-spec");
assert_eq!(cmd.args.len(), 1);
assert_eq!(
cmd.args[0],
CommandArg::Literal("iteration-123".to_string())
);
}
#[test]
fn test_parse_command_with_options() {
let cmd = parse_command_string("prodigy-code-review --focus security --verbose").unwrap();
assert_eq!(cmd.name, "prodigy-code-review");
assert_eq!(
cmd.options.get("focus"),
Some(&serde_json::json!("security"))
);
assert_eq!(cmd.options.get("verbose"), Some(&serde_json::json!(true)));
}
#[test]
fn test_parse_command_with_variable() {
let cmd = parse_command_string("prodigy-implement-spec ${SPEC_ID}").unwrap();
assert_eq!(cmd.name, "prodigy-implement-spec");
assert_eq!(cmd.args[0], CommandArg::Variable("SPEC_ID".to_string()));
}
#[test]
fn test_expand_variables() {
let mut cmd = Command::new("prodigy-implement-spec")
.with_arg("${SPEC_ID}")
.with_option("focus", serde_json::json!("${FOCUS_AREA}"));
let mut vars = HashMap::new();
vars.insert("SPEC_ID".to_string(), "iteration-123".to_string());
vars.insert("FOCUS_AREA".to_string(), "performance".to_string());
expand_variables(&mut cmd, &vars);
assert_eq!(cmd.args[0], CommandArg::Variable("SPEC_ID".to_string()));
assert_eq!(
cmd.options.get("focus"),
Some(&serde_json::json!("performance"))
);
}
#[test]
fn test_parse_empty_command() {
assert!(parse_command_string("").is_err());
assert!(parse_command_string(" ").is_err());
}
#[test]
fn test_parse_complex_command() {
let cmd = parse_command_string(
"prodigy-code-review --focus security --max-issues 10 --verbose file1.rs file2.rs",
)
.unwrap();
assert_eq!(cmd.name, "prodigy-code-review");
assert_eq!(cmd.args.len(), 2);
assert_eq!(cmd.args[0], CommandArg::Literal("file1.rs".to_string()));
assert_eq!(cmd.args[1], CommandArg::Literal("file2.rs".to_string()));
assert_eq!(
cmd.options.get("focus"),
Some(&serde_json::json!("security"))
);
assert_eq!(
cmd.options.get("max-issues"),
Some(&serde_json::json!("10"))
);
assert_eq!(cmd.options.get("verbose"), Some(&serde_json::json!(true)));
}
#[test]
fn test_parse_command_string_simple() {
let result = parse_command_string("echo hello");
assert!(result.is_ok());
let command = result.unwrap();
assert_eq!(command.name, "echo");
assert_eq!(command.args.len(), 1);
assert_eq!(command.args[0], CommandArg::Literal("hello".to_string()));
}
#[test]
fn test_parse_command_string_with_variables() {
let result = parse_command_string("echo ${USER}");
assert!(result.is_ok());
let command = result.unwrap();
assert_eq!(command.name, "echo");
assert_eq!(command.args.len(), 1);
assert_eq!(command.args[0], CommandArg::Variable("USER".to_string()));
}
}