import { readdir, readFile } from 'node:fs/promises'; import { basename, dirname, join } from 'node:path'; import type { OpencodeSkillInfo } from './client'; const SKILL_ROOT_NAMES = ['skill', 'skills'] as const; async function collectSkillFiles(root: string): Promise { let entries; try { entries = await readdir(root, { withFileTypes: true }); } catch (error) { if (typeof error === 'object' && error && 'code' in error && error.code === 'ENOENT') { return []; } throw error; } const files: string[] = []; for (const entry of entries) { const entryPath = join(root, entry.name); if (entry.isFile() && entry.name === 'SKILL.md') { files.push(entryPath); continue; } if (entry.isDirectory()) { files.push(...await collectSkillFiles(entryPath)); } } return files; } function unquoteYamlScalar(value: string): string { const trimmed = value.trim(); if ( (trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'")) ) { return trimmed.slice(1, -1).trim(); } return trimmed; } function readFrontmatterScalar(content: string, key: string): string | undefined { const match = content.match(/^---\s*\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/); if (!match) return undefined; const keyPattern = new RegExp(`^${key}:\\s*(.*)$`); for (const line of match[1].split(/\r?\n/)) { const scalar = line.match(keyPattern); if (!scalar) continue; const value = unquoteYamlScalar(scalar[1] ?? ''); return value || undefined; } return undefined; } export async function listInstalledOpencodeSkills(managedConfigDir?: string | null): Promise { const configDir = managedConfigDir?.trim(); if (!configDir) return []; const skillFiles = ( await Promise.all(SKILL_ROOT_NAMES.map((rootName) => collectSkillFiles(join(configDir, rootName)))) ).flat().sort((left, right) => left.localeCompare(right)); const seen = new Set(); const skills: OpencodeSkillInfo[] = []; for (const location of skillFiles) { const content = await readFile(location, 'utf8'); const name = readFrontmatterScalar(content, 'name') ?? basename(dirname(location)); if (!name || seen.has(name)) continue; seen.add(name); skills.push({ name, description: readFrontmatterScalar(content, 'description'), location, content, }); } return skills; }