/** Matches a valid YAML frontmatter block: opening ---, content, closing --- */
const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---/;
/** agentskills.io name format: lowercase alphanumeric + hyphens, 2-64 chars */
const SKILL_NAME_RE = /^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$/;
/** Single lowercase alphanumeric character (valid 1-char skill name) */
const SINGLE_CHAR_RE = /^[a-z0-9]$/;
/** Claude Code-specific frontmatter fields that non-Claude agents don't support */
const CLAUDE_FIELD_PATTERNS = [
    /^user-invoc?k?able\s*:.*\n?/gm,
    /^allowed-tools\s*:.*\n?/gm,
    /^model\s*:.*\n?/gm,
    /^argument-hint\s*:.*\n?/gm,
    /^context\s*:.*\n?/gm,
];
/** Detects `name:` field at the start of a line in frontmatter */
const HAS_NAME_RE = /^name:/m;
/** Matches an entire `name:` line including its value (used to replace the whole line, not just the token) */
const NAME_LINE_RE = /^name:.*$/m;
/** Captures the value of the first `name:` line (used to detect idempotent renames) */
const NAME_VALUE_RE = /^name:\s*(.*)$/m;
/** Detects `description:` field at the start of a line in frontmatter */
const HAS_DESCRIPTION_RE = /^description:/m;
/** Max description length per agentskills.io standard */
const MAX_DESCRIPTION_LENGTH = 200;
/** YAML-special characters that require quoting */
const YAML_SPECIAL_RE = /[:#\[\]{}'*&!>|"\\]/;
/** YAML boolean words that need quoting when they start a value */
const YAML_BOOL_START_RE = /^(true|false|yes|no|on|off|null)\b/i;
/**
 * Quote a YAML value if it contains special characters.
 * Escapes inner backslashes and double quotes, wraps in "...".
 */
export function quoteYAMLValue(value) {
    if (!YAML_SPECIAL_RE.test(value) && !YAML_BOOL_START_RE.test(value)) {
        return value;
    }
    const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
    return `"${escaped}"`;
}
/**
 * Validate a skill name against the agentskills.io standard.
 * Must be lowercase alphanumeric with hyphens, 1-64 chars, no leading/trailing hyphens.
 */
export function validateSkillNameStrict(name) {
    if (name.length === 0 || name.length > 64)
        return false;
    if (name.length === 1)
        return SINGLE_CHAR_RE.test(name);
    return SKILL_NAME_RE.test(name);
}
/**
 * Extract a description from the body of a SKILL.md file.
 * Returns the first non-heading, non-blank line, truncated to 200 chars.
 * Falls back to a humanized version of the skill name.
 */
export function extractDescription(body, skillName) {
    const lines = body.split("\n");
    for (const line of lines) {
        const trimmed = line.trim();
        if (trimmed === "" || trimmed.startsWith("#"))
            continue;
        return trimmed.length > MAX_DESCRIPTION_LENGTH
            ? trimmed.slice(0, MAX_DESCRIPTION_LENGTH)
            : trimmed;
    }
    return skillName.replace(/-/g, " ");
}
/**
 * Ensure a SKILL.md string contains valid `name` and `description` frontmatter.
 * Pure function — normalizes CRLF, preserves existing fields, injects missing ones.
 * When `forceName` is true, overrides the existing `name` field with `skillName`.
 */
export function ensureFrontmatter(content, skillName, forceName = false) {
    const normalized = content.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
    const match = normalized.match(FRONTMATTER_RE);
    if (!match) {
        const desc = extractDescription(normalized, skillName);
        return `---\nname: ${skillName}\ndescription: ${quoteYAMLValue(desc)}\n---\n\n${normalized}`;
    }
    const fmBlock = match[1];
    const hasName = HAS_NAME_RE.test(fmBlock);
    const hasDescription = HAS_DESCRIPTION_RE.test(fmBlock);
    if (hasName && hasDescription && !forceName) {
        return normalized;
    }
    // When forceName is set, replace existing name field with the namespaced name.
    // Idempotence guard: if the existing name already equals the requested name AND
    // a description is present, this branch is a no-op — return the source unchanged
    // so installs never mutate already-valid frontmatter (the no-touch principle).
    if (forceName && hasName) {
        const currentNameMatch = fmBlock.match(NAME_VALUE_RE);
        const currentName = currentNameMatch?.[1].trim() ?? "";
        const namesMatch = currentName === skillName;
        if (namesMatch && hasDescription) {
            return normalized;
        }
        // Replace the WHOLE name line (not just the `name:` token) so the old value
        // is overwritten cleanly. The previous regex matched only `^name:` and left
        // the old value dangling, producing `name: foo foo` corruption.
        const updatedFm = namesMatch ? fmBlock : fmBlock.replace(NAME_LINE_RE, `name: ${skillName}`);
        if (hasDescription) {
            // Description already present — preserve it verbatim, only the name changed.
            return normalized.replace(FRONTMATTER_RE, `---\n${updatedFm}\n---`);
        }
        // No description in source — extract one from the body and append.
        const body = normalized.slice(match[0].length);
        const extractedDesc = extractDescription(body, skillName);
        return normalized.replace(FRONTMATTER_RE, `---\n${updatedFm}\ndescription: ${quoteYAMLValue(extractedDesc)}\n---`);
    }
    let updatedBlock = fmBlock;
    if (!hasName) {
        updatedBlock = `name: ${skillName}\n${updatedBlock}`;
    }
    if (!hasDescription) {
        const body = normalized.slice(match[0].length);
        const desc = extractDescription(body, skillName);
        updatedBlock = `${updatedBlock}\ndescription: ${quoteYAMLValue(desc)}`;
    }
    return normalized.replace(FRONTMATTER_RE, `---\n${updatedBlock}\n---`);
}
/** Detects an `author:` line in frontmatter (anchored at start of line within the block). */
const AUTHOR_LINE_RE = /^author:.*$/m;
/** Detects a `version:` line in frontmatter. */
const VERSION_LINE_RE = /^version:.*$/m;
/** Detects a `forkedFrom:` line in frontmatter (informational; the canonical record lives in .vskill-meta.json). */
const FORKED_FROM_LINE_RE = /^forkedFrom:.*$/m;
/** Captures the value after a key (used for idempotence checks). */
const AUTHOR_VALUE_RE = /^author:\s*(.*)$/m;
const VERSION_VALUE_RE = /^version:\s*(.*)$/m;
const FORKED_FROM_VALUE_RE = /^forkedFrom:\s*(.*)$/m;
/**
 * Replace the value of an existing frontmatter line, or append a new line at
 * the end of the block if the field is absent. Returns the updated block (NOT
 * the whole document) so the caller can splice it back into the original.
 */
function setOrAppendField(fmBlock, key, value, lineRe) {
    const formatted = `${key}: ${quoteYAMLValue(value)}`;
    if (lineRe.test(fmBlock)) {
        return fmBlock.replace(lineRe, formatted);
    }
    // Append. Preserve trailing newline behaviour: if fmBlock ends with \n, keep it.
    const sep = fmBlock.endsWith("\n") || fmBlock.length === 0 ? "" : "\n";
    return `${fmBlock}${sep}\n${formatted}`.replace(/\n{3,}/g, "\n\n");
}
/**
 * Apply fork-authorship metadata to a SKILL.md string.
 *
 * Rewrites only the listed frontmatter fields (`name`, `author`, `version`,
 * `forkedFrom`) via regex while preserving every other field verbatim. Matches
 * the regex house-style of this module (no gray-matter / js-yaml dependency).
 *
 * Idempotent: running twice with the same arguments produces byte-equal output.
 *
 * On malformed YAML frontmatter (no `---` opening or no closing `---`), throws
 * so the caller can abort before any disk write.
 *
 * @param content  raw SKILL.md text (CRLF normalized internally)
 * @param meta.name        new fully-qualified skill name (e.g. "anton/ado-mapper")
 * @param meta.author      new author name
 * @param meta.version     new version string (typically "1.0.0" for a fresh fork)
 * @param meta.forkedFrom  optional source skill name to record in frontmatter
 *                         (the authoritative record lives in `.vskill-meta.json`;
 *                         this is a human-readable breadcrumb only)
 */
export function applyForkMetadata(content, meta) {
    const normalized = content.replace(/^﻿/, "").replace(/\r\n/g, "\n");
    const match = normalized.match(FRONTMATTER_RE);
    if (!match) {
        throw new Error("applyForkMetadata: SKILL.md is missing a YAML frontmatter block (no opening/closing `---`).");
    }
    let fmBlock = match[1];
    // Idempotence guard: if all four target fields already match the requested
    // values verbatim, return the input unchanged so reruns produce no drift.
    const currentName = fmBlock.match(NAME_VALUE_RE)?.[1].trim();
    const currentAuthor = fmBlock.match(AUTHOR_VALUE_RE)?.[1].trim();
    const currentVersion = fmBlock.match(VERSION_VALUE_RE)?.[1].trim();
    const currentForkedFrom = fmBlock.match(FORKED_FROM_VALUE_RE)?.[1].trim();
    const expectedForkedFrom = meta.forkedFrom ? quoteYAMLValue(meta.forkedFrom) : undefined;
    if (currentName === meta.name &&
        currentAuthor === quoteYAMLValue(meta.author) &&
        currentVersion === meta.version &&
        (expectedForkedFrom === undefined || currentForkedFrom === expectedForkedFrom)) {
        return normalized;
    }
    // 1. Rewrite or insert `name:`. Use the existing line-replace pattern so the
    //    full line (not just the token) is overwritten cleanly — same shape as
    //    `ensureFrontmatter`'s force-rename branch (avoids `name: foo foo`).
    if (HAS_NAME_RE.test(fmBlock)) {
        fmBlock = fmBlock.replace(NAME_LINE_RE, `name: ${meta.name}`);
    }
    else {
        fmBlock = `name: ${meta.name}\n${fmBlock}`;
    }
    // 2. Rewrite or insert `author:`.
    fmBlock = setOrAppendField(fmBlock, "author", meta.author, AUTHOR_LINE_RE);
    // 3. Rewrite or insert `version:`.
    fmBlock = setOrAppendField(fmBlock, "version", meta.version, VERSION_LINE_RE);
    // 4. Rewrite or insert `forkedFrom:` when supplied.
    if (meta.forkedFrom !== undefined) {
        fmBlock = setOrAppendField(fmBlock, "forkedFrom", meta.forkedFrom, FORKED_FROM_LINE_RE);
    }
    // 5. Ensure description exists (extract from body if missing) — match the
    //    no-touch convention from ensureFrontmatter.
    if (!HAS_DESCRIPTION_RE.test(fmBlock)) {
        const body = normalized.slice(match[0].length);
        const desc = extractDescription(body, meta.name);
        fmBlock = `${fmBlock}\ndescription: ${quoteYAMLValue(desc)}`;
    }
    return normalized.replace(FRONTMATTER_RE, `---\n${fmBlock}\n---`);
}
/** Detects `target-agents:` field in frontmatter */
const HAS_TARGET_AGENTS_RE = /^target-agents:\s*(.*)/m;
/**
 * Parse the optional `target-agents` field from SKILL.md frontmatter.
 * Returns an array of agent IDs, or undefined if the field is absent.
 */
export function parseTargetAgents(content) {
    const normalized = content.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
    const match = normalized.match(FRONTMATTER_RE);
    if (!match)
        return undefined;
    const fmBlock = match[1];
    const agentsMatch = fmBlock.match(HAS_TARGET_AGENTS_RE);
    if (!agentsMatch)
        return undefined;
    const raw = agentsMatch[1].trim().replace(/^["']|["']$/g, "");
    const agents = raw.split(",").map((s) => s.trim()).filter(Boolean);
    return agents.length > 0 ? agents : undefined;
}
/**
 * Strip Claude Code-specific frontmatter fields for non-Claude agents.
 * Removes: user-invocable, allowed-tools, model, argument-hint, context.
 * Ensures `name:` is present (required by all agents).
 */
export function stripClaudeFields(content, skillName) {
    const normalized = content.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
    if (!normalized.startsWith("---")) {
        const desc = extractDescription(normalized, skillName);
        return `---\nname: ${skillName}\ndescription: ${quoteYAMLValue(desc)}\n---\n\n${normalized}`;
    }
    const endIdx = normalized.indexOf("---", 3);
    if (endIdx === -1)
        return normalized;
    let fmBlock = normalized.substring(3, endIdx);
    const body = normalized.substring(endIdx + 3);
    for (const pattern of CLAUDE_FIELD_PATTERNS) {
        fmBlock = fmBlock.replace(pattern, "");
    }
    // Collapse consecutive blank lines left by field removal
    fmBlock = fmBlock.replace(/\n{3,}/g, "\n\n");
    if (!HAS_NAME_RE.test(fmBlock)) {
        // Leading \n ensures separation from opening ---
        fmBlock = `\nname: ${skillName}${fmBlock}`;
    }
    return `---${fmBlock}---${body}`;
}
//# sourceMappingURL=frontmatter.js.map