fix: skill handler - static imports, no await in generators
This commit is contained in:
parent
547f794e47
commit
dd6ce86b26
1 changed files with 130 additions and 0 deletions
130
skill_handler.ts
Normal file
130
skill_handler.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
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<Array<{ name: string; description?: string; location: string; content: string }>> {
|
||||
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<void> {
|
||||
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)
|
||||
}),
|
||||
)
|
||||
Loading…
Reference in a new issue