use crate::db::pool; use rusqlite::params; use serde::{Deserialize, Serialize}; // ── 数据结构 ────────────────────────────────────────────────────────────────── #[derive(Debug, Serialize, Clone)] pub struct ScannedTool { pub id: String, pub name: String, pub category: String, // editor / runtime / pkg_manager / version_manager / cli / database pub cmd: String, pub path: Option, pub version: Option, pub found: bool, } #[derive(Debug, Serialize, Deserialize)] pub struct DevToolRecord { pub id: String, pub name: String, pub category: String, pub cmd: Option, pub path: Option, pub version: Option, pub note: Option, pub env: String, pub space_id: Option, } #[derive(Debug, Deserialize)] pub struct SaveDevToolInput { pub id: String, pub name: String, pub category: String, pub cmd: Option, pub path: Option, pub version: Option, pub note: Option, pub env: Option, pub space_id: Option, } // ── 内置工具特征库 ───────────────────────────────────────────────────────────── struct ToolSpec { id: &'static str, name: &'static str, category: &'static str, cmd: &'static str, version_flag: &'static str, /// Windows 常见安装路径(fallback,当 PATH 里找不到时) fallback_paths: &'static [&'static str], } fn tool_specs() -> Vec { vec![ // 编辑器 / IDE ToolSpec { id: "cursor", name: "Cursor", category: "editor", cmd: "cursor", version_flag: "--version", fallback_paths: &[] }, ToolSpec { id: "vscode", name: "VS Code", category: "editor", cmd: "code", version_flag: "--version", fallback_paths: &[] }, ToolSpec { id: "windsurf", name: "Windsurf", category: "editor", cmd: "windsurf", version_flag: "--version", fallback_paths: &[] }, ToolSpec { id: "vim", name: "Vim", category: "editor", cmd: "vim", version_flag: "--version", fallback_paths: &[] }, ToolSpec { id: "nvim", name: "Neovim", category: "editor", cmd: "nvim", version_flag: "--version", fallback_paths: &[] }, // 语言运行时 ToolSpec { id: "node", name: "Node.js", category: "runtime", cmd: "node", version_flag: "--version", fallback_paths: &[] }, ToolSpec { id: "python", name: "Python", category: "runtime", cmd: "python", version_flag: "--version", fallback_paths: &[] }, ToolSpec { id: "python3", name: "Python 3", category: "runtime", cmd: "python3", version_flag: "--version", fallback_paths: &[] }, ToolSpec { id: "go", name: "Go", category: "runtime", cmd: "go", version_flag: "version", fallback_paths: &["C:\\Program Files\\Go\\bin\\go.exe"] }, ToolSpec { id: "rustc", name: "Rust (rustc)", category: "runtime", cmd: "rustc", version_flag: "--version", fallback_paths: &[] }, ToolSpec { id: "java", name: "Java", category: "runtime", cmd: "java", version_flag: "-version", fallback_paths: &[] }, ToolSpec { id: "ruby", name: "Ruby", category: "runtime", cmd: "ruby", version_flag: "--version", fallback_paths: &[] }, ToolSpec { id: "deno", name: "Deno", category: "runtime", cmd: "deno", version_flag: "--version", fallback_paths: &[] }, ToolSpec { id: "bun", name: "Bun", category: "runtime", cmd: "bun", version_flag: "--version", fallback_paths: &[] }, // 包管理器 ToolSpec { id: "pnpm", name: "pnpm", category: "pkg_manager", cmd: "pnpm", version_flag: "--version", fallback_paths: &[] }, ToolSpec { id: "npm", name: "npm", category: "pkg_manager", cmd: "npm", version_flag: "--version", fallback_paths: &[] }, ToolSpec { id: "yarn", name: "Yarn", category: "pkg_manager", cmd: "yarn", version_flag: "--version", fallback_paths: &[] }, ToolSpec { id: "cargo", name: "Cargo", category: "pkg_manager", cmd: "cargo", version_flag: "--version", fallback_paths: &[] }, ToolSpec { id: "pip", name: "pip", category: "pkg_manager", cmd: "pip", version_flag: "--version", fallback_paths: &[] }, ToolSpec { id: "pip3", name: "pip3", category: "pkg_manager", cmd: "pip3", version_flag: "--version", fallback_paths: &[] }, ToolSpec { id: "composer", name: "Composer", category: "pkg_manager", cmd: "composer", version_flag: "--version", fallback_paths: &[] }, // 版本管理器 ToolSpec { id: "rustup", name: "rustup", category: "version_manager", cmd: "rustup", version_flag: "--version", fallback_paths: &[] }, ToolSpec { id: "fnm", name: "fnm", category: "version_manager", cmd: "fnm", version_flag: "--version", fallback_paths: &[] }, ToolSpec { id: "volta", name: "Volta", category: "version_manager", cmd: "volta", version_flag: "--version", fallback_paths: &[] }, // CLI 工具 ToolSpec { id: "git", name: "Git", category: "cli", cmd: "git", version_flag: "--version", fallback_paths: &["C:\\Program Files\\Git\\bin\\git.exe"] }, ToolSpec { id: "claude", name: "Claude CLI", category: "cli", cmd: "claude", version_flag: "--version", fallback_paths: &[] }, ToolSpec { id: "docker", name: "Docker", category: "cli", cmd: "docker", version_flag: "--version", fallback_paths: &[] }, ToolSpec { id: "gh", name: "GitHub CLI", category: "cli", cmd: "gh", version_flag: "--version", fallback_paths: &[] }, ToolSpec { id: "kubectl", name: "kubectl", category: "cli", cmd: "kubectl", version_flag: "version", fallback_paths: &[] }, ToolSpec { id: "terraform", name: "Terraform", category: "cli", cmd: "terraform", version_flag: "version", fallback_paths: &[] }, ToolSpec { id: "make", name: "Make", category: "cli", cmd: "make", version_flag: "--version", fallback_paths: &[] }, ToolSpec { id: "az", name: "Azure CLI", category: "cli", cmd: "az", version_flag: "--version", fallback_paths: &[] }, ToolSpec { id: "aws", name: "AWS CLI", category: "cli", cmd: "aws", version_flag: "--version", fallback_paths: &[] }, // 数据库客户端 ToolSpec { id: "mysql", name: "MySQL CLI", category: "database", cmd: "mysql", version_flag: "--version", fallback_paths: &[] }, ToolSpec { id: "psql", name: "PostgreSQL CLI",category: "database", cmd: "psql", version_flag: "--version", fallback_paths: &[] }, ToolSpec { id: "redis-cli", name: "Redis CLI", category: "database", cmd: "redis-cli", version_flag: "--version", fallback_paths: &[] }, ToolSpec { id: "sqlite3", name: "SQLite", category: "database", cmd: "sqlite3", version_flag: "--version", fallback_paths: &[] }, ToolSpec { id: "mongosh", name: "MongoDB Shell", category: "database", cmd: "mongosh", version_flag: "--version", fallback_paths: &[] }, ] } // ── 辅助函数 ────────────────────────────────────────────────────────────────── /// 在 Windows PATH 里查找可执行文件路径 fn find_in_path(cmd: &str) -> Option { #[cfg(target_os = "windows")] let output = crate::silent_cmd("where").arg(cmd).output().ok()?; #[cfg(not(target_os = "windows"))] let output = crate::silent_cmd("which").arg(cmd).output().ok()?; if output.status.success() { let path = String::from_utf8_lossy(&output.stdout) .lines() .next() .map(|s| s.trim().to_string())?; if path.is_empty() { None } else { Some(path) } } else { None } } /// 尝试获取 Windows 工具版本号 fn get_version(cmd: &str, flag: &str) -> Option { let output = crate::silent_cmd(cmd).arg(flag).output().ok()?; let raw = if output.stdout.is_empty() { String::from_utf8_lossy(&output.stderr).to_string() } else { String::from_utf8_lossy(&output.stdout).to_string() }; let line = raw.lines().next()?.trim().to_string(); if line.is_empty() { None } else { Some(line.chars().take(80).collect()) } } /// 在 WSL 中查找可执行文件路径(使用登录 shell,确保 nvm 等 PATH 已加载) fn find_in_wsl(cmd: &str) -> Option { let output = crate::silent_cmd("wsl") .args(["bash", "-l", "-c", &format!("which {cmd}")]) .output() .ok()?; if output.status.success() { let path = String::from_utf8_lossy(&output.stdout).trim().to_string(); if path.is_empty() { None } else { Some(path) } } else { None } } /// 尝试获取 WSL 工具版本号(使用登录 shell) fn get_wsl_version(cmd: &str, flag: &str) -> Option { let output = crate::silent_cmd("wsl") .args(["bash", "-l", "-c", &format!("{cmd} {flag}")]) .output() .ok()?; let raw = if output.stdout.is_empty() { String::from_utf8_lossy(&output.stderr).to_string() } else { String::from_utf8_lossy(&output.stdout).to_string() }; let line = raw.lines().next()?.trim().to_string(); if line.is_empty() { None } else { Some(line.chars().take(80).collect()) } } // ── Tauri 命令 ──────────────────────────────────────────────────────────────── /// 扫描当前系统中已安装的开发工具 #[tauri::command] pub fn scan_dev_tools() -> Vec { let specs = tool_specs(); let mut result = Vec::with_capacity(specs.len()); for spec in &specs { let path = find_in_path(spec.cmd) .or_else(|| { // fallback: 检查固定路径 spec.fallback_paths.iter().find_map(|p| { if std::path::Path::new(p).exists() { Some(p.to_string()) } else { None } }) }); let found = path.is_some(); let version = if found { get_version(spec.cmd, spec.version_flag) } else { None }; result.push(ScannedTool { id: spec.id.to_string(), name: spec.name.to_string(), category: spec.category.to_string(), cmd: spec.cmd.to_string(), path, version, found, }); } result } /// 获取已保存的工具列表(按 env + space_id 过滤) #[tauri::command] pub fn get_dev_tools(env: String, space_id: Option) -> Result, String> { let conn = pool().get().map_err(|e| e.to_string())?; let rows = match &space_id { Some(sid) => { let mut stmt = conn .prepare("SELECT id, name, category, cmd, path, version, note, env, space_id FROM dev_tools WHERE env = ?1 AND space_id = ?2 ORDER BY category, name") .map_err(|e| e.to_string())?; let x = stmt .query_map(params![env, sid], |row| { Ok(DevToolRecord { id: row.get(0)?, name: row.get(1)?, category: row.get(2)?, cmd: row.get(3)?, path: row.get(4)?, version: row.get(5)?, note: row.get(6)?, env: row.get(7)?, space_id: row.get(8)?, }) }) .map_err(|e| e.to_string())? .collect::, _>>() .map_err(|e| e.to_string())?; x } None => { let mut stmt = conn .prepare("SELECT id, name, category, cmd, path, version, note, env, space_id FROM dev_tools WHERE env = ?1 AND space_id IS NULL ORDER BY category, name") .map_err(|e| e.to_string())?; let x = stmt .query_map(params![env], |row| { Ok(DevToolRecord { id: row.get(0)?, name: row.get(1)?, category: row.get(2)?, cmd: row.get(3)?, path: row.get(4)?, version: row.get(5)?, note: row.get(6)?, env: row.get(7)?, space_id: row.get(8)?, }) }) .map_err(|e| e.to_string())? .collect::, _>>() .map_err(|e| e.to_string())?; x } }; Ok(rows) } /// 批量保存选中的工具(已存在则更新) /// 当 space_id 存在时,DB id 前缀为 "{space_id}:{original_id}",保证跨空间不冲突 #[tauri::command] pub fn save_dev_tools(tools: Vec) -> Result<(), String> { let conn = pool().get().map_err(|e| e.to_string())?; for t in tools { let env = t.env.clone().unwrap_or_else(|| "windows".to_string()); let db_id = match &t.space_id { Some(sid) => format!("{}:{}", sid, t.id), None => t.id.clone(), }; conn.execute( "INSERT INTO dev_tools (id, name, category, cmd, path, version, note, env, space_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9) ON CONFLICT(id) DO UPDATE SET name = excluded.name, category = excluded.category, cmd = excluded.cmd, path = excluded.path, version = excluded.version, note = excluded.note, env = excluded.env, space_id = excluded.space_id", params![db_id, t.name, t.category, t.cmd, t.path, t.version, t.note, env, t.space_id], ) .map_err(|e| e.to_string())?; } Ok(()) } /// 删除已保存的工具 #[tauri::command] pub fn delete_dev_tool(id: String) -> Result<(), String> { let conn = pool().get().map_err(|e| e.to_string())?; conn.execute("DELETE FROM dev_tools WHERE id = ?1", params![id]) .map_err(|e| e.to_string())?; Ok(()) } /// 手动添加或更新单个工具 #[tauri::command] pub fn upsert_dev_tool(tool: SaveDevToolInput) -> Result<(), String> { save_dev_tools(vec![tool]) } /// 扫描 WSL 环境中已安装的开发工具 #[tauri::command] pub fn scan_wsl_dev_tools() -> Vec { let specs = tool_specs(); let mut result = Vec::with_capacity(specs.len()); for spec in &specs { let path = find_in_wsl(spec.cmd); let found = path.is_some(); let version = if found { get_wsl_version(spec.cmd, spec.version_flag) } else { None }; result.push(ScannedTool { id: format!("{}-wsl", spec.id), name: spec.name.to_string(), category: spec.category.to_string(), cmd: spec.cmd.to_string(), path, version, found, }); } result }