/
Alex_Ural
/
opencode
Обзор
Документация
Войти
/
Alex_Ural
/
opencode
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
dev
packages/core/src/skill/discovery.ts
213 строк
8 KB
James Long
refactor(core): remove infrastructure layer exports (#34624)
30 июн 2026, 19:17
Не верифицирован
30 июн 2026, 19:17
472d0f3
Код
Авторство
О чём код?
export * as SkillDiscovery from "./discovery" import path from "path" import { Context, Effect, Layer, Schedule, Schema } from "effect" import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" import { FSUtil } from "../fs-util" import { Global } from "../global" import { makeGlobalNode } from "../effect/app-node" import { httpClient } from "../effect/app-node-platform" import { AbsolutePath } from "../schema" const skillConcurrency = 4 const fileConcurrency = 8 function isSafeSegment(value: string) { return ( value.length > 0 && value !== "." && value !== ".." && !value.includes("/") && !value.includes("\\") && !value.includes("\0") ) } function isSafeRelativePath(value: string) { const segments = value.split("/") return ( value.length > 0 && !value.includes("\\") && !value.includes("\0") && !value.includes("?") && !value.includes("#") && !URL.canParse(value) && !path.posix.isAbsolute(value) && !path.win32.isAbsolute(value) && segments.every((segment) => { try { const decoded = decodeURIComponent(segment) return ( decoded.length > 0 && decoded !== "." && decoded !== ".." && !decoded.includes("/") && !decoded.includes("\\") && !decoded.includes("\0") ) } catch { return false } }) ) } class IndexSkill extends Schema.Class<IndexSkill>("SkillDiscovery.IndexSkill")({ name: Schema.String, version: Schema.optional(Schema.String), files: Schema.Array(Schema.String), }) {} class Index extends Schema.Class<Index>("SkillDiscovery.Index")({ skills: Schema.Array(IndexSkill), }) {} export interface Interface { readonly pull: (url: string) => Effect.Effect<AbsolutePath[]> } export class Service extends Context.Service<Service, Interface>()("@opencode/v2/SkillDiscovery") {} const layer = Layer.effect( Service, Effect.gen(function* () { const fs = yield* FSUtil.Service const global = yield* Global.Service const http = (yield* HttpClient.HttpClient).pipe( HttpClient.retryTransient({ retryOn: "errors-and-responses", times: 2, schedule: Schedule.exponential(200).pipe(Schedule.jittered), }), HttpClient.filterStatusOk, ) const download = Effect.fn("SkillDiscovery.download")(function* (url: string, destination: string) { if (yield* fs.exists(destination).pipe(Effect.orDie)) return true return yield* HttpClientRequest.get(url).pipe( http.execute, Effect.flatMap((response) => response.arrayBuffer), Effect.flatMap((body) => fs.writeWithDirs(destination, new Uint8Array(body))), Effect.as(true), Effect.catch((error) => Effect.logError("failed to download skill file", { url, error }).pipe(Effect.as(false)), ), ) }) return Service.of({ pull: Effect.fn("SkillDiscovery.pull")(function* (url) { const base = url.endsWith("/") ? url : `${url}/` const source = new URL(base) const index = new URL("index.json", source).href const data = yield* HttpClientRequest.get(index).pipe( HttpClientRequest.acceptJson, http.execute, Effect.flatMap(HttpClientResponse.schemaBodyJson(Index)), Effect.catch((error) => Effect.logError("failed to fetch skill index", { url: index, error }).pipe(Effect.as(undefined)), ), ) if (!data) return [] const sourceRoot = path.resolve(global.cache, "skills", Bun.hash(base).toString(16)) return yield* Effect.forEach( data.skills.flatMap((skill) => { if (!isSafeSegment(skill.name)) { return [] } if (!skill.files.includes("SKILL.md") && !skill.files.includes(`${skill.name}.md`)) { return [] } const root = path.resolve(sourceRoot, skill.name) if (!FSUtil.contains(sourceRoot, root) || root === sourceRoot) { return [] } const skillUrl = new URL(`${encodeURIComponent(skill.name)}/`, source) const versionFile = path.join(root, ".opencode-version") const files = skill.files.map((file) => { if (!isSafeRelativePath(file)) return undefined let resource: URL try { resource = new URL(file, skillUrl) } catch { return undefined } if (resource.origin !== source.origin) return undefined const destination = path.resolve(root, file) if (!FSUtil.contains(root, destination) || destination === root) return undefined return { url: resource.href, destination, file, } }) if (files.some((file) => file === undefined)) { return [] } return [{ skill, root, versionFile, files: files as { url: string; destination: string; file: string }[] }] }), ({ skill, root, versionFile, files }) => Effect.gen(function* () { const version = skill.version const current = version === undefined ? undefined : yield* fs.readFileStringSafe(versionFile).pipe(Effect.catch(() => Effect.succeed(undefined))) if (version === undefined || current === version) { yield* Effect.forEach(files, (file) => download(file.url, file.destination), { concurrency: fileConcurrency, discard: true, }) } else { const token = crypto.randomUUID() const staging = `${root}.tmp-${token}` const backup = `${root}.old-${token}` yield* Effect.gen(function* () { const downloaded = yield* Effect.forEach( files, (file) => download(file.url, path.resolve(staging, file.file)), { concurrency: fileConcurrency }, ) if (!downloaded.every(Boolean)) return const exists = (yield* fs.exists(path.join(staging, "SKILL.md")).pipe(Effect.orDie)) || (yield* fs.exists(path.join(staging, `${skill.name}.md`)).pipe(Effect.orDie)) if (!exists) return yield* fs.writeFileString(path.join(staging, ".opencode-version"), version) yield* Effect.uninterruptible( Effect.gen(function* () { const cached = yield* fs.exists(root).pipe(Effect.orDie) if (cached) yield* fs.rename(root, backup) yield* fs.rename(staging, root).pipe( Effect.catch((error) => Effect.gen(function* () { if (cached) yield* fs.rename(backup, root).pipe(Effect.ignore) return yield* Effect.fail(error) }), ), ) if (cached) yield* fs.remove(backup, { recursive: true, force: true }).pipe(Effect.ignore) }), ) }).pipe( Effect.catch((error) => Effect.logError("failed to refresh skill", { skill: skill.name, error })), Effect.ensuring(fs.remove(staging, { recursive: true, force: true }).pipe(Effect.ignore)), ) } const exists = (yield* fs.exists(path.join(root, "SKILL.md")).pipe(Effect.orDie)) || (yield* fs.exists(path.join(root, `${skill.name}.md`)).pipe(Effect.orDie)) return exists ? [AbsolutePath.make(root)] : [] }), { concurrency: skillConcurrency }, ).pipe(Effect.map((directories) => directories.flat())) }), }) }), ) export const node = makeGlobalNode({ service: Service, layer, deps: [httpClient, FSUtil.node, Global.node] })