import path from "path" import { readdir, readFile, mkdir, writeFile, rm } from "fs/promises" import { Effect } from "effect" import { HttpApiBuilder, HttpApiError } from "effect/unstable/httpapi" import { InstanceHttpApi } from "../api" import { Config } from "@/config/config" const FORGEJO_BASE = "https://forge.wilddragon.net" function resolveUrl(input: string): string { return /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(input) ? `${FORGEJO_BASE}/${input}/raw/branch/main/SKILL.md` : input } function parseField(content: string, field: string): string | undefined { const m = content.match(new RegExp(`^---[\\s\\S]*?^${field}:\\s*(.+?)\\s*$`, "m")) return m?.[1] } async function listSkills( dirs: string[], ): Promise> { const skills: Array<{ name: string; description?: string; location: string; content: string }> = [] for (const configDir of dirs) { const skillsDir = path.join(configDir, "skills") let entries: string[] try { entries = await readdir(skillsDir) } catch { continue } for (const entry of entries) { const file = path.join(skillsDir, entry, "SKILL.md") try { const content = await readFile(file, "utf-8") skills.push({ name: parseField(content, "name") ?? entry, description: parseField(content, "description"), location: file, content, }) } catch { /* skip unreadable */ } } } return skills } async function installSkill( dirs: string[], url: string, ): Promise<{ name: string; description?: string; location: string; content: string }> { const res = await fetch(resolveUrl(url)) if (!res.ok) throw new Error(`Fetch failed: ${res.status} ${res.statusText}`) const content = await res.text() const name = parseField(content, "name") if (!name) throw new Error("SKILL.md is missing 'name' in frontmatter") const dir = path.join(dirs[0], "skills", name) const file = path.join(dir, "SKILL.md") await mkdir(dir, { recursive: true }) await writeFile(file, content, "utf-8") return { name, description: parseField(content, "description"), location: file, content } } async function uploadSkill( dirs: string[], inputName: string, content: string, ): Promise<{ name: string; description?: string; location: string; content: string }> { const name = parseField(content, "name") ?? inputName const dir = path.join(dirs[0], "skills", name) const file = path.join(dir, "SKILL.md") await mkdir(dir, { recursive: true }) await writeFile(file, content, "utf-8") return { name, description: parseField(content, "description"), location: file, content } } async function removeSkill(dirs: string[], skillName: string): Promise { for (const configDir of dirs) { const skillDir = path.join(configDir, "skills", skillName) try { await rm(skillDir, { recursive: true, force: true }) return } catch { /* try next */ } } throw new Error(`Skill '${skillName}' not found`) } export const skillHandlers = HttpApiBuilder.group(InstanceHttpApi, "skill", (handlers) => Effect.gen(function* () { const config = yield* Config.Service const list = Effect.fn("SkillHttpApi.list")(function* () { const dirs = yield* config.directories() return yield* Effect.tryPromise({ try: () => listSkills(dirs), catch: (e) => new Error(String(e)), }) }) const install = Effect.fn("SkillHttpApi.install")(function* (ctx) { const dirs = yield* config.directories() if (!dirs.length) return yield* Effect.fail(HttpApiError.BadRequest.make("No config directories found")) return yield* Effect.tryPromise({ try: () => installSkill(dirs, ctx.payload.url), catch: (e) => HttpApiError.BadRequest.make(String(e)), }) }) const upload = Effect.fn("SkillHttpApi.upload")(function* (ctx) { const dirs = yield* config.directories() if (!dirs.length) return yield* Effect.fail(HttpApiError.BadRequest.make("No config directories found")) return yield* Effect.tryPromise({ try: () => uploadSkill(dirs, ctx.payload.name, ctx.payload.content), catch: (e) => HttpApiError.BadRequest.make(String(e)), }) }) const remove = Effect.fn("SkillHttpApi.remove")(function* (ctx) { const dirs = yield* config.directories() yield* Effect.tryPromise({ try: () => removeSkill(dirs, ctx.pathParams.name), catch: (e) => HttpApiError.NotFound.make(String(e)), }) return true as const }) return handlers .handle("list", list) .handle("install", install) .handle("upload", upload) .handle("remove", remove) }), )