vkit 0.1.1

Fast Rust dev CLI: manage Node ports, run scripts, install & sync VS Code / Cursor extensions.
//! 读取编辑器已安装的扩展列表(`<editor> --list-extensions --show-versions`)。

use std::process::Command;

use anyhow::{anyhow, Result};

use crate::vsix::install::Editor;

/// 一个已安装扩展:id + 可选版本。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InstalledExtension {
    pub id: String,
    pub version: Option<String>,
}

/// 解析 `--show-versions` 的输出(每行形如 `publisher.name@1.2.3`)。
pub fn parse_extensions(stdout: &str) -> Vec<InstalledExtension> {
    stdout
        .lines()
        .map(str::trim)
        .filter(|line| !line.is_empty())
        .map(|line| match line.rfind('@') {
            Some(at) if at > 0 => InstalledExtension {
                id: line[..at].to_string(),
                version: Some(line[at + 1..].to_string()),
            },
            _ => InstalledExtension {
                id: line.to_string(),
                version: None,
            },
        })
        .collect()
}

/// 调用编辑器 CLI 列出已安装扩展。
pub fn list_installed(editor: Editor) -> Result<Vec<InstalledExtension>> {
    let output = Command::new(editor.bin())
        .args(["--list-extensions", "--show-versions"])
        .output()
        .map_err(|err| anyhow!("无法执行 {}:{err}", editor.bin()))?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        let stdout = String::from_utf8_lossy(&output.stdout);
        let detail = {
            let stderr = stderr.trim();
            if stderr.is_empty() {
                stdout.trim()
            } else {
                stderr
            }
        };
        return Err(anyhow!(
            "{} --list-extensions 执行失败:{}",
            editor.label(),
            if detail.is_empty() {
                "未知错误"
            } else {
                detail
            }
        ));
    }

    Ok(parse_extensions(&String::from_utf8_lossy(&output.stdout)))
}

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

    #[test]
    fn parses_id_and_version() {
        let exts = parse_extensions("ms-python.python@2024.1.0\nesbenp.prettier-vscode@10.4.0\n");
        assert_eq!(exts.len(), 2);
        assert_eq!(exts[0].id, "ms-python.python");
        assert_eq!(exts[0].version.as_deref(), Some("2024.1.0"));
    }

    #[test]
    fn tolerates_missing_version_and_blank_lines() {
        let exts = parse_extensions("  \nfoo.bar\n\n");
        assert_eq!(exts.len(), 1);
        assert_eq!(exts[0].id, "foo.bar");
        assert_eq!(exts[0].version, None);
    }
}