import fs from 'node:fs'; import path from 'node:path'; import MarkdownIt from 'markdown-it'; import { parse as parseYaml } from 'yaml'; import { slugifyHeading } from './slugify-heading.mjs'; const markdown = new MarkdownIt({ html: false, linkify: false, typographer: false }); markdown.validateLink = () => true; const textExtensions = new Set([ '.css', '.html', '.js', '.json', '.jsx', '.md', '.mjs', '.ts', '.tsx', '.txt', '.vue', '.yaml', '.yml', ]); const comparePaths = (left, right) => { if (left < right) { return -1; } return left > right ? 1 : 0; }; const assert = (condition, message) => { if (!condition) { throw new Error(message); } }; const isWithin = (filePath, parentPath) => { const relativePath = path.relative(parentPath, filePath); return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath)); }; const assertSkillName = (name) => { assert(typeof name === 'string', 'Skill name must be a string.'); assert(name.length <= 64, 'Skill name must not exceed 64 characters.'); assert(/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name), `Invalid skill name: ${name}`); }; const normalizeBundlePath = (filePath) => { const normalized = path.posix.normalize(filePath.replaceAll(path.sep, '/')); assert( normalized !== '' && normalized !== '.' && normalized !== '..' && !normalized.startsWith('../'), `Invalid bundle path: ${filePath}`, ); assert(!path.posix.isAbsolute(normalized), `Bundle path must be relative: ${filePath}`); return normalized; }; const assertSafeSource = (sourcePath, allowedRoot, expectedType) => { assert(isWithin(sourcePath, allowedRoot), `Source escapes allowed root: ${sourcePath}`); assert(fs.existsSync(sourcePath), `Source not found: ${sourcePath}`); const stats = fs.lstatSync(sourcePath); assert(!stats.isSymbolicLink(), `Symbolic links are not allowed in skill sources: ${sourcePath}`); assert( isWithin(fs.realpathSync(sourcePath), fs.realpathSync(allowedRoot)), `Source resolves outside allowed root: ${sourcePath}`, ); if (expectedType === 'file') { assert(stats.isFile(), `Expected a regular source file: ${sourcePath}`); } else if (expectedType === 'directory') { assert(stats.isDirectory(), `Expected a source directory: ${sourcePath}`); } }; const listSourceFiles = (sourcePath, allowedRoot) => { assertSafeSource(sourcePath, allowedRoot); const stats = fs.lstatSync(sourcePath); if (stats.isFile()) { return [sourcePath]; } assert(stats.isDirectory(), `Unsupported reference source: ${sourcePath}`); return fs .readdirSync(sourcePath, { withFileTypes: true }) .sort((left, right) => comparePaths(left.name, right.name)) .flatMap((entry) => listSourceFiles(path.join(sourcePath, entry.name), allowedRoot)); }; const addBundleFile = (bundle, targetPath, sourcePath, allowedRoot) => { const normalizedTarget = normalizeBundlePath(targetPath); assert(!bundle.has(normalizedTarget), `Duplicate bundle target: ${normalizedTarget}`); assertSafeSource(sourcePath, allowedRoot, 'file'); bundle.set(normalizedTarget, { content: fs.readFileSync(sourcePath), sourcePath, }); }; export const resolveSkillPaths = ({ config, repoRoot }) => { assertSkillName(config.name); const sourceRoot = path.resolve(repoRoot, 'src-skills'); const outputRoot = path.resolve(repoRoot, 'skills'); const sourceDir = path.resolve(sourceRoot, config.name); const outputDir = path.resolve(outputRoot, config.name); assert(path.dirname(sourceDir) === sourceRoot, 'Skill source must be a direct child of src-skills.'); assert(path.dirname(outputDir) === outputRoot, 'Skill output must be a direct child of skills.'); assert(!isWithin(sourceDir, outputDir) && !isWithin(outputDir, sourceDir), 'Skill source and output overlap.'); assertSafeSource(sourceRoot, repoRoot, 'directory'); if (fs.existsSync(outputRoot)) { assertSafeSource(outputRoot, repoRoot, 'directory'); } return { outputDir, outputRoot, sourceDir, sourceRoot }; }; export const createSkillBundle = ({ config, repoRoot }) => { const { sourceDir, sourceRoot } = resolveSkillPaths({ config, repoRoot }); const sourcePath = path.resolve(sourceDir, config.source); const bundle = new Map(); assertSafeSource(sourceDir, sourceRoot, 'directory'); assert(isWithin(sourcePath, sourceDir), `Skill source escapes its directory: ${config.source}`); addBundleFile(bundle, 'SKILL.md', sourcePath, sourceDir); for (const reference of config.references ?? []) { assert(Array.isArray(reference.include) && reference.include.length > 0, 'Reference include must be non-empty.'); const referenceRoot = path.resolve(repoRoot, reference.source); const targetRoot = normalizeBundlePath(reference.target); assertSafeSource(referenceRoot, repoRoot, 'directory'); for (const includePath of reference.include) { assert(typeof includePath === 'string' && includePath !== '', 'Reference include must be a path string.'); const includedSource = path.resolve(referenceRoot, includePath); assert(isWithin(includedSource, referenceRoot), `Reference include escapes root: ${includePath}`); assert(fs.existsSync(includedSource), `Reference include not found: ${reference.source}/${includePath}`); for (const sourceFile of listSourceFiles(includedSource, referenceRoot)) { const relativeSource = path.relative(referenceRoot, sourceFile).replaceAll(path.sep, '/'); addBundleFile(bundle, path.posix.join(targetRoot, relativeSource), sourceFile, referenceRoot); } } } return new Map([...bundle].sort(([left], [right]) => comparePaths(left, right))); }; const parseFrontmatter = (content) => { const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/); assert(match, 'SKILL.md must start with YAML frontmatter.'); let frontmatter; try { frontmatter = parseYaml(match[1]); } catch (error) { throw new Error(`SKILL.md contains invalid YAML frontmatter: ${error.message}`); } assert(frontmatter && typeof frontmatter === 'object' && !Array.isArray(frontmatter), 'Invalid skill frontmatter.'); return frontmatter; }; const headingText = (inlineToken) => { return (inlineToken.children ?? []) .map((child) => { if (child.type === 'text' || child.type === 'code_inline') { return child.content; } if (child.type === 'image') { return child.content; } return ''; }) .join(''); }; const createHeadingAnchors = (tokens) => { const anchors = new Set(); const occurrences = new Map(); for (let index = 0; index < tokens.length; index += 1) { if (tokens[index].type !== 'heading_open' || tokens[index + 1]?.type !== 'inline') { continue; } const text = headingText(tokens[index + 1]); const customAnchor = text.match(/\s*\{#([^}]+)}\s*$/)?.[1]; const base = customAnchor ?? slugifyHeading(text.replace(/\s*\{#[^}]+}\s*$/, '')); const occurrence = occurrences.get(base) ?? 0; const anchor = occurrence === 0 ? base : `${base}-${occurrence}`; occurrences.set(base, occurrence + 1); anchors.add(anchor); } return anchors; }; const decodeLinkPart = (value, context) => { try { return decodeURIComponent(value); } catch { throw new Error(`Invalid URL encoding in ${context}: ${value}`); } }; const normalizeLinkTarget = (rawTarget, sourcePath) => { if (rawTarget.startsWith('//')) { return null; } const rawScheme = rawTarget.match(/^([a-z][a-z+.-]*):/i)?.[1]?.toLowerCase(); if (rawScheme) { assert( ['http', 'https', 'mailto', 'tel'].includes(rawScheme), `Unsupported or unsafe URL scheme in ${sourcePath}: ${rawTarget}`, ); return null; } const hashIndex = rawTarget.indexOf('#'); const pathAndQuery = hashIndex === -1 ? rawTarget : rawTarget.slice(0, hashIndex); const rawFragment = hashIndex === -1 ? '' : rawTarget.slice(hashIndex + 1); const queryIndex = pathAndQuery.indexOf('?'); const rawPath = queryIndex === -1 ? pathAndQuery : pathAndQuery.slice(0, queryIndex); const targetPath = decodeLinkPart(rawPath, `link path in ${sourcePath}`); const fragment = decodeLinkPart(rawFragment, `link fragment in ${sourcePath}`); assert(!/^[a-z]:[\\/]/i.test(targetPath), `Windows absolute link in ${sourcePath}: ${rawTarget}`); assert(!targetPath.startsWith('\\'), `Backslash link is not portable in ${sourcePath}: ${rawTarget}`); assert(!targetPath.startsWith('/'), `Root-relative link is not portable in ${sourcePath}: ${rawTarget}`); return { fragment, targetPath }; }; const resolveBundleLink = (bundle, sourcePath, targetPath) => { const joinedPath = path.posix.normalize(path.posix.join(path.posix.dirname(sourcePath), targetPath)); assert( joinedPath !== '..' && !joinedPath.startsWith('../') && !path.posix.isAbsolute(joinedPath), `Link escapes skill bundle in ${sourcePath}: ${targetPath}`, ); if (bundle.has(joinedPath)) { return joinedPath; } const directoryIndex = path.posix.join(joinedPath, 'README.md'); if (bundle.has(directoryIndex)) { return directoryIndex; } throw new Error(`Broken link in ${sourcePath}: ${targetPath}`); }; const normalizeReferenceLabel = (label) => label.trim().replace(/\s+/g, ' ').toUpperCase(); const assertDefinedReferenceLinks = (tokens, references, filePath) => { for (const token of tokens) { if (token.type !== 'inline') { continue; } const inspectableContent = token.content.replace(/(`+)[\s\S]*?\1/g, ''); for (const match of inspectableContent.matchAll(/(? { const links = []; for (const token of tokens) { for (const child of token.children ?? []) { if (child.type === 'link_open') { links.push(child.attrGet('href')); } else if (child.type === 'image') { links.push(child.attrGet('src')); } } } return links.filter((link) => typeof link === 'string'); }; const parseMarkdown = (content, filePath) => { const environment = {}; const tokens = markdown.parse(content, environment); assertDefinedReferenceLinks(tokens, environment.references ?? {}, filePath); return { links: collectMarkdownLinks(tokens), tokens }; }; const assertMarkdownLinks = (bundle, filePath, content) => { const { links, tokens } = parseMarkdown(content, filePath); for (const rawTarget of links) { const target = normalizeLinkTarget(rawTarget, filePath); if (!target) { continue; } const resolvedPath = target.targetPath === '' ? filePath : resolveBundleLink(bundle, filePath, target.targetPath); if (target.fragment === '') { continue; } const targetContent = bundle.get(resolvedPath).content.toString('utf8'); const targetTokens = markdown.parse(targetContent, {}); const anchors = createHeadingAnchors(targetTokens); assert(anchors.has(target.fragment), `Broken anchor in ${filePath}: ${rawTarget}`); } createHeadingAnchors(tokens); }; const assertPortableText = (content, filePath, repoRoot, legacyMarkers) => { assert(!content.includes(repoRoot), `${filePath} contains an absolute workspace path.`); assert( !/(?:^|[\s("'`])\/(?:home|Users)\/[^/\s]+\//m.test(content), `${filePath} contains a user-specific absolute path.`, ); assert( !/(?:^|[\s("'`])[a-z]:\\Users\\/im.test(content), `${filePath} contains a Windows user-specific absolute path.`, ); assert(!content.includes('