vtcode 0.142.0

A Rust-based terminal coding agent with modular architecture supporting multiple LLM providers
use anyhow::Result;

use super::plugin_commands::PluginCommandAction;

pub(super) fn parse_plugin_command(input: &str) -> Result<Option<PluginCommandAction>> {
    let trimmed = input.trim();

    if !trimmed.starts_with("/plugin") {
        return Ok(None);
    }

    let rest = trimmed[7..].trim();

    if rest.is_empty() {
        return Ok(Some(PluginCommandAction::Interactive));
    }

    if matches!(rest, "manager" | "--manager" | "interactive" | "--interactive") {
        return Ok(Some(PluginCommandAction::Interactive));
    }

    if matches!(rest, "list" | "--list" | "-l") {
        return Ok(Some(PluginCommandAction::List));
    }

    if matches!(rest, "help" | "--help" | "-h") {
        return Ok(Some(PluginCommandAction::Help));
    }

    if matches!(rest, "refresh" | "--refresh") {
        return Ok(Some(PluginCommandAction::Refresh));
    }

    let parts: Vec<&str> = rest.splitn(2, ' ').collect();
    match parts[0] {
        "info" | "show" | "--info" | "--show" => {
            if let Some(name) = parts.get(1) {
                Ok(Some(PluginCommandAction::Info { name: name.to_string() }))
            } else {
                Err(anyhow::anyhow!("info: plugin name required"))
            }
        }
        "add" | "--add" => {
            if let Some(rest_str) = parts.get(1) {
                let tokens: Vec<&str> = rest_str.split_whitespace().collect();
                if tokens.is_empty() {
                    return Err(anyhow::anyhow!("add: source (git URL or local path) required"));
                }
                let source = tokens[0].to_string();
                let mut name = None;
                let mut index = 1;
                while index < tokens.len() {
                    match tokens[index] {
                        "--name" | "-n" => match tokens.get(index + 1) {
                            Some(value) => {
                                name = Some(value.to_string());
                                index += 2;
                            }
                            None => return Err(anyhow::anyhow!("add: --name requires a value")),
                        },
                        other => return Err(anyhow::anyhow!("add: unexpected argument '{other}'")),
                    }
                }
                Ok(Some(PluginCommandAction::Add { source, name }))
            } else {
                Err(anyhow::anyhow!("add: source (git URL or local path) required"))
            }
        }
        "remove" | "rm" | "--remove" => {
            if let Some(name) = parts.get(1) {
                Ok(Some(PluginCommandAction::Remove { name: name.to_string() }))
            } else {
                Err(anyhow::anyhow!("remove: plugin name required"))
            }
        }
        "validate" | "--validate" => {
            if let Some(path) = parts.get(1) {
                Ok(Some(PluginCommandAction::Validate { path: std::path::PathBuf::from(path) }))
            } else {
                Err(anyhow::anyhow!("validate: plugin path required"))
            }
        }
        cmd => Err(anyhow::anyhow!("unknown plugin subcommand: {cmd}")),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn parse(input: &str) -> Result<Option<PluginCommandAction>> {
        parse_plugin_command(input)
    }

    #[test]
    fn bare_command_opens_interactive_manager() {
        assert_eq!(parse("/plugin").unwrap(), Some(PluginCommandAction::Interactive));
    }

    #[test]
    fn manager_aliases_open_interactive_manager() {
        for input in [
            "/plugin manager",
            "/plugin --manager",
            "/plugin interactive",
            "/plugin --interactive",
        ] {
            assert_eq!(parse(input).unwrap(), Some(PluginCommandAction::Interactive), "input: {input}");
        }
    }

    #[test]
    fn list_parses() {
        for input in ["/plugin list", "/plugin --list", "/plugin -l"] {
            assert_eq!(parse(input).unwrap(), Some(PluginCommandAction::List), "input: {input}");
        }
    }

    #[test]
    fn help_parses() {
        for input in ["/plugin help", "/plugin --help", "/plugin -h"] {
            assert_eq!(parse(input).unwrap(), Some(PluginCommandAction::Help), "input: {input}");
        }
    }

    #[test]
    fn refresh_parses() {
        assert_eq!(parse("/plugin refresh").unwrap(), Some(PluginCommandAction::Refresh));
        assert_eq!(parse("/plugin --refresh").unwrap(), Some(PluginCommandAction::Refresh));
    }

    #[test]
    fn info_parses() {
        for input in [
            "/plugin info my-plugin",
            "/plugin show my-plugin",
            "/plugin --info my-plugin",
        ] {
            assert_eq!(
                parse(input).unwrap(),
                Some(PluginCommandAction::Info { name: "my-plugin".to_string() }),
                "input: {input}"
            );
        }
    }

    #[test]
    fn info_requires_name() {
        assert!(parse("/plugin info").is_err());
    }

    #[test]
    fn add_parses_source_only() {
        assert_eq!(
            parse("/plugin add https://github.com/example/my-plugin.git").unwrap(),
            Some(PluginCommandAction::Add {
                source: "https://github.com/example/my-plugin.git".to_string(),
                name: None
            })
        );
    }

    #[test]
    fn add_parses_source_with_name() {
        assert_eq!(
            parse("/plugin add https://github.com/example/my-plugin.git --name my-plugin").unwrap(),
            Some(PluginCommandAction::Add {
                source: "https://github.com/example/my-plugin.git".to_string(),
                name: Some("my-plugin".to_string()),
            })
        );
    }

    #[test]
    fn add_requires_source() {
        assert!(parse("/plugin add").is_err());
    }

    #[test]
    fn remove_parses() {
        for input in [
            "/plugin remove my-plugin",
            "/plugin rm my-plugin",
            "/plugin --remove my-plugin",
        ] {
            assert_eq!(
                parse(input).unwrap(),
                Some(PluginCommandAction::Remove { name: "my-plugin".to_string() }),
                "input: {input}"
            );
        }
    }

    #[test]
    fn remove_requires_name() {
        assert!(parse("/plugin remove").is_err());
    }

    #[test]
    fn validate_parses() {
        assert_eq!(
            parse("/plugin validate ./my-plugin").unwrap(),
            Some(PluginCommandAction::Validate { path: std::path::PathBuf::from("./my-plugin") })
        );
    }

    #[test]
    fn validate_requires_path() {
        assert!(parse("/plugin validate").is_err());
    }

    #[test]
    fn unknown_subcommand_is_error() {
        assert!(parse("/plugin frobnicate").is_err());
    }

    #[test]
    fn non_plugin_command_returns_none() {
        assert_eq!(parse("/help").unwrap(), None);
        assert_eq!(parse("just a prompt").unwrap(), None);
    }
}