vkit 0.1.2

Fast Rust dev CLI: manage Node ports, run scripts, install & sync VS Code / Cursor extensions.
pub mod editors;
pub mod ui;

use std::collections::HashSet;
use std::path::Path;

use anyhow::Result;
use ratatui::DefaultTerminal;

use crate::vsix::download;
use crate::vsix::install::{self, Editor};
use editors::InstalledExtension;

/// 同步方向:从 VS Code 同步到 Cursor(与 TS 版一致)。
pub const SOURCE: Editor = Editor::Code;
pub const TARGET: Editor = Editor::Cursor;

/// 一个待同步候选:源扩展 id、版本,以及目标是否已安装。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SyncCandidate {
    pub id: String,
    pub source_version: Option<String>,
    pub installed_in_target: bool,
}

/// 根据源 / 目标已装列表算出候选(去重、按 id 排序、标记目标是否已有)。
pub fn build_candidates(
    source: &[InstalledExtension],
    target: &[InstalledExtension],
) -> Vec<SyncCandidate> {
    let target_ids: HashSet<String> = target.iter().map(|e| e.id.to_lowercase()).collect();

    let mut seen: HashSet<String> = HashSet::new();
    let mut candidates: Vec<SyncCandidate> = Vec::new();
    for ext in source {
        let key = ext.id.to_lowercase();
        if !seen.insert(key.clone()) {
            continue;
        }
        candidates.push(SyncCandidate {
            id: ext.id.clone(),
            source_version: ext.version.clone(),
            installed_in_target: target_ids.contains(&key),
        });
    }
    candidates.sort_by_key(|c| c.id.to_lowercase());
    candidates
}

/// 单个扩展的同步结果。
#[derive(Debug, Clone)]
pub struct SyncItemResult {
    pub id: String,
    pub success: bool,
    pub message: String,
}

/// 同步单个扩展:下载 vsix 到临时目录,再安装到目标编辑器。
pub fn sync_extension(id: &str, target: Editor, tmp_dir: &Path) -> SyncItemResult {
    match download::resolve_and_download_to(id, Some(tmp_dir)) {
        Ok(downloaded) => {
            let result = install::install(target, &downloaded.path);
            SyncItemResult {
                id: id.to_string(),
                success: result.success,
                message: result.message,
            }
        }
        Err(err) => SyncItemResult {
            id: id.to_string(),
            success: false,
            message: err.to_string(),
        },
    }
}

/// 运行 `sync` 命令(独立入口)。
pub fn run() -> Result<()> {
    let mut terminal = ratatui::init();
    let result = ui::App::new().run(&mut terminal);
    ratatui::restore();
    result
}

/// 在 dashboard 复用的终端会话里运行 sync。
pub fn run_in_terminal(terminal: &mut DefaultTerminal) -> Result<()> {
    ui::App::new().run(terminal)
}

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

    fn ext(id: &str) -> InstalledExtension {
        InstalledExtension {
            id: id.to_string(),
            version: Some("1.0.0".to_string()),
        }
    }

    #[test]
    fn marks_target_installed_and_sorts() {
        let source = vec![ext("b.two"), ext("A.one"), ext("b.two")];
        let target = vec![ext("A.one")];
        let candidates = build_candidates(&source, &target);
        // 去重后 2 个,按 id 排序(大小写不敏感)。
        assert_eq!(candidates.len(), 2);
        assert_eq!(candidates[0].id, "A.one");
        assert!(candidates[0].installed_in_target);
        assert_eq!(candidates[1].id, "b.two");
        assert!(!candidates[1].installed_in_target);
    }
}