Files
makelore/electron/opencode/skill-registry.ts
2026-07-29 17:22:35 +08:00

81 lines
2.4 KiB
TypeScript

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<string[]> {
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<OpencodeSkillInfo[]> {
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<string>();
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;
}