fix(core): refresh cached remote skills (#34059)
This commit is contained in:
parent
ae853561cd
commit
971518c6d9
4 changed files with 216 additions and 25 deletions
|
|
@ -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()))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue