81 lines
2.4 KiB
TypeScript
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;
|
|
}
|