dispatch_templates 向项目种入 AGENTS.md/lefthook.yml/git-workflow.md 标准模板(蓝本取自 enterprise-system,已通用化),不存在才写、绝不覆盖用户已有;apply_onboarding_pack 组合模板分发 + apply_blueprint_buff 返回 OnboardingReport。含 3 内嵌模板 + 不覆盖单测 + commands.ts 封装。前端 UI 待接。
116 lines
4.3 KiB
Rust
116 lines
4.3 KiB
Rust
//! 接入包模板分发(基线档)。
|
||
//!
|
||
//! 向项目种入 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);
|
||
}
|
||
}
|