summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2026-02-06 00:36:08 -0500
committerDax Raad <[email protected]>2026-02-06 00:36:11 -0500
commitc35bd39829e3760cfc2749a91cc0de779eb677f9 (patch)
treeb17605316e1b077e054c78ef8908c42713d36e77
parent266de27a0b56c29a3bdb81a5adb211f93214f5a8 (diff)
downloadopencode-c35bd39829e3760cfc2749a91cc0de779eb677f9.tar.gz
opencode-c35bd39829e3760cfc2749a91cc0de779eb677f9.zip
tui: parallelize skill downloads for faster loading
Refactored skill discovery to download skills in parallel instead of sequentially, reducing load times when multiple skills need to be fetched from remote URLs. Also updated AGENTS.md with guidance on using dev branch for diffs.
-rw-r--r--AGENTS.md1
-rw-r--r--packages/opencode/src/skill/discovery.ts107
-rw-r--r--packages/opencode/src/skill/skill.ts6
3 files changed, 66 insertions, 48 deletions
diff --git a/AGENTS.md b/AGENTS.md
index eeec0c341..d51134c0e 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,6 +1,7 @@
- To regenerate the JavaScript SDK, run `./packages/sdk/js/script/build.ts`.
- ALWAYS USE PARALLEL TOOLS WHEN APPLICABLE.
- The default branch in this repo is `dev`.
+- Local `main` ref may not exist; use `dev` or `origin/dev` for diffs.
- Prefer automation: execute requested actions without confirmation unless blocked by missing info or safety/irreversibility.
## Style Guide
diff --git a/packages/opencode/src/skill/discovery.ts b/packages/opencode/src/skill/discovery.ts
index 12b3cb3f7..a4bf97d7a 100644
--- a/packages/opencode/src/skill/discovery.ts
+++ b/packages/opencode/src/skill/discovery.ts
@@ -1,7 +1,7 @@
import path from "path"
import { mkdir } from "fs/promises"
import { Log } from "../util/log"
-import { Global } from "@/global"
+import { Global } from "../global"
export namespace Discovery {
const log = Log.create({ service: "skill-discovery" })
@@ -20,60 +20,77 @@ export namespace Discovery {
async function get(url: string, dest: string): Promise<boolean> {
if (await Bun.file(dest).exists()) return true
- try {
- const response = await fetch(url)
- if (!response.ok) {
- log.error("failed to download", { url, status: response.status })
+ return fetch(url)
+ .then(async (response) => {
+ if (!response.ok) {
+ log.error("failed to download", { url, status: response.status })
+ return false
+ }
+ await Bun.write(dest, await response.text())
+ return true
+ })
+ .catch((err) => {
+ log.error("failed to download", { url, err })
return false
- }
- const content = await response.text()
- await Bun.write(dest, content)
- return true
- } catch (err) {
- log.error("failed to download", { url, err })
- return false
- }
+ })
}
export async function pull(url: string): Promise<string[]> {
const result: string[] = []
- const indexUrl = new URL("index.json", url.endsWith("/") ? url : `${url}/`).href
- const cacheDir = dir()
-
- try {
- log.info("fetching index", { url: indexUrl })
- const response = await fetch(indexUrl)
- if (!response.ok) {
- log.error("failed to fetch index", { url: indexUrl, status: response.status })
- return result
- }
-
- const index = (await response.json()) as Index
- if (!index.skills || !Array.isArray(index.skills)) {
- log.warn("invalid index format", { url: indexUrl })
- return result
- }
+ const base = url.endsWith("/") ? url : `${url}/`
+ const index = new URL("index.json", base).href
+ const cache = dir()
+ const host = base.slice(0, -1)
- for (const skill of index.skills) {
- if (!skill.name || !skill.files || !Array.isArray(skill.files)) {
- log.warn("invalid skill entry", { url: indexUrl, skill })
- continue
+ log.info("fetching index", { url: index })
+ const data = await fetch(index)
+ .then(async (response) => {
+ if (!response.ok) {
+ log.error("failed to fetch index", { url: index, status: response.status })
+ return undefined
}
+ return response
+ .json()
+ .then((json) => json as Index)
+ .catch((err) => {
+ log.error("failed to parse index", { url: index, err })
+ return undefined
+ })
+ })
+ .catch((err) => {
+ log.error("failed to fetch index", { url: index, err })
+ return undefined
+ })
- const skillDir = path.join(cacheDir, skill.name)
- for (const file of skill.files) {
- const fileUrl = new URL(file, `${url.replace(/\/$/, "")}/${skill.name}/`).href
- const localPath = path.join(skillDir, file)
- await mkdir(path.dirname(localPath), { recursive: true })
- await get(fileUrl, localPath)
- }
+ if (!data?.skills || !Array.isArray(data.skills)) {
+ log.warn("invalid index format", { url: index })
+ return result
+ }
- const skillMd = path.join(skillDir, "SKILL.md")
- if (await Bun.file(skillMd).exists()) result.push(skillDir)
+ const list = data.skills.filter((skill) => {
+ if (!skill?.name || !Array.isArray(skill.files)) {
+ log.warn("invalid skill entry", { url: index, skill })
+ return false
}
- } catch (err) {
- log.error("failed to fetch from URL", { url, err })
- }
+ return true
+ })
+
+ await Promise.all(
+ list.map(async (skill) => {
+ const root = path.join(cache, skill.name)
+ await Promise.all(
+ skill.files.map(async (file) => {
+ const link = new URL(file, `${host}/${skill.name}/`).href
+ const dest = path.join(root, file)
+ await mkdir(path.dirname(dest), { recursive: true })
+ await get(link, dest)
+ }),
+ )
+
+ const md = path.join(root, "SKILL.md")
+ if (await Bun.file(md).exists()) result.push(root)
+ }),
+ )
return result
}
diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts
index b8eb64250..42795b7eb 100644
--- a/packages/opencode/src/skill/skill.ts
+++ b/packages/opencode/src/skill/skill.ts
@@ -153,9 +153,9 @@ export namespace Skill {
}
// Download and load skills from URLs
- for (const skillUrl of config.skills?.urls ?? []) {
- const downloadedDirs = await Discovery.pull(skillUrl)
- for (const dir of downloadedDirs) {
+ for (const url of config.skills?.urls ?? []) {
+ const list = await Discovery.pull(url)
+ for (const dir of list) {
dirs.add(dir)
for await (const match of SKILL_GLOB.scan({
cwd: dir,