diff --git a/.blueprint/CONVENTIONS.md b/.blueprint/CONVENTIONS.md index 62a6174..c23caaa 100644 --- a/.blueprint/CONVENTIONS.md +++ b/.blueprint/CONVENTIONS.md @@ -1,6 +1,6 @@ --- -version: 1.4.0 -updated: 2026-04-04 +version: 1.4.2 +updated: 2026-04-08 source: dev-manager-tauri --- @@ -60,6 +60,8 @@ modules/.md 中的每个二级标题(`###`)是一张任务卡: 详细说明(可选)。 ``` +> **files 回填规则**:任务卡的 `files` 在规划时填入预期路径。实现完成后,若实际文件与规划有出入(合并、拆分、重命名),须在标记 ✅ done 之前将 `files` 更新为实际路径——这保证下次续接或派发时上下文准确。 + ### 状态前缀 | 前缀 | status | 含义 | @@ -476,3 +478,4 @@ cp -r .claude/skills/splitter ~/.claude/skills/ - 每次更新后修改 manifest.yaml 顶部的 `updated` 日期 - progress 由桌面 App 自动从任务卡完成比例计算,不需要手动维护 - 当用户的操作隐含了蓝图变更但没有明说时,Claude 应主动提议更新(而非沉默) +- **批量执行完成后须做完整性检查**:同一会话内连续完成多张任务卡时,结束前必须逐一确认:① 每张已完成的子任务 status 改为 done,前缀改为 ✅;② manifest.yaml 中对应模块的 status 已同步更新。两层缺一不可。(背景:单卡两阶段流程天然同步,批量执行时容易只改模块级 status 而漏掉子任务级更新,导致 App 看板 progress 数据失真) diff --git a/.blueprint/usage.json b/.blueprint/usage.json index 845f14c..1b84fe0 100644 --- a/.blueprint/usage.json +++ b/.blueprint/usage.json @@ -67,6 +67,29 @@ "todo": 1, "total": 135 } + }, + { + "at": "2026-04-07T01:44:05Z", + "blocked_reasons": [], + "conventions_version": "1.4.0", + "iteration": 1, + "modules": { + "blocked": 0, + "concept": 0, + "done": 33, + "in_progress": 0, + "planned": 0, + "stalled": [], + "total": 33 + }, + "tasks": { + "blocked": 0, + "dispatched": 0, + "done": 130, + "in_progress": 0, + "todo": 1, + "total": 135 + } } ] } \ No newline at end of file diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 1afed17..468480b 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -880,7 +880,7 @@ dependencies = [ [[package]] name = "dev-manager-tauri" -version = "0.1.13" +version = "0.1.14" dependencies = [ "axum", "base64 0.22.1", diff --git a/src-tauri/src/commands/blueprint.rs b/src-tauri/src/commands/blueprint.rs index 682465f..c3d0363 100644 --- a/src-tauri/src/commands/blueprint.rs +++ b/src-tauri/src/commands/blueprint.rs @@ -599,7 +599,7 @@ fn read_file_short(root: &Path, filename: &str, max_lines: usize) -> String { } fn get_git_log(root: &Path) -> String { - let output = std::process::Command::new("git") + let output = crate::silent_cmd("git") .args(["log", "--oneline", "-20"]) .current_dir(root) .output(); @@ -612,7 +612,7 @@ fn get_git_log(root: &Path) -> String { /// init 用的 git 摘要:总提交数 + 最近 15 条 fn get_git_summary(root: &Path) -> String { let run = |args: &[&str]| { - std::process::Command::new("git") + crate::silent_cmd("git") .args(args) .current_dir(root) .output() diff --git a/src-tauri/src/commands/buff.rs b/src-tauri/src/commands/buff.rs index 3272eb7..a384f63 100644 --- a/src-tauri/src/commands/buff.rs +++ b/src-tauri/src/commands/buff.rs @@ -199,12 +199,12 @@ fn get_head_commit(root: &Path) -> Result { let out = if is_wsl_path(&path_str) { let linux = unc_to_linux(&path_str) .ok_or_else(|| "无法转换 WSL 路径".to_string())?; - std::process::Command::new("wsl") + crate::silent_cmd("wsl") .args(["git", "-C", &linux, "rev-parse", "HEAD"]) .output() .map_err(|e| e.to_string())? } else { - std::process::Command::new("git") + crate::silent_cmd("git") .args(["rev-parse", "HEAD"]) .current_dir(root) .output() @@ -228,7 +228,7 @@ fn get_changed_files(root: &Path, last_commit: Option<&str>) -> Result) -> Result Vec { /// 在 Windows PATH 里查找可执行文件路径 fn find_in_path(cmd: &str) -> Option { #[cfg(target_os = "windows")] - let output = StdCommand::new("where").arg(cmd).output().ok()?; + let output = crate::silent_cmd("where").arg(cmd).output().ok()?; #[cfg(not(target_os = "windows"))] - let output = StdCommand::new("which").arg(cmd).output().ok()?; + let output = crate::silent_cmd("which").arg(cmd).output().ok()?; if output.status.success() { let path = String::from_utf8_lossy(&output.stdout) @@ -125,7 +124,7 @@ fn find_in_path(cmd: &str) -> Option { /// 尝试获取 Windows 工具版本号 fn get_version(cmd: &str, flag: &str) -> Option { - let output = StdCommand::new(cmd).arg(flag).output().ok()?; + 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 { @@ -137,7 +136,7 @@ fn get_version(cmd: &str, flag: &str) -> Option { /// 在 WSL 中查找可执行文件路径(使用登录 shell,确保 nvm 等 PATH 已加载) fn find_in_wsl(cmd: &str) -> Option { - let output = StdCommand::new("wsl") + let output = crate::silent_cmd("wsl") .args(["bash", "-l", "-c", &format!("which {cmd}")]) .output() .ok()?; @@ -151,7 +150,7 @@ fn find_in_wsl(cmd: &str) -> Option { /// 尝试获取 WSL 工具版本号(使用登录 shell) fn get_wsl_version(cmd: &str, flag: &str) -> Option { - let output = StdCommand::new("wsl") + let output = crate::silent_cmd("wsl") .args(["bash", "-l", "-c", &format!("{cmd} {flag}")]) .output() .ok()?; diff --git a/src-tauri/src/commands/mentor.rs b/src-tauri/src/commands/mentor.rs index 219eb71..b39914c 100644 --- a/src-tauri/src/commands/mentor.rs +++ b/src-tauri/src/commands/mentor.rs @@ -171,7 +171,7 @@ fn read_wsl_distro(conn: &rusqlite::Connection) -> String { /// 比 Path::exists() 对 \\wsl.localhost\... UNC 路径更可靠 /// stderr 重定向到 null,避免 WSL 网络警告打印到控制台 fn wsl_test_dir(distro: &str, linux_path: &str) -> bool { - std::process::Command::new("wsl") + crate::silent_cmd("wsl") .args(["-d", distro, "--", "test", "-d", linux_path]) .stderr(std::process::Stdio::null()) .status() @@ -195,7 +195,7 @@ fn resolve_wsl_distro(configured: &str, linux_path: &str) -> String { /// 列出系统所有已安装 WSL 发行版(解析 UTF-16 LE 输出) fn list_wsl_distros() -> Vec { - let output = match std::process::Command::new("wsl") + let output = match crate::silent_cmd("wsl") .args(["-l", "-q"]) .stderr(std::process::Stdio::null()) .output() @@ -229,7 +229,7 @@ fn list_wsl_distros() -> Vec { /// 在 WSL 内运行 git log,返回最近一条提交的单行摘要 fn wsl_git_log(distro: &str, linux_path: &str) -> Option { - let output = std::process::Command::new("wsl") + let output = crate::silent_cmd("wsl") .args(["-d", distro, "--", "git", "-C", linux_path, "log", "--oneline", "-1"]) .stderr(std::process::Stdio::null()) .output() @@ -267,7 +267,7 @@ fn unc_to_distro_and_linux(unc: &str) -> (String, String) { // ── Windows 辅助 ────────────────────────────────────────────────────────────── fn win_git_log(root: &Path) -> Option { - std::process::Command::new("git") + crate::silent_cmd("git") .args(["log", "--oneline", "-1"]) .current_dir(root) .output() @@ -283,7 +283,7 @@ fn win_git_log(root: &Path) -> Option { fn read_blueprint_summary_wsl(linux_path: &str, distro: &str) -> Option { let manifest_linux = format!("{}/.blueprint/manifest.yaml", linux_path); // 用 wsl test -f 判断文件存在(不依赖 Path::exists()) - let exists = std::process::Command::new("wsl") + let exists = crate::silent_cmd("wsl") .args(["-d", distro, "--", "test", "-f", &manifest_linux]) .stderr(std::process::Stdio::null()) .status() @@ -544,7 +544,7 @@ pub fn run_startup_health_scan() { .query_row("SELECT value FROM settings WHERE key='wsl_distro'", [], |r| r.get::<_, String>(0)) .unwrap_or_else(|_| "Ubuntu".to_string()); let manifest_linux = format!("{}/.blueprint/manifest.yaml", lp); - let exists = std::process::Command::new("wsl") + let exists = crate::silent_cmd("wsl") .args(["-d", &distro, "--", "test", "-f", &manifest_linux]) .stderr(std::process::Stdio::null()) .status() @@ -587,14 +587,14 @@ fn get_days_since_last_commit( let distro = conn .query_row("SELECT value FROM settings WHERE key='wsl_distro'", [], |r| r.get::<_, String>(0)) .unwrap_or_else(|_| "Ubuntu".to_string()); - std::process::Command::new("wsl") + crate::silent_cmd("wsl") .args(["-d", &distro, "--", "git", "-C", lp, "log", "-1", "--format=%ct"]) .stderr(std::process::Stdio::null()) .output() .ok()? } else { let root = win_path?; - std::process::Command::new("git") + crate::silent_cmd("git") .args(["log", "-1", "--format=%ct"]) .current_dir(root) .output() diff --git a/src-tauri/src/commands/network.rs b/src-tauri/src/commands/network.rs index 01f89aa..12b5798 100644 --- a/src-tauri/src/commands/network.rs +++ b/src-tauri/src/commands/network.rs @@ -3,7 +3,6 @@ use crate::db::pool; use rusqlite::params; use serde::{Deserialize, Serialize}; -use std::process::Command; use uuid::Uuid; // ── 数据结构 ────────────────────────────────────────────────────────────────── @@ -38,7 +37,7 @@ fn rule_display_name(tool_name: &str) -> String { } fn run_ps(script: &str) -> Result { - let output = Command::new("powershell") + let output = crate::silent_cmd("powershell") .args(["-NonInteractive", "-NoProfile", "-Command", script]) .output() .map_err(|e| format!("无法启动 PowerShell: {e}"))?; @@ -322,7 +321,7 @@ fn get_wsl_distro() -> String { fn run_wsl_root(cmd: &str) -> Result { let distro = get_wsl_distro(); - std::process::Command::new("wsl") + crate::silent_cmd("wsl") .args(["-d", &distro, "-u", "root", "-e", "bash", "-c", cmd]) .output() .map_err(|e| format!("WSL 不可用: {e}")) @@ -596,7 +595,7 @@ pub fn test_wsl_proxy_ip() -> Result { #[tauri::command] pub fn test_win_proxy_ip() -> Result { // 1. 读取系统代理地址 - let reg_out = std::process::Command::new("powershell") + let reg_out = crate::silent_cmd("powershell") .args(["-NonInteractive", "-NoProfile", "-Command", r#"(Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings' -ErrorAction SilentlyContinue).ProxyServer"#]) .output() @@ -607,7 +606,7 @@ pub fn test_win_proxy_ip() -> Result { } // 2. 用 curl.exe 通过代理获取 IP - let output = std::process::Command::new("curl.exe") + let output = crate::silent_cmd("curl.exe") .args([ "--proxy", &format!("http://{}", proxy_server), "-s", "--max-time", "10", diff --git a/src-tauri/src/commands/projects.rs b/src-tauri/src/commands/projects.rs index 0022836..7298d6d 100644 --- a/src-tauri/src/commands/projects.rs +++ b/src-tauri/src/commands/projects.rs @@ -332,7 +332,7 @@ pub fn delete_project(id: String, delete_local: Option) -> Result<(), Stri } // WSL 路径(通过 wsl.exe 删除) if let Some(path) = wsl_path.filter(|p| !p.is_empty()) { - let _ = std::process::Command::new("wsl") + let _ = crate::silent_cmd("wsl") .args(["--", "rm", "-rf", &path]) .output(); } diff --git a/src-tauri/src/commands/publish.rs b/src-tauri/src/commands/publish.rs index 5405acf..63b0db8 100644 --- a/src-tauri/src/commands/publish.rs +++ b/src-tauri/src/commands/publish.rs @@ -45,7 +45,7 @@ fn open_repo(path: &str) -> Result { /// 列出当前系统已安装的 WSL 发行版(wsl -l -q) #[tauri::command] pub fn get_wsl_distros() -> Result, String> { - let output = std::process::Command::new("wsl") + let output = crate::silent_cmd("wsl") .args(["-l", "-q"]) .output() .map_err(|_| "未检测到 WSL,请确认已安装 WSL 2".to_string())?; @@ -237,7 +237,7 @@ pub fn extract_linux_path(original: &str, win_path: &str) -> Option { fn run_wsl_git(linux_path: &str, git_args: &[&str]) -> Result { let mut args: Vec<&str> = vec!["git", "-C", linux_path]; args.extend_from_slice(git_args); - let output = std::process::Command::new("wsl") + let output = crate::silent_cmd("wsl") .args(&args) .output() .map_err(|e| format!("wsl 命令失败: {e}"))?; diff --git a/src-tauri/src/commands/settings.rs b/src-tauri/src/commands/settings.rs index 85a2599..2596e76 100644 --- a/src-tauri/src/commands/settings.rs +++ b/src-tauri/src/commands/settings.rs @@ -116,7 +116,7 @@ pub fn detect_editors() -> Result, String> { for (name, cmd, fallback_paths) in candidates { // 先用 where 命令查 PATH - let where_out = std::process::Command::new("cmd") + let where_out = crate::silent_cmd("cmd") .args(["/c", &format!("where {cmd} 2>nul")]) .output() .ok(); @@ -158,7 +158,7 @@ pub fn detect_editors() -> Result, String> { #[tauri::command] pub fn search_editor_path(cmd: String) -> Result, String> { // 先 where - let where_out = std::process::Command::new("cmd") + let where_out = crate::silent_cmd("cmd") .args(["/c", &format!("where {cmd} 2>nul")]) .output() .map_err(|e| e.to_string())?; diff --git a/src-tauri/src/commands/spaces.rs b/src-tauri/src/commands/spaces.rs index 566cc1b..a0ec525 100644 --- a/src-tauri/src/commands/spaces.rs +++ b/src-tauri/src/commands/spaces.rs @@ -135,11 +135,11 @@ pub fn remove_deploy_command(id: String) -> Result<(), String> { #[tauri::command] pub fn execute_deploy(command: String, run_in: String, cwd: Option) -> Result { let mut cmd = if run_in == "wsl" { - let mut c = std::process::Command::new("wsl"); + let mut c = crate::silent_cmd("wsl"); c.args(["-e", "bash", "-c", &command]); c } else { - let mut c = std::process::Command::new("cmd"); + let mut c = crate::silent_cmd("cmd"); c.args(["/c", &command]); c }; diff --git a/src-tauri/src/commands/templates.rs b/src-tauri/src/commands/templates.rs index 0518234..28a38ac 100644 --- a/src-tauri/src/commands/templates.rs +++ b/src-tauri/src/commands/templates.rs @@ -250,11 +250,11 @@ pub fn create_project_from_template( let lp = linux_project_path .as_deref() .unwrap_or(&project_win_path); - std::process::Command::new("wsl") + crate::silent_cmd("wsl") .args(["bash", "-c", &format!("cd '{}' && {}", lp, cmd)]) .output() } else { - std::process::Command::new("cmd") + crate::silent_cmd("cmd") .args(["/c", &format!("chcp 65001 > nul && {}", cmd)]) .current_dir(&project_win_path) .output() diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index 20964f0..f76bd89 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -316,6 +316,84 @@ fn migrate(conn: &rusqlite::Connection) { ").expect("migrate project_group_map FK to project_workspaces"); } + // 迁移 project_tools / activity_log / project_env_tools: + // 将外键从 projects(id) 改为 project_workspaces(id) + let pt_sql: Option = conn.query_row( + "SELECT sql FROM sqlite_master WHERE type='table' AND name='project_tools'", + [], |row| row.get(0), + ).ok(); + if pt_sql.as_deref().map(|s| s.contains("REFERENCES projects")).unwrap_or(false) { + conn.execute_batch(" + PRAGMA foreign_keys=OFF; + CREATE TABLE project_tools_new ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL REFERENCES project_workspaces(id) ON DELETE CASCADE, + name TEXT NOT NULL, + icon TEXT NOT NULL DEFAULT '🔧', + exe_path TEXT NOT NULL, + args TEXT, + sort_order INTEGER DEFAULT 0 + ); + INSERT OR IGNORE INTO project_tools_new + SELECT t.id, t.project_id, t.name, t.icon, t.exe_path, t.args, t.sort_order + FROM project_tools t + INNER JOIN project_workspaces pw ON pw.id = t.project_id; + DROP TABLE project_tools; + ALTER TABLE project_tools_new RENAME TO project_tools; + PRAGMA foreign_keys=ON; + ").expect("migrate project_tools FK to project_workspaces"); + } + + let al_sql: Option = conn.query_row( + "SELECT sql FROM sqlite_master WHERE type='table' AND name='activity_log'", + [], |row| row.get(0), + ).ok(); + if al_sql.as_deref().map(|s| s.contains("REFERENCES projects")).unwrap_or(false) { + conn.execute_batch(" + PRAGMA foreign_keys=OFF; + CREATE TABLE activity_log_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + project_id TEXT NOT NULL REFERENCES project_workspaces(id) ON DELETE CASCADE, + summary TEXT NOT NULL, + created_at TEXT DEFAULT (datetime('now')) + ); + INSERT OR IGNORE INTO activity_log_new (id, project_id, summary, created_at) + SELECT a.id, a.project_id, a.summary, a.created_at + FROM activity_log a + INNER JOIN project_workspaces pw ON pw.id = a.project_id; + DROP TABLE activity_log; + ALTER TABLE activity_log_new RENAME TO activity_log; + PRAGMA foreign_keys=ON; + ").expect("migrate activity_log FK to project_workspaces"); + } + + let pet_sql: Option = conn.query_row( + "SELECT sql FROM sqlite_master WHERE type='table' AND name='project_env_tools'", + [], |row| row.get(0), + ).ok(); + if pet_sql.as_deref().map(|s| s.contains("REFERENCES projects")).unwrap_or(false) { + conn.execute_batch(" + PRAGMA foreign_keys=OFF; + CREATE TABLE project_env_tools_new ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL REFERENCES project_workspaces(id) ON DELETE CASCADE, + layer INTEGER NOT NULL, + name TEXT NOT NULL, + source TEXT, + version TEXT, + note TEXT, + updated_at TEXT DEFAULT (datetime('now')) + ); + INSERT OR IGNORE INTO project_env_tools_new + SELECT t.id, t.project_id, t.layer, t.name, t.source, t.version, t.note, t.updated_at + FROM project_env_tools t + INNER JOIN project_workspaces pw ON pw.id = t.project_id; + DROP TABLE project_env_tools; + ALTER TABLE project_env_tools_new RENAME TO project_env_tools; + PRAGMA foreign_keys=ON; + ").expect("migrate project_env_tools FK to project_workspaces"); + } + // 从旧 projects 表迁移数据(INSERT OR IGNORE 保证幂等) let _ = conn.execute_batch(" INSERT OR IGNORE INTO project_profiles diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3788675..d84013e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -5,6 +5,19 @@ mod mcp_server; mod models; use git2; +/// 创建不弹 CMD 窗口的子进程(Windows 专用 CREATE_NO_WINDOW 标志)。 +/// 所有后台静默命令都应通过此函数创建,避免在 Windows 上频繁闪出黑框。 +pub(crate) fn silent_cmd(program: &str) -> std::process::Command { + let mut cmd = std::process::Command::new(program); + #[cfg(target_os = "windows")] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + cmd.creation_flags(CREATE_NO_WINDOW); + } + cmd +} + use commands::{activity::*, blueprint::*, buff::{apply_blueprint_buff, remove_blueprint_buff, get_buff_status, list_buffed_projects}, canvas::*, cicd::*, devtools::*, git_ops::*, git_repos::*, github::*, groups::*, mentor::{add_project_note, get_group_mentor_report, get_mentor_context, get_project_notes, get_startup_alerts, run_startup_health_scan}, network::{get_wsl_ip, sync_wsl_proxy, get_wsl_proxy_status, clear_wsl_proxy, test_wsl_proxy_ip, test_win_proxy_ip}, project_scan::*, project_tools::*, projects::{batch_create_projects, create_project, delete_project, get_project, get_projects, scan_subfolders, update_project, update_project_tags}, proxy::*, publish::*, settings::{check_mcp_health, detect_editors, get_settings, run_health_check, search_editor_path, update_settings}, shell::*, spaces::*, tags::*, templates::*}; #[cfg_attr(mobile, tauri::mobile_entry_point)]