/** * 首次启动模板:优先业务项目 project-templates/,否则 kcode-pi 内置模板。 * 支持 .trellis/、.kcode/、AGENTS.md、CLAUDE.md。 */ import * as fs from "node:fs"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; export function resolveKcodePiPackageRoot(fromDir = path.dirname(fileURLToPath(import.meta.url))): string { let current = path.resolve(fromDir); while (true) { const packageJson = path.join(current, "package.json"); if (fs.existsSync(packageJson)) { try { const parsed = JSON.parse(fs.readFileSync(packageJson, "utf8")); if (parsed?.name === "kcode-pi") return current; } catch { // Keep walking; a malformed package.json should not hide a parent package. } } const parent = path.dirname(current); if (parent === current) return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); current = parent; } } const KCODE_PI_PACKAGE_ROOT = resolveKcodePiPackageRoot(); /** 批量拷贝子目录全部文件 */ function shouldSkipTemplateEntry(name: string): boolean { return name === "__pycache__" || name.endsWith(".pyc") || name.endsWith(".pyo"); } function copyDir(src: string, dest: string): string[] { const created: string[] = []; if (!fs.existsSync(src)) return created; const entries = fs.readdirSync(src, { withFileTypes: true }); for (const entry of entries) { if (shouldSkipTemplateEntry(entry.name)) continue; const s = path.join(src, entry.name); const d = path.join(dest, entry.name); if (entry.isDirectory()) { fs.mkdirSync(d, { recursive: true }); created.push(...copyDir(s, d)); } else if (entry.isFile()) { if (!fs.existsSync(d)) { fs.cpSync(s, d); created.push(d); } } } return created; } /** 从模板源拷贝 .trellis/ 到目标项目(仅不存在的文件,排除 spec/ 由 init 单独处理) */ export function initTrellisDir(cwd: string): string[] { const src = path.join(KCODE_PI_PACKAGE_ROOT, "project-templates", ".trellis"); const dest = path.join(cwd, ".trellis"); fs.mkdirSync(dest, { recursive: true }); const created: string[] = []; const entries = fs.readdirSync(src, { withFileTypes: true }); for (const entry of entries) { if (entry.name === "spec" || shouldSkipTemplateEntry(entry.name)) continue; // spec/ 由 init 按产品线选择拷贝 const s = path.join(src, entry.name); const d = path.join(dest, entry.name); if (entry.isDirectory()) { fs.mkdirSync(d, { recursive: true }); created.push(...copyDir(s, d)); } else if (entry.isFile()) { if (!fs.existsSync(d)) { fs.cpSync(s, d); created.push(d); } } } return created; } /** 从模板源拷贝 .kcode/ 到目标项目(仅不存在的文件) */ export function initKcodeDir(cwd: string): string[] { const src = path.join(KCODE_PI_PACKAGE_ROOT, "project-templates", ".kcode"); const dest = path.join(cwd, ".kcode"); fs.mkdirSync(dest, { recursive: true }); return copyDir(src, dest); } export function loadProjectTemplate(cwd: string, relativePath: string): string | undefined { const local = path.join(cwd, "project-templates", relativePath); const bundled = path.join(KCODE_PI_PACKAGE_ROOT, "project-templates", relativePath); try { return fs.readFileSync(local, "utf8"); } catch { try { return fs.readFileSync(bundled, "utf8"); } catch { return undefined; } } } export function loadAllInitTemplates(cwd: string): Record { return { rootAgents: loadProjectTemplate(cwd, "AGENTS.md"), rootClaude: loadProjectTemplate(cwd, "CLAUDE.md"), }; }