import type { Dirent } from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; import type { SkillSource } from "./types.ts"; /** 프론트매터를 본문과 분리한다. yaml 파서를 끌어오지 않으려고 필요한 필드만 얕게 읽는다. */ export function splitFrontmatter(md: string): { fm: Record; body: string } { const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(md); if (!m) return { fm: {}, body: md }; const fm: Record = {}; for (const line of (m[1] ?? "").split(/\r?\n/)) { const kv = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line); if (!kv) continue; let v = (kv[2] ?? "").trim(); // 따옴표로 감싼 값의 따옴표만 벗긴다 if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) { v = v.slice(1, -1); } fm[kv[1] as string] = v; } return { fm, body: md.slice(m[0].length) }; } async function walk(dir: string, base = dir): Promise { const out: string[] = []; let entries: Dirent[]; try { entries = await fs.readdir(dir, { withFileTypes: true }); } catch { return out; } for (const e of entries) { const full = path.join(dir, e.name); if (e.isDirectory()) out.push(...(await walk(full, base))); else out.push(path.relative(base, full).split(path.sep).join("/")); } return out.sort(); } /** * 스킬 원본 디렉터리를 읽어 메모리에 올린다. * SKILL.md 는 필수, 나머지(references/ 등)는 전부 그대로 따라간다. */ export async function loadSkillSource(root: string, version: string): Promise { const skillMd = await fs.readFile(path.join(root, "SKILL.md"), "utf8"); const { fm, body } = splitFrontmatter(skillMd); const files = new Map(); for (const rel of await walk(root)) { if (rel === "SKILL.md") continue; if (rel.endsWith(".orig") || rel === ".designpaca_version") continue; files.set(rel, await fs.readFile(path.join(root, rel), "utf8")); } return { version, skillMd, files, body: body.trim(), description: fm["description"] ?? "웹 디자인 파이프라인 스킬", }; }