Parent directory

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}