File size: 3,037 Bytes
b2d9e47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import { readFileSync, readdirSync, existsSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";

const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, "..", "..");
const SKILLS = join(ROOT, "plugin", "skills");

const errors: string[] = [];
const MAX_LINES = 100;
const DUP_MARKER = "/plugin list";

function parseFrontmatter(text: string): Record<string, string> | null {
  if (!text.startsWith("---")) return null;
  const end = text.indexOf("\n---", 3);
  if (end === -1) return null;
  const body = text.slice(3, end);
  const out: Record<string, string> = {};
  for (const line of body.split("\n")) {
    const m = /^([a-zA-Z_-]+):\s*(.*)$/.exec(line.trim());
    if (m) out[m[1]] = m[2];
  }
  return out;
}

const dirs = readdirSync(SKILLS, { withFileTypes: true })
  .filter((e) => e.isDirectory() && !e.name.startsWith("_"))
  .map((e) => e.name);

for (const name of dirs) {
  const skillFile = join(SKILLS, name, "SKILL.md");
  if (!existsSync(skillFile)) {
    errors.push(`${name}: missing SKILL.md`);
    continue;
  }
  const text = readFileSync(skillFile, "utf8");
  const rel = `plugin/skills/${name}/SKILL.md`;

  const fm = parseFrontmatter(text);
  if (!fm) {
    errors.push(`${rel}: missing or malformed frontmatter`);
  } else {
    if (!fm.name) errors.push(`${rel}: frontmatter missing 'name'`);
    if (fm.name && fm.name !== name) errors.push(`${rel}: frontmatter name '${fm.name}' != dir '${name}'`);
    if (!fm.description) errors.push(`${rel}: frontmatter missing 'description'`);
    else {
      if (!/use when/i.test(fm.description)) errors.push(`${rel}: description must contain a "Use when ..." trigger sentence`);
      if (fm.description.length > 1024) errors.push(`${rel}: description exceeds 1024 chars`);
    }
  }

  const lines = text.split("\n").length;
  if (lines > MAX_LINES) errors.push(`${rel}: ${lines} lines (max ${MAX_LINES}); move detail into REFERENCE.md or EXAMPLES.md`);

  const body = fm ? text.slice(text.indexOf("\n---", 3) + 4) : text;
  if (body.includes(DUP_MARKER)) {
    errors.push(`${rel}: inlines the shared troubleshooting block; reference ../_shared/TROUBLESHOOTING.md instead`);
  }
}

if (!existsSync(join(SKILLS, "_shared", "TROUBLESHOOTING.md"))) {
  errors.push(`_shared/TROUBLESHOOTING.md is missing`);
}

const pluginJson = join(ROOT, "plugin", "plugin.json");
if (existsSync(pluginJson)) {
  const desc = (JSON.parse(readFileSync(pluginJson, "utf8")).description as string) ?? "";
  const m = /(\d+)\s+skills/.exec(desc);
  if (!m) errors.push(`plugin/plugin.json: description should state the skill count as "N skills"`);
  else if (Number(m[1]) !== dirs.length) {
    errors.push(`plugin/plugin.json: description says ${m[1]} skills but ${dirs.length} skill dirs exist`);
  }
}

if (errors.length) {
  console.error("Skill lint failed:");
  for (const e of errors) console.error(`  - ${e}`);
  process.exit(1);
}
console.log(`Skill lint passed: ${dirs.length} skills checked.`);