/**
 * install/transforms/_shared.mjs — pure helpers shared by every per-runtime
 * transform module. Zero filesystem access — exported functions take strings
 * in and return strings out.
 *
 * The transforms layer turns a canonical SKILL.md / agent.md / hook.{js,sh}
 * into runtime-specific output bytes. Each runtime module composes these
 * helpers (e.g. codex strips `tools` + `color`, gemini escapes ${var}, etc.).
 *
 * Why a hand-rolled frontmatter splitter instead of the `yaml` library for
 * the PARSE step: we want to preserve exact ordering, comments, and
 * formatting in the body. We DO use `yaml` for re-serialization when a
 * transform mutates frontmatter (strip fields, etc.) — that path accepts
 * shape changes.
 */
import { basename, dirname } from 'node:path';
import yaml from 'yaml';
/**
 * @typedef {Object} ParsedFile
 * @property {Record<string, unknown>|null} fm  parsed frontmatter (null if none)
 * @property {string} body                       the post-frontmatter content
 * @property {string} eol                        '\n' or '\r\n' (preserved on round-trip)
 * @property {boolean} hasFrontmatter
 */
/**
 * @typedef {'skill'|'agent'|'hook-js'|'hook-sh'|'unknown'} FileType
 */
/**
 * Color-name → hex map. Covers every color name that appears in canonical
 * frontmatter today (cyan, magenta, blue, green, yellow, red) plus the
 * common extras (orange, purple, pink, gray, white, black) for forward
 * compatibility.
 *
 * Hex values are taken from Tailwind 3 default palette (500 shade) which
 * matches GSD's canonical color map. Runtimes that need hex (OpenCode,
 * Kilo) consume this directly.
 */
export const COLOR_NAME_TO_HEX = Object.freeze({
    cyan: '#06b6d4',
    magenta: '#d946ef',
    blue: '#3b82f6',
    green: '#22c55e',
    yellow: '#eab308',
    red: '#ef4444',
    orange: '#f97316',
    purple: '#a855f7',
    pink: '#ec4899',
    gray: '#6b7280',
    grey: '#6b7280',
    white: '#ffffff',
    black: '#000000',
});
/**
 * Convert a color name to a 7-char hex string. Pass-through if the input
 * is already a hex value (starts with `#`). Returns null for unknown names
 * so callers can decide whether to drop the field or default it.
 *
 * @param {string|null|undefined} name
 * @returns {string|null}
 */
export function colorNameToHex(name) {
    if (!name || typeof name !== 'string')
        return null;
    if (name.startsWith('#'))
        return name;
    return COLOR_NAME_TO_HEX[name.toLowerCase()] ?? null;
}
/**
 * Split content into frontmatter + body. Preserves the exact body bytes
 * (including the trailing leading-newline that follows the closing `---`).
 *
 * Returns `{ fm: null, body: content, hasFrontmatter: false, eol: '\n' }`
 * when there is no `---\n` opener.
 *
 * @param {string} content
 * @returns {ParsedFile}
 */
export function parseFrontmatter(content) {
    const eol = /\r\n/.test(content) ? '\r\n' : '\n';
    if (!content.startsWith('---' + eol) &&
        !content.startsWith('---\n') &&
        !content.startsWith('---\r\n')) {
        return { fm: null, body: content, hasFrontmatter: false, eol };
    }
    // Find closing `---` on its own line.
    const lines = content.split(/\r?\n/);
    let endIdx = -1;
    for (let i = 1; i < lines.length; i++) {
        if (lines[i] === '---') {
            endIdx = i;
            break;
        }
    }
    if (endIdx === -1) {
        return { fm: null, body: content, hasFrontmatter: false, eol };
    }
    const fmText = lines.slice(1, endIdx).join('\n');
    const body = lines.slice(endIdx + 1).join(eol);
    let fm;
    try {
        fm = yaml.parse(fmText) ?? {};
    }
    catch {
        // Malformed frontmatter — treat as bodyless.
        return { fm: null, body: content, hasFrontmatter: false, eol };
    }
    if (typeof fm !== 'object' || Array.isArray(fm)) {
        return { fm: null, body: content, hasFrontmatter: false, eol };
    }
    return { fm: /** @type {Record<string, unknown>} */ (fm), body, hasFrontmatter: true, eol };
}
/**
 * Serialize a parsed file back to a string. Drops keys whose value is
 * `undefined` so that `delete fm.tools; serialize(...)` produces clean
 * output.
 *
 * @param {ParsedFile} parsed
 * @returns {string}
 */
export function serializeFrontmatter(parsed) {
    const { fm, body, hasFrontmatter, eol } = parsed;
    if (!hasFrontmatter || !fm)
        return body;
    const cleaned = /** @type {Record<string, unknown>} */ ({});
    for (const [k, v] of Object.entries(fm)) {
        if (v !== undefined)
            cleaned[k] = v;
    }
    const fmText = yaml
        .stringify(cleaned, { lineWidth: 0, defaultStringType: 'PLAIN' })
        .replace(/\r?\n$/, ''); // yaml adds a trailing newline; we add it back below
    return `---${eol}${fmText}${eol}---${eol}${body}`;
}
/**
 * Drop an arbitrary set of fields from the frontmatter object. Returns the
 * same object (mutated) for callers that want to chain.
 *
 * @param {Record<string, unknown>|null|undefined} fm
 * @param {string[]} fieldNames
 * @returns {Record<string, unknown>}
 */
export function stripFields(fm, fieldNames) {
    if (!fm)
        return {};
    for (const f of fieldNames) {
        delete fm[f];
    }
    return fm;
}
/**
 * Replace the literal `{{APT_VERSION}}` token with a real semver string.
 * Used by every transform after frontmatter mutation, and by the canonical
 * file injector when it needs to substitute at install time.
 *
 * @param {string} content
 * @param {string} version
 * @returns {string}
 */
export function substituteVersion(content, version) {
    if (!content)
        return content;
    const versioned = content.split('{{APT_VERSION}}').join(version);
    return substituteKernelPaths(versioned);
}
/**
 * Monorepo-relative `node packages/framework/bin/apt-tools.mjs` invocations
 * are baked into 194 places across canonical skills/agents. In the Aperant
 * monorepo itself they resolve fine; in every external project they throw
 * `MODULE_NOT_FOUND`. Install pipeline rewrites them to the project-local
 * kernel path that `installKernel()` provisions under `.aperant/deps/`.
 *
 * Kept a plain string-replace (not regex) so no escaping surprises and it's
 * still a no-op for snippets that reference `apt-tools` as a bare word
 * (which are already portable — rely on the user having the binary on PATH).
 *
 * @param {string} content
 * @returns {string}
 */
export function substituteKernelPaths(content) {
    if (!content)
        return content;
    return content
        .split('node packages/framework/bin/apt-tools.mjs')
        .join('node .aperant/deps/node_modules/@aperant/framework/bin/apt-tools.mjs');
}
/**
 * Rewrite slash-command references like `/apt:foo` to `/apt-foo` (or any
 * other prefix swap). Runtimes that disallow `:` in command names (Codex,
 * Copilot, Antigravity, Cline, CodeBuddy) need this.
 *
 * IMPORTANT: only rewrites in slash-command context (whitespace, line start,
 * or backtick before). Does NOT touch `apt-tools` (literal binary name) or
 * URLs that happen to contain `apt:`.
 *
 * @param {string} content
 * @param {string} fromPrefix  e.g. '/apt:'
 * @param {string} toPrefix    e.g. '/apt-'
 * @returns {string}
 */
export function rewriteCommandRefs(content, fromPrefix = '/apt:', toPrefix = '/apt-') {
    // Match the prefix when preceded by whitespace, line start, ` (backtick),
    // `(`, `[`, `"`, or `'`. Captures the command name (alphanumerics + `-`).
    const escFrom = fromPrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    const re = new RegExp(`(^|[\\s\\\`\\(\\[\\"'])${escFrom}([a-zA-Z0-9-]+)`, 'g');
    return content.replace(re, (_m, lead, cmd) => `${lead}${toPrefix}${cmd}`);
}
/**
 * Classify a canonical file by its path/extension so transforms can branch
 * on file type without hand-rolled checks.
 *
 * @param {string} filePath  absolute or relative — only the basename + extension matter
 * @returns {FileType}
 */
export function classifyFileType(filePath) {
    const lower = filePath.toLowerCase();
    if (lower.endsWith('.js') || lower.endsWith('.mjs'))
        return 'hook-js';
    if (lower.endsWith('.sh'))
        return 'hook-sh';
    if (lower.endsWith('skill.md'))
        return 'skill';
    if (lower.endsWith('.md'))
        return 'agent';
    return 'unknown';
}
/**
 * Inject (or substitute) the `apt-skill-version` / `apt-hook-version` header
 * for a single output file. For markdown, the header lives in YAML frontmatter
 * (already injected by scripts/inject-version-header.mjs into canonical files,
 * so this is a no-op for markdown except substitution). For .js/.mjs and .sh,
 * the header is the topmost line as a comment.
 *
 * The `version` argument is substituted in place of the literal
 * `{{APT_VERSION}}` token throughout the file. If the literal token is not
 * present and the file is a hook, the header line is prepended.
 *
 * @param {string} content
 * @param {FileType} fileType
 * @param {string} version
 * @returns {string}
 */
export function injectHeader(content, fileType, version) {
    const substituted = substituteVersion(content, version);
    if (fileType === 'skill' || fileType === 'agent') {
        return substituted;
    }
    if (fileType === 'hook-js') {
        // Already-headered: substitution above handled it.
        if (/^\/\/\s*apt-hook-version\s*:/m.test(substituted))
            return substituted;
        return `// apt-hook-version: ${version}\n${substituted}`;
    }
    if (fileType === 'hook-sh') {
        if (/^#\s*apt-hook-version\s*:/m.test(substituted))
            return substituted;
        // Preserve shebang on first line if present.
        if (substituted.startsWith('#!')) {
            const nl = substituted.indexOf('\n');
            if (nl !== -1) {
                return (substituted.slice(0, nl + 1) +
                    `# apt-hook-version: ${version}\n` +
                    substituted.slice(nl + 1));
            }
        }
        return `# apt-hook-version: ${version}\n${substituted}`;
    }
    return substituted;
}
/**
 * Convenience: parse + mutate frontmatter via callback + serialize, all in
 * one call. The callback receives the mutable `fm` object and may return
 * void or a replacement object. Body is preserved.
 *
 * @param {string} content
 * @param {(fm: Record<string, unknown>) => Record<string, unknown> | void} mutator
 * @returns {string}
 */
export function withFrontmatter(content, mutator) {
    const parsed = parseFrontmatter(content);
    if (!parsed.hasFrontmatter || !parsed.fm)
        return content;
    const result = mutator(parsed.fm);
    if (result && typeof result === 'object') {
        return serializeFrontmatter({ ...parsed, fm: result });
    }
    return serializeFrontmatter(parsed);
}
/**
 * Factory for the "simple" class of runtime transforms: strip frontmatter
 * fields, optionally rewrite /apt:foo → /apt-foo, and output to
 * agents/<name>.md (+ commands/<name>.md for skills).
 *
 * This covers copilot, antigravity, augment, cline, codebuddy, trae, qwen —
 * seven near-identical files that differ only in the `strip` list and
 * whether command references are rewritten.
 *
 * @param {Object} opts
 * @param {string[]} opts.strip           frontmatter fields to remove
 * @param {boolean} [opts.rewriteCommands] rewrite /apt:foo → /apt-foo (default false)
 * @returns {(input: import('../pipeline.mjs').TransformInput) => import('../pipeline.mjs').TransformOutput[]}
 */
export function createSimpleTransform({ strip, rewriteCommands = false }) {
    return function transform({ canonicalContent, canonicalRelPath, version, fileType }) {
        const subbed = substituteVersion(canonicalContent, version);
        const parsed = parseFrontmatter(subbed);
        let payload = subbed;
        if (parsed.hasFrontmatter && parsed.fm) {
            stripFields(parsed.fm, strip);
            payload = serializeFrontmatter(parsed);
        }
        if (rewriteCommands) {
            payload = rewriteCommandRefs(payload, '/apt:', '/apt-');
        }
        if (fileType === 'skill') {
            const folder = basename(dirname(canonicalRelPath));
            return [
                { outputPath: `agents/${folder}.md`, content: payload },
                { outputPath: `commands/${folder}.md`, content: payload },
            ];
        }
        if (fileType === 'agent') {
            const file = basename(canonicalRelPath, '.md');
            return [{ outputPath: `agents/${file}.md`, content: payload }];
        }
        return [];
    };
}
//# sourceMappingURL=_shared.mjs.map