vkit 0.1.4

Fast Rust dev CLI: manage git worktrees, Node ports, run scripts, install & sync VS Code / Cursor extensions.
//! 把 vsix 安装到已安装的编辑器(Cursor / VS Code)。

use std::path::Path;
use std::process::Command;

/// 支持的编辑器 CLI。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Editor {
    Cursor,
    Code,
}

impl Editor {
    pub fn bin(self) -> &'static str {
        match self {
            Editor::Cursor => "cursor",
            Editor::Code => "code",
        }
    }

    pub fn label(self) -> &'static str {
        match self {
            Editor::Cursor => "Cursor",
            Editor::Code => "VS Code",
        }
    }
}

/// 探测 PATH 中可用的编辑器 CLI(保持 Cursor、VS Code 顺序)。
pub fn find_editors() -> Vec<Editor> {
    [Editor::Cursor, Editor::Code]
        .into_iter()
        .filter(|editor| which::which(editor.bin()).is_ok())
        .collect()
}

/// 单个编辑器的安装结果。
pub struct InstallResult {
    pub editor: Editor,
    pub success: bool,
    pub message: String,
}

/// 调用 `<editor> --install-extension <vsix>` 安装。
pub fn install(editor: Editor, vsix_path: &Path) -> InstallResult {
    let output = Command::new(editor.bin())
        .arg("--install-extension")
        .arg(vsix_path)
        .output();

    match output {
        Ok(output) if output.status.success() => InstallResult {
            editor,
            success: true,
            message: format!("已安装到 {}", editor.label()),
        },
        Ok(output) => {
            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
                }
            };
            InstallResult {
                editor,
                success: false,
                message: if detail.is_empty() {
                    "安装失败".to_string()
                } else {
                    detail.to_string()
                },
            }
        }
        Err(err) => InstallResult {
            editor,
            success: false,
            message: format!("无法执行 {}{err}", editor.bin()),
        },
    }
}

/// 依次安装到多个编辑器,汇总一条结果信息。
pub fn install_to_editors(editors: &[Editor], vsix_path: &Path) -> (bool, String) {
    let results: Vec<InstallResult> = editors
        .iter()
        .map(|editor| install(*editor, vsix_path))
        .collect();

    let succeeded: Vec<&InstallResult> = results.iter().filter(|r| r.success).collect();
    let failed: Vec<&InstallResult> = results.iter().filter(|r| !r.success).collect();

    if failed.is_empty() {
        let names = succeeded
            .iter()
            .map(|r| r.editor.label())
            .collect::<Vec<_>>()
            .join("");
        return (true, format!("已安装到 {names}"));
    }

    if succeeded.is_empty() {
        let detail = failed
            .iter()
            .map(|r| format!("{}{}", r.editor.label(), r.message))
            .collect::<Vec<_>>()
            .join("");
        return (false, detail);
    }

    let ok_names = succeeded
        .iter()
        .map(|r| r.editor.label())
        .collect::<Vec<_>>()
        .join("");
    let fail_detail = failed
        .iter()
        .map(|r| format!("{} 失败:{}", r.editor.label(), r.message))
        .collect::<Vec<_>>()
        .join("");
    (false, format!("已安装到 {ok_names}{fail_detail}"))
}