dev-manager-tauri/src-tauri/src/commands/onboarding.rs
lanrtop 5f8b2e8bec feat(onboarding): 接入包模板分发 + apply_onboarding_pack 后端
dispatch_templates 向项目种入 AGENTS.md/lefthook.yml/git-workflow.md 标准模板(蓝本取自 enterprise-system,已通用化),不存在才写、绝不覆盖用户已有;apply_onboarding_pack 组合模板分发 + apply_blueprint_buff 返回 OnboardingReport。含 3 内嵌模板 + 不覆盖单测 + commands.ts 封装。前端 UI 待接。
2026-06-19 13:00:22 +09:00

116 lines
4.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 接入包模板分发(基线档)。
//!
//! 向项目种入 AGENTS.md / lefthook.yml / git-workflow.md 标准模板:
//! 文件不存在才写、绝不覆盖用户已有;返回每个模板的处理结果。
//! 模板蓝本取自 enterprise-system已通用化。
use serde::{Deserialize, Serialize};
use std::path::Path;
// 标准模板(编译期内嵌)
const TMPL_AGENTS: &str = include_str!("../../resources/onboarding/AGENTS.md.tmpl");
const TMPL_LEFTHOOK: &str = include_str!("../../resources/onboarding/lefthook.yml.tmpl");
const TMPL_GIT_WORKFLOW: &str = include_str!("../../resources/onboarding/git-workflow.md.tmpl");
/// 单个模板的分发结果。
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct TemplateResult {
/// 相对项目根的目标路径
pub name: String,
/// "written" | "skipped" | "failed"
pub action: String,
pub detail: String,
}
/// 分发单个模板:存在则跳过(绝不覆盖),不存在则写入。
fn dispatch_one(root: &Path, relative: &str, content: &str) -> TemplateResult {
let target = root.join(relative);
if target.exists() {
return TemplateResult {
name: relative.to_string(),
action: "skipped".into(),
detail: "已存在,未覆盖".into(),
};
}
if let Some(parent) = target.parent() {
if let Err(e) = std::fs::create_dir_all(parent) {
return TemplateResult {
name: relative.to_string(),
action: "failed".into(),
detail: format!("建目录失败: {e}"),
};
}
}
match std::fs::write(&target, content) {
Ok(_) => TemplateResult {
name: relative.to_string(),
action: "written".into(),
detail: "已写入".into(),
},
Err(e) => TemplateResult {
name: relative.to_string(),
action: "failed".into(),
detail: format!("写入失败: {e}"),
},
}
}
/// 向项目分发全部基线模板,返回每个的处理结果。
/// project_path 为项目根WSL 项目为 \\wsl.localhost\... UNC 路径,可直接写)。
pub fn dispatch_templates(project_path: &str) -> Vec<TemplateResult> {
let root = Path::new(project_path.trim_end_matches(['/', '\\']));
vec![
dispatch_one(root, "AGENTS.md", TMPL_AGENTS),
dispatch_one(root, "lefthook.yml", TMPL_LEFTHOOK),
dispatch_one(root, "docs/ai-context/git-workflow.md", TMPL_GIT_WORKFLOW),
]
}
/// 接入包应用报告:模板分发结果 + buff 状态。
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct OnboardingReport {
pub templates: Vec<TemplateResult>,
pub buff: crate::commands::buff::BuffStatus,
}
/// 应用接入包(基线档):分发标准模板 + 挂 buff炼境登记 + git 观测/飞轮采集)。
#[tauri::command]
pub fn apply_onboarding_pack(
project_path: String,
project_id: String,
) -> Result<OnboardingReport, String> {
let templates = dispatch_templates(&project_path);
let buff = crate::commands::buff::apply_blueprint_buff(project_path, project_id)?;
Ok(OnboardingReport { templates, buff })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn writes_then_skips_never_overwrites() {
let base = std::env::temp_dir()
.join(format!("lianjing_onboarding_test_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(&base).unwrap();
let pp = base.to_str().unwrap();
// 首次:全部写入
let r1 = dispatch_templates(pp);
assert!(r1.iter().all(|t| t.action == "written"), "首次应全部写入");
assert!(base.join("AGENTS.md").exists());
assert!(base.join("lefthook.yml").exists());
assert!(base.join("docs/ai-context/git-workflow.md").exists());
// 改一个文件,验证再次分发不覆盖
std::fs::write(base.join("AGENTS.md"), "用户自己的内容").unwrap();
let r2 = dispatch_templates(pp);
assert!(r2.iter().all(|t| t.action == "skipped"), "已存在应全部跳过");
let kept = std::fs::read_to_string(base.join("AGENTS.md")).unwrap();
assert_eq!(kept, "用户自己的内容", "绝不覆盖用户已有内容");
let _ = std::fs::remove_dir_all(&base);
}
}