//! 接入包模板分发(基线档)。 //! //! 向项目种入 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 { 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, pub buff: crate::commands::buff::BuffStatus, } /// 应用接入包(基线档):分发标准模板 + 挂 buff(炼境登记 + git 观测/飞轮采集)。 #[tauri::command] pub fn apply_onboarding_pack( project_path: String, project_id: String, ) -> Result { 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); } }