import * as fs from 'fs' import * as path from 'path' export const ROOT_DIR = path.join(__dirname, '..') export const AGENTS_SKILLS_DIR = path.join(ROOT_DIR, '.agents', 'skills') export const CLAUDE_SKILLS_DIR = path.join(ROOT_DIR, '.claude', 'skills') export const AGENTS_SKILLS_GITIGNORE = path.join(AGENTS_SKILLS_DIR, '.gitignore') export const CLAUDE_SKILLS_GITIGNORE = path.join(CLAUDE_SKILLS_DIR, '.gitignore') export const PUBLIC_SKILLS_FILE = path.join(AGENTS_SKILLS_DIR, 'public-skills.txt') const SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/ export function listSkillNames(): string[] { const content = readFileSafe(PUBLIC_SKILLS_FILE) if (content === null) { throw new Error('.agents/skills/public-skills.txt is missing') } const names: string[] = [] const seen = new Set() const lines = content.split('\n') for (const [index, rawLine] of lines.entries()) { const trimmedLine = rawLine.trim() if (trimmedLine === '' || trimmedLine.startsWith('#')) { continue } if (trimmedLine.includes('#')) { throw new Error( `inline comments are not allowed at .agents/skills/public-skills.txt:${index + 1}; ` + 'put comments on the previous line' ) } const name = trimmedLine if (!SKILL_NAME_PATTERN.test(name)) { throw new Error(`invalid skill name '${name}' at .agents/skills/public-skills.txt:${index + 1}`) } if (seen.has(name)) { throw new Error(`duplicate skill name '${name}' at .agents/skills/public-skills.txt:${index + 1}`) } seen.add(name) names.push(name) } return names.sort((a, b) => a.localeCompare(b)) } export function buildAgentsSkillsGitignore(skillNames: string[]): string { const lines = [ '# AUTO-GENERATED by `pnpm skills:sync`.', '# Do not edit manually.', '*', '!.gitignore', '!README*.md', '!public-skills.txt' ] for (const skillName of skillNames) { lines.push(`!${skillName}/`) lines.push(`!${skillName}/**`) } return `${lines.join('\n')}\n` } export function buildClaudeSkillsGitignore(skillNames: string[]): string { const lines = [ '# AUTO-GENERATED by `pnpm skills:sync`.', '# Do not edit manually.', '*', '!.gitignore', '!README*.md' ] for (const skillName of skillNames) { lines.push(`!${skillName}`) } return `${lines.join('\n')}\n` } export function writeFileIfChanged(filePath: string, content: string): boolean { let current = '' try { current = fs.readFileSync(filePath, 'utf-8') } catch (error) { const nodeError = error as NodeJS.ErrnoException if (nodeError.code !== 'ENOENT') { throw error } } if (current === content) { return false } fs.writeFileSync(filePath, content, 'utf-8') return true } export function readFileSafe(filePath: string): string | null { if (!fs.existsSync(filePath)) { return null } return fs.readFileSync(filePath, 'utf-8') }