patcher.ts
2685 bytes
1import type { FrontmatterDocument, FrontmatterPatch, SkillInvocationMode } from "../types.ts";
2
3const DISABLE_KEY = "disable-model-invocation";
4const DISABLE_KEY_RE = /^\s*disable-model-invocation\s*:/;
5
6export interface FrontmatterPatcher {
7 patchInvocationMode(doc: FrontmatterDocument, desiredMode: SkillInvocationMode): FrontmatterPatch;
8}
9
10export class MinimalFrontmatterPatcher implements FrontmatterPatcher {
11 patchInvocationMode(doc: FrontmatterDocument, desiredMode: SkillInvocationMode): FrontmatterPatch {
12 if (!doc.hasFrontmatter) {
13 const newFrontmatter = desiredMode === "manual-only" ? `${DISABLE_KEY}: true${doc.lineEnding}` : "";
14 const newText = newFrontmatter
15 ? `---${doc.lineEnding}${newFrontmatter}---${doc.lineEnding}${doc.raw}`
16 : doc.raw;
17 return { oldText: doc.raw, newText };
18 }
19
20 const newFrontmatter = desiredMode === "manual-only"
21 ? ensureManualOnly(doc.frontmatterText, doc.lineEnding)
22 : ensureAgentInvocable(doc.frontmatterText);
23
24 const newText = doc.raw.slice(0, doc.frontmatterStart) + newFrontmatter + doc.raw.slice(doc.frontmatterEnd);
25 return { oldText: doc.raw, newText };
26 }
27}
28
29function ensureManualOnly(frontmatterText: string, lineEnding: "\n" | "\r\n"): string {
30 const lines = splitLinesPreserve(frontmatterText, lineEnding);
31 let replaced = false;
32 const next: string[] = [];
33
34 for (const line of lines) {
35 if (DISABLE_KEY_RE.test(stripEol(line))) {
36 if (!replaced) {
37 next.push(`${DISABLE_KEY}: true${getEol(line) || lineEnding}`);
38 replaced = true;
39 }
40 continue;
41 }
42 next.push(line);
43 }
44
45 if (!replaced) {
46 if (next.length > 0 && !endsWithEol(next[next.length - 1]!)) {
47 next[next.length - 1] = `${next[next.length - 1]}${lineEnding}`;
48 }
49 next.push(`${DISABLE_KEY}: true${lineEnding}`);
50 }
51
52 return next.join("");
53}
54
55function ensureAgentInvocable(frontmatterText: string): string {
56 return splitLinesPreserve(frontmatterText, frontmatterText.includes("\r\n") ? "\r\n" : "\n")
57 .filter((line) => !DISABLE_KEY_RE.test(stripEol(line)))
58 .join("");
59}
60
61function splitLinesPreserve(text: string, fallbackEol: "\n" | "\r\n"): string[] {
62 if (text.length === 0) return [];
63 const lines = text.match(/.*(?:\r?\n|$)/g)?.filter((line) => line.length > 0) ?? [text];
64 return lines.length > 0 ? lines : [fallbackEol];
65}
66
67function getEol(line: string): string {
68 if (line.endsWith("\r\n")) return "\r\n";
69 if (line.endsWith("\n")) return "\n";
70 return "";
71}
72
73function stripEol(line: string): string {
74 return line.replace(/\r?\n$/, "");
75}
76
77function endsWithEol(line: string): boolean {
78 return line.endsWith("\n");
79}