fix(core): refresh cached remote skills (#34059)

This commit is contained in:
Shoubhit Dash 2026-06-26 17:42:50 +05:30 committed by GitHub
commit 971518c6d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 216 additions and 25 deletions

View file

@ -52,6 +52,7 @@ function isSafeRelativePath(value: string) {
class IndexSkill extends Schema.Class<IndexSkill>("SkillDiscovery.IndexSkill")({
name: Schema.String,
version: Schema.optional(Schema.String),
files: Schema.Array(Schema.String),
}) {}
@ -80,12 +81,15 @@ export const layer = Layer.effect(
)
const download = Effect.fn("SkillDiscovery.download")(function* (url: string, destination: string) {
if (yield* fs.exists(destination).pipe(Effect.orDie)) return
yield* HttpClientRequest.get(url).pipe(
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.catch((error) => Effect.logError("failed to download skill file", { url, error })),
Effect.as(true),
Effect.catch((error) =>
Effect.logError("failed to download skill file", { url, error }).pipe(Effect.as(false)),
),
)
})
@ -120,6 +124,7 @@ export const layer = Layer.effect(
}
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
@ -135,23 +140,66 @@ export const layer = Layer.effect(
return {
url: resource.href,
destination,
file,
}
})
if (files.some((file) => file === undefined)) {
return []
}
return [{ skill, root, files: files as { url: string; destination: string }[] }]
return [{ skill, root, versionFile, files: files as { url: string; destination: string; file: string }[] }]
}),
({ skill, root, files }) =>
({ skill, root, versionFile, files }) =>
Effect.gen(function* () {
yield* Effect.forEach(files, (file) => download(file.url, file.destination), {
concurrency: fileConcurrency,
discard: true,
})
return (yield* fs.exists(path.join(root, "SKILL.md")).pipe(Effect.orDie)) ||
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))
? [AbsolutePath.make(root)]
: []
return exists ? [AbsolutePath.make(root)] : []
}),
{ concurrency: skillConcurrency },
).pipe(Effect.map((directories) => directories.flat()))