#!/usr/bin/env node
// Task 621 — bundle-time frontmatter check for every SKILL.md across the
// plugins trees.
//
// Why: Claude Code surfaces a skill in the session's available-skills reminder,
// and makes it matchable by the description-driven search tools (ToolSearch,
// skill-find), ONLY if its SKILL.md opens with YAML frontmatter carrying a
// `name:` and a `description:`. A SKILL.md that opens straight into an `#` H1
// has no description for the reminder to list and nothing for fuzzy search to
// match — it stays invisible until an operator finds the directory by hand,
// while the agent burns turns on blind find/grep. The skill remains invokable
// by exact name, so the defect is silent: no error, no log line. This module
// is the standing guard that fails the bundle before such a payload ships, so
// the next frontmatter-less skill cannot regress discovery the same way.
//
// A file passes iff it opens with a `---\n...\n---` frontmatter block whose
// keys include both `name:` and `description:`. Three failure shapes are
// reported distinctly: no frontmatter block at all, missing `name:`, missing
// `description:`.

import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
import { resolve, relative, join, dirname } from "node:path";
import { pathToFileURL } from "node:url";

// Walk `dir` recursively, collecting every file whose basename matches `predicate`.
function walk(dir, predicate, out = []) {
  if (!existsSync(dir)) return out;
  let entries;
  try {
    entries = readdirSync(dir);
  } catch {
    return out;
  }
  for (const entry of entries) {
    const full = join(dir, entry);
    let st;
    try {
      st = statSync(full);
    } catch {
      continue;
    }
    if (st.isDirectory()) {
      walk(full, predicate, out);
    } else if (predicate(entry, full)) {
      out.push(full);
    }
  }
  return out;
}

// Extract the leading frontmatter block. A block exists only when the very
// first line is `---` and a closing `---` follows on its own line. The closing
// fence is anchored to end-of-line (`\r?\n|$`) so a body line that merely
// starts with `---` (a `---trailing` divider) cannot be mistaken for the
// close, and a frontmatter-only file ending exactly at `---` still matches.
// Returns the block body (between the fences) or null when there is no block.
function frontmatterBody(raw) {
  const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
  return m ? m[1] : null;
}

// True if the block contains a `<key>:` line at the start of any line. The key
// may be followed by a value, a block scalar (`|`/`>`), or nothing.
function hasKey(block, key) {
  const re = new RegExp(`^${key}:`, "m");
  return re.test(block);
}

function scanFile(filePath, repoRoot) {
  let raw;
  try {
    raw = readFileSync(filePath, "utf-8");
  } catch {
    return null;
  }
  const source = relative(repoRoot, filePath);
  const block = frontmatterBody(raw);
  if (block === null) {
    return { source, reason: "no YAML frontmatter block (file opens without a leading `---`)" };
  }
  const missing = [];
  if (!hasKey(block, "name")) missing.push("name");
  if (!hasKey(block, "description")) missing.push("description");
  if (missing.length > 0) {
    return { source, reason: `frontmatter missing required key(s): ${missing.join(", ")}` };
  }
  return null;
}

export function runSkillFrontmatterCheck({ platformRoot, premiumRoot }) {
  const repoRoot = resolve(platformRoot, "..");
  const sources = [];

  // platform: every SKILL.md under plugins/
  walk(resolve(platformRoot, "plugins"), (name) => name === "SKILL.md", sources);

  // premium: every SKILL.md under any bundle (standalone or sub)
  if (premiumRoot && existsSync(premiumRoot)) {
    walk(premiumRoot, (name) => name === "SKILL.md", sources);
  }

  const violations = [];
  for (const src of sources) {
    const v = scanFile(src, repoRoot);
    if (v) violations.push(v);
  }

  return {
    ok: violations.length === 0,
    violations,
    scanned: sources.length,
  };
}

// CLI entry — `node check-skill-frontmatter.mjs [platformRoot] [premiumRoot]`.
// pathToFileURL keeps the self-detect correct across paths with spaces or
// non-ASCII characters. When loaded via `import` from bundle.js the URLs
// differ and this block is skipped.
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  const defaultPlatform = resolve(import.meta.dirname, "..");
  const defaultPremium = resolve(import.meta.dirname, "..", "..", "premium-plugins");
  const platformRoot = process.argv[2] ? resolve(process.argv[2]) : defaultPlatform;
  const premiumRoot = process.argv[3] ? resolve(process.argv[3]) : defaultPremium;
  const r = runSkillFrontmatterCheck({ platformRoot, premiumRoot });
  if (!r.ok) {
    for (const v of r.violations) {
      console.error(`[bundle-validator] skill frontmatter missing`);
      console.error(`  source: ${v.source}`);
      console.error(`  reason: ${v.reason}`);
    }
    console.error(
      `FATAL: ${r.violations.length} SKILL.md file(s) without name/description frontmatter across ${r.scanned} scanned file(s).`,
    );
    process.exit(1);
  }
  console.log(
    `[bundle-validator] skill frontmatter ok scanned=${r.scanned}`,
  );
}
