oh-my-opencode/src/features/opencode-skill-loader/merger/config-skill-entry-loader.ts

106 lines
3.2 KiB
TypeScript

import type { LoadedSkill, SkillMetadata } from "../types"
import type { SkillDefinition } from "../../../config/schema"
import type { CommandDefinition } from "../../claude-code-command-loader/types"
import { existsSync, readFileSync } from "fs"
import { dirname, isAbsolute, resolve } from "path"
import { homedir } from "os"
import { parseFrontmatter } from "../../../shared/frontmatter"
import { sanitizeModelField } from "../../../shared/model-sanitizer"
import { resolveSkillPathReferences } from "../../../shared/skill-path-resolver"
import { parseAllowedTools } from "../allowed-tools-parser"
function resolveFilePath(from: string, configDir?: string): string {
let filePath = from
if (filePath.startsWith("{file:") && filePath.endsWith("}")) {
filePath = filePath.slice(6, -1)
}
if (filePath.startsWith("~/")) {
return resolve(homedir(), filePath.slice(2))
}
if (isAbsolute(filePath)) {
return filePath
}
const baseDir = configDir || process.cwd()
return resolve(baseDir, filePath)
}
function loadSkillFromFile(filePath: string): { template: string; metadata: SkillMetadata } | null {
try {
if (!existsSync(filePath)) return null
const content = readFileSync(filePath, "utf-8")
const { data, body } = parseFrontmatter<SkillMetadata>(content)
return { template: body, metadata: data }
} catch {
return null
}
}
export function configEntryToLoadedSkill(
name: string,
entry: SkillDefinition,
configDir?: string
): LoadedSkill | null {
let template = entry.template || ""
let fileMetadata: SkillMetadata = {}
if (entry.from) {
const filePath = resolveFilePath(entry.from, configDir)
const loaded = loadSkillFromFile(filePath)
if (loaded) {
template = loaded.template
fileMetadata = loaded.metadata
} else {
return null
}
}
if (!template && !entry.from) {
return null
}
const description = entry.description || fileMetadata.description || ""
const resolvedPath = entry.from
? dirname(resolveFilePath(entry.from, configDir))
: configDir || process.cwd()
const resolvedTemplate = resolveSkillPathReferences(template.trim(), resolvedPath)
const wrappedTemplate = `<skill-instruction>
Base directory for this skill: ${resolvedPath}/
File references (@path) in this skill are relative to this directory.
${resolvedTemplate}
</skill-instruction>
<user-request>
$ARGUMENTS
</user-request>`
const definition: CommandDefinition = {
name,
description: `(config - Skill) ${description}`,
template: wrappedTemplate,
model: sanitizeModelField(entry.model || fileMetadata.model, "opencode"),
agent: entry.agent || fileMetadata.agent,
subtask: entry.subtask ?? fileMetadata.subtask,
argumentHint: entry["argument-hint"] || fileMetadata["argument-hint"],
}
const allowedTools = entry["allowed-tools"] || parseAllowedTools(fileMetadata["allowed-tools"])
return {
name,
path: entry.from ? resolveFilePath(entry.from, configDir) : undefined,
resolvedPath,
definition,
scope: "config",
license: entry.license || fileMetadata.license,
compatibility: entry.compatibility || fileMetadata.compatibility,
metadata: (entry.metadata as Record<string, string> | undefined) || fileMetadata.metadata,
allowedTools,
}
}