fix: 消除 Windows 子进程黑框闪烁,修复 DB 外键迁移
- 新增 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 回填规则和批量执行完整性检查规则
This commit is contained in:
parent
704a1c9422
commit
0065866f6b
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
version: 1.4.0
|
version: 1.4.2
|
||||||
updated: 2026-04-04
|
updated: 2026-04-08
|
||||||
source: dev-manager-tauri
|
source: dev-manager-tauri
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -60,6 +60,8 @@ modules/<id>.md 中的每个二级标题(`###`)是一张任务卡:
|
|||||||
详细说明(可选)。
|
详细说明(可选)。
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **files 回填规则**:任务卡的 `files` 在规划时填入预期路径。实现完成后,若实际文件与规划有出入(合并、拆分、重命名),须在标记 ✅ done 之前将 `files` 更新为实际路径——这保证下次续接或派发时上下文准确。
|
||||||
|
|
||||||
### 状态前缀
|
### 状态前缀
|
||||||
|
|
||||||
| 前缀 | status | 含义 |
|
| 前缀 | status | 含义 |
|
||||||
@ -476,3 +478,4 @@ cp -r .claude/skills/splitter ~/.claude/skills/
|
|||||||
- 每次更新后修改 manifest.yaml 顶部的 `updated` 日期
|
- 每次更新后修改 manifest.yaml 顶部的 `updated` 日期
|
||||||
- progress 由桌面 App 自动从任务卡完成比例计算,不需要手动维护
|
- progress 由桌面 App 自动从任务卡完成比例计算,不需要手动维护
|
||||||
- 当用户的操作隐含了蓝图变更但没有明说时,Claude 应主动提议更新(而非沉默)
|
- 当用户的操作隐含了蓝图变更但没有明说时,Claude 应主动提议更新(而非沉默)
|
||||||
|
- **批量执行完成后须做完整性检查**:同一会话内连续完成多张任务卡时,结束前必须逐一确认:① 每张已完成的子任务 status 改为 done,前缀改为 ✅;② manifest.yaml 中对应模块的 status 已同步更新。两层缺一不可。(背景:单卡两阶段流程天然同步,批量执行时容易只改模块级 status 而漏掉子任务级更新,导致 App 看板 progress 数据失真)
|
||||||
|
|||||||
@ -67,6 +67,29 @@
|
|||||||
"todo": 1,
|
"todo": 1,
|
||||||
"total": 135
|
"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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@ -880,7 +880,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dev-manager-tauri"
|
name = "dev-manager-tauri"
|
||||||
version = "0.1.13"
|
version = "0.1.14"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
|
|||||||
@ -599,7 +599,7 @@ fn read_file_short(root: &Path, filename: &str, max_lines: usize) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_git_log(root: &Path) -> 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"])
|
.args(["log", "--oneline", "-20"])
|
||||||
.current_dir(root)
|
.current_dir(root)
|
||||||
.output();
|
.output();
|
||||||
@ -612,7 +612,7 @@ fn get_git_log(root: &Path) -> String {
|
|||||||
/// init 用的 git 摘要:总提交数 + 最近 15 条
|
/// init 用的 git 摘要:总提交数 + 最近 15 条
|
||||||
fn get_git_summary(root: &Path) -> String {
|
fn get_git_summary(root: &Path) -> String {
|
||||||
let run = |args: &[&str]| {
|
let run = |args: &[&str]| {
|
||||||
std::process::Command::new("git")
|
crate::silent_cmd("git")
|
||||||
.args(args)
|
.args(args)
|
||||||
.current_dir(root)
|
.current_dir(root)
|
||||||
.output()
|
.output()
|
||||||
|
|||||||
@ -199,12 +199,12 @@ fn get_head_commit(root: &Path) -> Result<String, String> {
|
|||||||
let out = if is_wsl_path(&path_str) {
|
let out = if is_wsl_path(&path_str) {
|
||||||
let linux = unc_to_linux(&path_str)
|
let linux = unc_to_linux(&path_str)
|
||||||
.ok_or_else(|| "无法转换 WSL 路径".to_string())?;
|
.ok_or_else(|| "无法转换 WSL 路径".to_string())?;
|
||||||
std::process::Command::new("wsl")
|
crate::silent_cmd("wsl")
|
||||||
.args(["git", "-C", &linux, "rev-parse", "HEAD"])
|
.args(["git", "-C", &linux, "rev-parse", "HEAD"])
|
||||||
.output()
|
.output()
|
||||||
.map_err(|e| e.to_string())?
|
.map_err(|e| e.to_string())?
|
||||||
} else {
|
} else {
|
||||||
std::process::Command::new("git")
|
crate::silent_cmd("git")
|
||||||
.args(["rev-parse", "HEAD"])
|
.args(["rev-parse", "HEAD"])
|
||||||
.current_dir(root)
|
.current_dir(root)
|
||||||
.output()
|
.output()
|
||||||
@ -228,7 +228,7 @@ fn get_changed_files(root: &Path, last_commit: Option<&str>) -> Result<Vec<Strin
|
|||||||
} else {
|
} else {
|
||||||
format!("git -C {linux} diff-tree --no-commit-id -r --name-only HEAD")
|
format!("git -C {linux} diff-tree --no-commit-id -r --name-only HEAD")
|
||||||
};
|
};
|
||||||
std::process::Command::new("wsl")
|
crate::silent_cmd("wsl")
|
||||||
.args(["bash", "-c", &cmd])
|
.args(["bash", "-c", &cmd])
|
||||||
.output()
|
.output()
|
||||||
.map_err(|e| e.to_string())?
|
.map_err(|e| e.to_string())?
|
||||||
@ -238,7 +238,7 @@ fn get_changed_files(root: &Path, last_commit: Option<&str>) -> Result<Vec<Strin
|
|||||||
} else {
|
} else {
|
||||||
vec!["diff-tree", "--no-commit-id", "-r", "--name-only", "HEAD"]
|
vec!["diff-tree", "--no-commit-id", "-r", "--name-only", "HEAD"]
|
||||||
};
|
};
|
||||||
std::process::Command::new("git")
|
crate::silent_cmd("git")
|
||||||
.args(&args)
|
.args(&args)
|
||||||
.current_dir(root)
|
.current_dir(root)
|
||||||
.output()
|
.output()
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
use crate::db::pool;
|
use crate::db::pool;
|
||||||
use rusqlite::params;
|
use rusqlite::params;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::process::Command as StdCommand;
|
|
||||||
|
|
||||||
// ── 数据结构 ──────────────────────────────────────────────────────────────────
|
// ── 数据结构 ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@ -108,9 +107,9 @@ fn tool_specs() -> Vec<ToolSpec> {
|
|||||||
/// 在 Windows PATH 里查找可执行文件路径
|
/// 在 Windows PATH 里查找可执行文件路径
|
||||||
fn find_in_path(cmd: &str) -> Option<String> {
|
fn find_in_path(cmd: &str) -> Option<String> {
|
||||||
#[cfg(target_os = "windows")]
|
#[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"))]
|
#[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() {
|
if output.status.success() {
|
||||||
let path = String::from_utf8_lossy(&output.stdout)
|
let path = String::from_utf8_lossy(&output.stdout)
|
||||||
@ -125,7 +124,7 @@ fn find_in_path(cmd: &str) -> Option<String> {
|
|||||||
|
|
||||||
/// 尝试获取 Windows 工具版本号
|
/// 尝试获取 Windows 工具版本号
|
||||||
fn get_version(cmd: &str, flag: &str) -> Option<String> {
|
fn get_version(cmd: &str, flag: &str) -> Option<String> {
|
||||||
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() {
|
let raw = if output.stdout.is_empty() {
|
||||||
String::from_utf8_lossy(&output.stderr).to_string()
|
String::from_utf8_lossy(&output.stderr).to_string()
|
||||||
} else {
|
} else {
|
||||||
@ -137,7 +136,7 @@ fn get_version(cmd: &str, flag: &str) -> Option<String> {
|
|||||||
|
|
||||||
/// 在 WSL 中查找可执行文件路径(使用登录 shell,确保 nvm 等 PATH 已加载)
|
/// 在 WSL 中查找可执行文件路径(使用登录 shell,确保 nvm 等 PATH 已加载)
|
||||||
fn find_in_wsl(cmd: &str) -> Option<String> {
|
fn find_in_wsl(cmd: &str) -> Option<String> {
|
||||||
let output = StdCommand::new("wsl")
|
let output = crate::silent_cmd("wsl")
|
||||||
.args(["bash", "-l", "-c", &format!("which {cmd}")])
|
.args(["bash", "-l", "-c", &format!("which {cmd}")])
|
||||||
.output()
|
.output()
|
||||||
.ok()?;
|
.ok()?;
|
||||||
@ -151,7 +150,7 @@ fn find_in_wsl(cmd: &str) -> Option<String> {
|
|||||||
|
|
||||||
/// 尝试获取 WSL 工具版本号(使用登录 shell)
|
/// 尝试获取 WSL 工具版本号(使用登录 shell)
|
||||||
fn get_wsl_version(cmd: &str, flag: &str) -> Option<String> {
|
fn get_wsl_version(cmd: &str, flag: &str) -> Option<String> {
|
||||||
let output = StdCommand::new("wsl")
|
let output = crate::silent_cmd("wsl")
|
||||||
.args(["bash", "-l", "-c", &format!("{cmd} {flag}")])
|
.args(["bash", "-l", "-c", &format!("{cmd} {flag}")])
|
||||||
.output()
|
.output()
|
||||||
.ok()?;
|
.ok()?;
|
||||||
|
|||||||
@ -171,7 +171,7 @@ fn read_wsl_distro(conn: &rusqlite::Connection) -> String {
|
|||||||
/// 比 Path::exists() 对 \\wsl.localhost\... UNC 路径更可靠
|
/// 比 Path::exists() 对 \\wsl.localhost\... UNC 路径更可靠
|
||||||
/// stderr 重定向到 null,避免 WSL 网络警告打印到控制台
|
/// stderr 重定向到 null,避免 WSL 网络警告打印到控制台
|
||||||
fn wsl_test_dir(distro: &str, linux_path: &str) -> bool {
|
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])
|
.args(["-d", distro, "--", "test", "-d", linux_path])
|
||||||
.stderr(std::process::Stdio::null())
|
.stderr(std::process::Stdio::null())
|
||||||
.status()
|
.status()
|
||||||
@ -195,7 +195,7 @@ fn resolve_wsl_distro(configured: &str, linux_path: &str) -> String {
|
|||||||
|
|
||||||
/// 列出系统所有已安装 WSL 发行版(解析 UTF-16 LE 输出)
|
/// 列出系统所有已安装 WSL 发行版(解析 UTF-16 LE 输出)
|
||||||
fn list_wsl_distros() -> Vec<String> {
|
fn list_wsl_distros() -> Vec<String> {
|
||||||
let output = match std::process::Command::new("wsl")
|
let output = match crate::silent_cmd("wsl")
|
||||||
.args(["-l", "-q"])
|
.args(["-l", "-q"])
|
||||||
.stderr(std::process::Stdio::null())
|
.stderr(std::process::Stdio::null())
|
||||||
.output()
|
.output()
|
||||||
@ -229,7 +229,7 @@ fn list_wsl_distros() -> Vec<String> {
|
|||||||
|
|
||||||
/// 在 WSL 内运行 git log,返回最近一条提交的单行摘要
|
/// 在 WSL 内运行 git log,返回最近一条提交的单行摘要
|
||||||
fn wsl_git_log(distro: &str, linux_path: &str) -> Option<String> {
|
fn wsl_git_log(distro: &str, linux_path: &str) -> Option<String> {
|
||||||
let output = std::process::Command::new("wsl")
|
let output = crate::silent_cmd("wsl")
|
||||||
.args(["-d", distro, "--", "git", "-C", linux_path, "log", "--oneline", "-1"])
|
.args(["-d", distro, "--", "git", "-C", linux_path, "log", "--oneline", "-1"])
|
||||||
.stderr(std::process::Stdio::null())
|
.stderr(std::process::Stdio::null())
|
||||||
.output()
|
.output()
|
||||||
@ -267,7 +267,7 @@ fn unc_to_distro_and_linux(unc: &str) -> (String, String) {
|
|||||||
// ── Windows 辅助 ──────────────────────────────────────────────────────────────
|
// ── Windows 辅助 ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn win_git_log(root: &Path) -> Option<String> {
|
fn win_git_log(root: &Path) -> Option<String> {
|
||||||
std::process::Command::new("git")
|
crate::silent_cmd("git")
|
||||||
.args(["log", "--oneline", "-1"])
|
.args(["log", "--oneline", "-1"])
|
||||||
.current_dir(root)
|
.current_dir(root)
|
||||||
.output()
|
.output()
|
||||||
@ -283,7 +283,7 @@ fn win_git_log(root: &Path) -> Option<String> {
|
|||||||
fn read_blueprint_summary_wsl(linux_path: &str, distro: &str) -> Option<BlueprintSummary> {
|
fn read_blueprint_summary_wsl(linux_path: &str, distro: &str) -> Option<BlueprintSummary> {
|
||||||
let manifest_linux = format!("{}/.blueprint/manifest.yaml", linux_path);
|
let manifest_linux = format!("{}/.blueprint/manifest.yaml", linux_path);
|
||||||
// 用 wsl test -f 判断文件存在(不依赖 Path::exists())
|
// 用 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])
|
.args(["-d", distro, "--", "test", "-f", &manifest_linux])
|
||||||
.stderr(std::process::Stdio::null())
|
.stderr(std::process::Stdio::null())
|
||||||
.status()
|
.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))
|
.query_row("SELECT value FROM settings WHERE key='wsl_distro'", [], |r| r.get::<_, String>(0))
|
||||||
.unwrap_or_else(|_| "Ubuntu".to_string());
|
.unwrap_or_else(|_| "Ubuntu".to_string());
|
||||||
let manifest_linux = format!("{}/.blueprint/manifest.yaml", lp);
|
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])
|
.args(["-d", &distro, "--", "test", "-f", &manifest_linux])
|
||||||
.stderr(std::process::Stdio::null())
|
.stderr(std::process::Stdio::null())
|
||||||
.status()
|
.status()
|
||||||
@ -587,14 +587,14 @@ fn get_days_since_last_commit(
|
|||||||
let distro = conn
|
let distro = conn
|
||||||
.query_row("SELECT value FROM settings WHERE key='wsl_distro'", [], |r| r.get::<_, String>(0))
|
.query_row("SELECT value FROM settings WHERE key='wsl_distro'", [], |r| r.get::<_, String>(0))
|
||||||
.unwrap_or_else(|_| "Ubuntu".to_string());
|
.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"])
|
.args(["-d", &distro, "--", "git", "-C", lp, "log", "-1", "--format=%ct"])
|
||||||
.stderr(std::process::Stdio::null())
|
.stderr(std::process::Stdio::null())
|
||||||
.output()
|
.output()
|
||||||
.ok()?
|
.ok()?
|
||||||
} else {
|
} else {
|
||||||
let root = win_path?;
|
let root = win_path?;
|
||||||
std::process::Command::new("git")
|
crate::silent_cmd("git")
|
||||||
.args(["log", "-1", "--format=%ct"])
|
.args(["log", "-1", "--format=%ct"])
|
||||||
.current_dir(root)
|
.current_dir(root)
|
||||||
.output()
|
.output()
|
||||||
|
|||||||
@ -3,7 +3,6 @@
|
|||||||
use crate::db::pool;
|
use crate::db::pool;
|
||||||
use rusqlite::params;
|
use rusqlite::params;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::process::Command;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
// ── 数据结构 ──────────────────────────────────────────────────────────────────
|
// ── 数据结构 ──────────────────────────────────────────────────────────────────
|
||||||
@ -38,7 +37,7 @@ fn rule_display_name(tool_name: &str) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn run_ps(script: &str) -> Result<String, String> {
|
fn run_ps(script: &str) -> Result<String, String> {
|
||||||
let output = Command::new("powershell")
|
let output = crate::silent_cmd("powershell")
|
||||||
.args(["-NonInteractive", "-NoProfile", "-Command", script])
|
.args(["-NonInteractive", "-NoProfile", "-Command", script])
|
||||||
.output()
|
.output()
|
||||||
.map_err(|e| format!("无法启动 PowerShell: {e}"))?;
|
.map_err(|e| format!("无法启动 PowerShell: {e}"))?;
|
||||||
@ -322,7 +321,7 @@ fn get_wsl_distro() -> String {
|
|||||||
|
|
||||||
fn run_wsl_root(cmd: &str) -> Result<std::process::Output, String> {
|
fn run_wsl_root(cmd: &str) -> Result<std::process::Output, String> {
|
||||||
let distro = get_wsl_distro();
|
let distro = get_wsl_distro();
|
||||||
std::process::Command::new("wsl")
|
crate::silent_cmd("wsl")
|
||||||
.args(["-d", &distro, "-u", "root", "-e", "bash", "-c", cmd])
|
.args(["-d", &distro, "-u", "root", "-e", "bash", "-c", cmd])
|
||||||
.output()
|
.output()
|
||||||
.map_err(|e| format!("WSL 不可用: {e}"))
|
.map_err(|e| format!("WSL 不可用: {e}"))
|
||||||
@ -596,7 +595,7 @@ pub fn test_wsl_proxy_ip() -> Result<String, String> {
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn test_win_proxy_ip() -> Result<String, String> {
|
pub fn test_win_proxy_ip() -> Result<String, String> {
|
||||||
// 1. 读取系统代理地址
|
// 1. 读取系统代理地址
|
||||||
let reg_out = std::process::Command::new("powershell")
|
let reg_out = crate::silent_cmd("powershell")
|
||||||
.args(["-NonInteractive", "-NoProfile", "-Command",
|
.args(["-NonInteractive", "-NoProfile", "-Command",
|
||||||
r#"(Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings' -ErrorAction SilentlyContinue).ProxyServer"#])
|
r#"(Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings' -ErrorAction SilentlyContinue).ProxyServer"#])
|
||||||
.output()
|
.output()
|
||||||
@ -607,7 +606,7 @@ pub fn test_win_proxy_ip() -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. 用 curl.exe 通过代理获取 IP
|
// 2. 用 curl.exe 通过代理获取 IP
|
||||||
let output = std::process::Command::new("curl.exe")
|
let output = crate::silent_cmd("curl.exe")
|
||||||
.args([
|
.args([
|
||||||
"--proxy", &format!("http://{}", proxy_server),
|
"--proxy", &format!("http://{}", proxy_server),
|
||||||
"-s", "--max-time", "10",
|
"-s", "--max-time", "10",
|
||||||
|
|||||||
@ -332,7 +332,7 @@ pub fn delete_project(id: String, delete_local: Option<bool>) -> Result<(), Stri
|
|||||||
}
|
}
|
||||||
// WSL 路径(通过 wsl.exe 删除)
|
// WSL 路径(通过 wsl.exe 删除)
|
||||||
if let Some(path) = wsl_path.filter(|p| !p.is_empty()) {
|
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])
|
.args(["--", "rm", "-rf", &path])
|
||||||
.output();
|
.output();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,7 +45,7 @@ fn open_repo(path: &str) -> Result<Repository, String> {
|
|||||||
/// 列出当前系统已安装的 WSL 发行版(wsl -l -q)
|
/// 列出当前系统已安装的 WSL 发行版(wsl -l -q)
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_wsl_distros() -> Result<Vec<String>, String> {
|
pub fn get_wsl_distros() -> Result<Vec<String>, String> {
|
||||||
let output = std::process::Command::new("wsl")
|
let output = crate::silent_cmd("wsl")
|
||||||
.args(["-l", "-q"])
|
.args(["-l", "-q"])
|
||||||
.output()
|
.output()
|
||||||
.map_err(|_| "未检测到 WSL,请确认已安装 WSL 2".to_string())?;
|
.map_err(|_| "未检测到 WSL,请确认已安装 WSL 2".to_string())?;
|
||||||
@ -237,7 +237,7 @@ pub fn extract_linux_path(original: &str, win_path: &str) -> Option<String> {
|
|||||||
fn run_wsl_git(linux_path: &str, git_args: &[&str]) -> Result<String, String> {
|
fn run_wsl_git(linux_path: &str, git_args: &[&str]) -> Result<String, String> {
|
||||||
let mut args: Vec<&str> = vec!["git", "-C", linux_path];
|
let mut args: Vec<&str> = vec!["git", "-C", linux_path];
|
||||||
args.extend_from_slice(git_args);
|
args.extend_from_slice(git_args);
|
||||||
let output = std::process::Command::new("wsl")
|
let output = crate::silent_cmd("wsl")
|
||||||
.args(&args)
|
.args(&args)
|
||||||
.output()
|
.output()
|
||||||
.map_err(|e| format!("wsl 命令失败: {e}"))?;
|
.map_err(|e| format!("wsl 命令失败: {e}"))?;
|
||||||
|
|||||||
@ -116,7 +116,7 @@ pub fn detect_editors() -> Result<Vec<Value>, String> {
|
|||||||
|
|
||||||
for (name, cmd, fallback_paths) in candidates {
|
for (name, cmd, fallback_paths) in candidates {
|
||||||
// 先用 where 命令查 PATH
|
// 先用 where 命令查 PATH
|
||||||
let where_out = std::process::Command::new("cmd")
|
let where_out = crate::silent_cmd("cmd")
|
||||||
.args(["/c", &format!("where {cmd} 2>nul")])
|
.args(["/c", &format!("where {cmd} 2>nul")])
|
||||||
.output()
|
.output()
|
||||||
.ok();
|
.ok();
|
||||||
@ -158,7 +158,7 @@ pub fn detect_editors() -> Result<Vec<Value>, String> {
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn search_editor_path(cmd: String) -> Result<Option<String>, String> {
|
pub fn search_editor_path(cmd: String) -> Result<Option<String>, String> {
|
||||||
// 先 where
|
// 先 where
|
||||||
let where_out = std::process::Command::new("cmd")
|
let where_out = crate::silent_cmd("cmd")
|
||||||
.args(["/c", &format!("where {cmd} 2>nul")])
|
.args(["/c", &format!("where {cmd} 2>nul")])
|
||||||
.output()
|
.output()
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|||||||
@ -135,11 +135,11 @@ pub fn remove_deploy_command(id: String) -> Result<(), String> {
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn execute_deploy(command: String, run_in: String, cwd: Option<String>) -> Result<String, String> {
|
pub fn execute_deploy(command: String, run_in: String, cwd: Option<String>) -> Result<String, String> {
|
||||||
let mut cmd = if run_in == "wsl" {
|
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.args(["-e", "bash", "-c", &command]);
|
||||||
c
|
c
|
||||||
} else {
|
} else {
|
||||||
let mut c = std::process::Command::new("cmd");
|
let mut c = crate::silent_cmd("cmd");
|
||||||
c.args(["/c", &command]);
|
c.args(["/c", &command]);
|
||||||
c
|
c
|
||||||
};
|
};
|
||||||
|
|||||||
@ -250,11 +250,11 @@ pub fn create_project_from_template(
|
|||||||
let lp = linux_project_path
|
let lp = linux_project_path
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.unwrap_or(&project_win_path);
|
.unwrap_or(&project_win_path);
|
||||||
std::process::Command::new("wsl")
|
crate::silent_cmd("wsl")
|
||||||
.args(["bash", "-c", &format!("cd '{}' && {}", lp, cmd)])
|
.args(["bash", "-c", &format!("cd '{}' && {}", lp, cmd)])
|
||||||
.output()
|
.output()
|
||||||
} else {
|
} else {
|
||||||
std::process::Command::new("cmd")
|
crate::silent_cmd("cmd")
|
||||||
.args(["/c", &format!("chcp 65001 > nul && {}", cmd)])
|
.args(["/c", &format!("chcp 65001 > nul && {}", cmd)])
|
||||||
.current_dir(&project_win_path)
|
.current_dir(&project_win_path)
|
||||||
.output()
|
.output()
|
||||||
|
|||||||
@ -316,6 +316,84 @@ fn migrate(conn: &rusqlite::Connection) {
|
|||||||
").expect("migrate project_group_map FK to project_workspaces");
|
").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<String> = 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<String> = 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<String> = 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 保证幂等)
|
// 从旧 projects 表迁移数据(INSERT OR IGNORE 保证幂等)
|
||||||
let _ = conn.execute_batch("
|
let _ = conn.execute_batch("
|
||||||
INSERT OR IGNORE INTO project_profiles
|
INSERT OR IGNORE INTO project_profiles
|
||||||
|
|||||||
@ -5,6 +5,19 @@ mod mcp_server;
|
|||||||
mod models;
|
mod models;
|
||||||
use git2;
|
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::*};
|
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)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user