- 新增 silent_cmd() 辅助函数,统一添加 CREATE_NO_WINDOW 标志,全量替换 commands/ 中的裸 std::process::Command::new 调用 - 补充 project_tools / activity_log / project_env_tools 外键从 projects 迁移到 project_workspaces 的 migrate 逻辑 - CONVENTIONS.md v1.4.2:补充 files 回填规则和批量执行完整性检查规则
322 lines
16 KiB
Rust
322 lines
16 KiB
Rust
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<String>,
|
||
pub version: Option<String>,
|
||
pub found: bool,
|
||
}
|
||
|
||
#[derive(Debug, Serialize, Deserialize)]
|
||
pub struct DevToolRecord {
|
||
pub id: String,
|
||
pub name: String,
|
||
pub category: String,
|
||
pub cmd: Option<String>,
|
||
pub path: Option<String>,
|
||
pub version: Option<String>,
|
||
pub note: Option<String>,
|
||
pub env: String,
|
||
pub space_id: Option<String>,
|
||
}
|
||
|
||
#[derive(Debug, Deserialize)]
|
||
pub struct SaveDevToolInput {
|
||
pub id: String,
|
||
pub name: String,
|
||
pub category: String,
|
||
pub cmd: Option<String>,
|
||
pub path: Option<String>,
|
||
pub version: Option<String>,
|
||
pub note: Option<String>,
|
||
pub env: Option<String>,
|
||
pub space_id: Option<String>,
|
||
}
|
||
|
||
// ── 内置工具特征库 ─────────────────────────────────────────────────────────────
|
||
|
||
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<ToolSpec> {
|
||
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<String> {
|
||
#[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<String> {
|
||
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<String> {
|
||
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<String> {
|
||
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<ScannedTool> {
|
||
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<String>) -> Result<Vec<DevToolRecord>, 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::<Result<Vec<_>, _>>()
|
||
.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::<Result<Vec<_>, _>>()
|
||
.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<SaveDevToolInput>) -> 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<ScannedTool> {
|
||
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
|
||
}
|