summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--bun.lock18
-rw-r--r--package.json1
-rw-r--r--packages/opencode/package.json1
-rw-r--r--packages/opencode/src/cli/cmd/tui/context/theme.tsx7
-rw-r--r--packages/opencode/src/config/config.ts25
-rw-r--r--packages/opencode/src/file/ignore.ts15
-rw-r--r--packages/opencode/src/project/project.ts16
-rw-r--r--packages/opencode/src/session/instruction.ts13
-rw-r--r--packages/opencode/src/skill/skill.ts47
-rw-r--r--packages/opencode/src/storage/json-migration.ts8
-rw-r--r--packages/opencode/src/storage/storage.ts39
-rw-r--r--packages/opencode/src/tool/registry.ts4
-rw-r--r--packages/opencode/src/tool/truncation.ts4
-rw-r--r--packages/opencode/src/util/filesystem.ts12
-rw-r--r--packages/opencode/src/util/glob.ts34
-rw-r--r--packages/opencode/src/util/log.ts13
-rw-r--r--packages/opencode/test/util/glob.test.ts91
17 files changed, 122 insertions, 226 deletions
diff --git a/bun.lock b/bun.lock
index ff732efd1..2df39fa54 100644
--- a/bun.lock
+++ b/bun.lock
@@ -15,7 +15,6 @@
"@actions/artifact": "5.0.1",
"@tsconfig/bun": "catalog:",
"@types/mime-types": "3.0.1",
- "glob": "13.0.5",
"husky": "9.1.7",
"prettier": "3.6.2",
"semver": "^7.6.0",
@@ -322,7 +321,6 @@
"diff": "catalog:",
"drizzle-orm": "1.0.0-beta.12-a5629fb",
"fuzzysort": "3.1.0",
- "glob": "13.0.5",
"google-auth-library": "10.5.0",
"gray-matter": "4.0.3",
"hono": "catalog:",
@@ -2696,7 +2694,7 @@
"github-slugger": ["[email protected]", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
- "glob": ["[email protected]", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="],
+ "glob": ["[email protected]", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
"glob-parent": ["[email protected]", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
@@ -3076,7 +3074,7 @@
"lower-case": ["[email protected]", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg=="],
- "lru-cache": ["[email protected]", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="],
+ "lru-cache": ["[email protected]", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
"lru.min": ["[email protected]", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="],
@@ -4788,14 +4786,14 @@
"openid-client/jose": ["[email protected]", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="],
- "openid-client/lru-cache": ["[email protected]", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
-
"p-locate/p-limit": ["[email protected]", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
"parse-entities/@types/unist": ["@types/[email protected]", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
"parse5/entities": ["[email protected]", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
+ "path-scurry/lru-cache": ["[email protected]", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="],
+
"pixelmatch/pngjs": ["[email protected]", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="],
"pkg-up/find-up": ["[email protected]", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="],
@@ -4868,9 +4866,9 @@
"unifont/ofetch": ["[email protected]", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
- "utif2/pako": ["[email protected]", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
+ "unstorage/lru-cache": ["[email protected]", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="],
- "vite-plugin-icons-spritesheet/glob": ["[email protected]", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
+ "utif2/pako": ["[email protected]", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
"vitest/tinyexec": ["[email protected]", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
@@ -5228,6 +5226,8 @@
"astro/unstorage/h3": ["[email protected]", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.5", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg=="],
+ "astro/unstorage/lru-cache": ["[email protected]", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="],
+
"astro/unstorage/ofetch": ["[email protected]", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
"aws-sdk/xml2js/sax": ["[email protected]", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="],
@@ -5358,8 +5358,6 @@
"type-is/mime-types/mime-db": ["[email protected]", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
- "vite-plugin-icons-spritesheet/glob/minimatch": ["[email protected]", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A=="],
-
"wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/[email protected]", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
"wrangler/esbuild/@esbuild/android-arm": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="],
diff --git a/package.json b/package.json
index 2e7c1172a..f1ba10269 100644
--- a/package.json
+++ b/package.json
@@ -70,7 +70,6 @@
"@actions/artifact": "5.0.1",
"@tsconfig/bun": "catalog:",
"@types/mime-types": "3.0.1",
- "glob": "13.0.5",
"husky": "9.1.7",
"prettier": "3.6.2",
"semver": "^7.6.0",
diff --git a/packages/opencode/package.json b/packages/opencode/package.json
index dada02497..21af8f85a 100644
--- a/packages/opencode/package.json
+++ b/packages/opencode/package.json
@@ -107,7 +107,6 @@
"diff": "catalog:",
"drizzle-orm": "1.0.0-beta.12-a5629fb",
"fuzzysort": "3.1.0",
- "glob": "13.0.5",
"google-auth-library": "10.5.0",
"gray-matter": "4.0.3",
"hono": "catalog:",
diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx
index 621b7cbf8..f9db1d77c 100644
--- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx
+++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx
@@ -3,7 +3,6 @@ import path from "path"
import { createEffect, createMemo, onMount } from "solid-js"
import { useSync } from "@tui/context/sync"
import { createSimpleContext } from "./helper"
-import { Glob } from "../../../../util/glob"
import aura from "./theme/aura.json" with { type: "json" }
import ayu from "./theme/ayu.json" with { type: "json" }
import catppuccin from "./theme/catppuccin.json" with { type: "json" }
@@ -392,6 +391,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
},
})
+const CUSTOM_THEME_GLOB = new Bun.Glob("themes/*.json")
async function getCustomThemes() {
const directories = [
Global.Path.config,
@@ -405,10 +405,11 @@ async function getCustomThemes() {
const result: Record<string, ThemeJson> = {}
for (const dir of directories) {
- for (const item of await Glob.scan("themes/*.json", {
- cwd: dir,
+ for await (const item of CUSTOM_THEME_GLOB.scan({
absolute: true,
+ followSymlinks: true,
dot: true,
+ cwd: dir,
})) {
const name = path.basename(item, ".json")
result[name] = await Filesystem.readJson(item)
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index 23e0b5b46..36f6c762b 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -28,7 +28,6 @@ import { constants, existsSync } from "fs"
import { Bus } from "@/bus"
import { GlobalBus } from "@/bus/global"
import { Event } from "../server/event"
-import { Glob } from "../util/glob"
import { PackageRegistry } from "@/bun/registry"
import { proxied } from "@/util/proxied"
import { iife } from "@/util/iife"
@@ -352,12 +351,14 @@ export namespace Config {
return ext.length ? file.slice(0, -ext.length) : file
}
+ const COMMAND_GLOB = new Bun.Glob("{command,commands}/**/*.md")
async function loadCommand(dir: string) {
const result: Record<string, Command> = {}
- for (const item of await Glob.scan("{command,commands}/**/*.md", {
- cwd: dir,
+ for await (const item of COMMAND_GLOB.scan({
absolute: true,
+ followSymlinks: true,
dot: true,
+ cwd: dir,
})) {
const md = await ConfigMarkdown.parse(item).catch(async (err) => {
const message = ConfigMarkdown.FrontmatterError.isInstance(err)
@@ -389,13 +390,15 @@ export namespace Config {
return result
}
+ const AGENT_GLOB = new Bun.Glob("{agent,agents}/**/*.md")
async function loadAgent(dir: string) {
const result: Record<string, Agent> = {}
- for (const item of await Glob.scan("{agent,agents}/**/*.md", {
- cwd: dir,
+ for await (const item of AGENT_GLOB.scan({
absolute: true,
+ followSymlinks: true,
dot: true,
+ cwd: dir,
})) {
const md = await ConfigMarkdown.parse(item).catch(async (err) => {
const message = ConfigMarkdown.FrontmatterError.isInstance(err)
@@ -427,12 +430,14 @@ export namespace Config {
return result
}
+ const MODE_GLOB = new Bun.Glob("{mode,modes}/*.md")
async function loadMode(dir: string) {
const result: Record<string, Agent> = {}
- for (const item of await Glob.scan("{mode,modes}/*.md", {
- cwd: dir,
+ for await (const item of MODE_GLOB.scan({
absolute: true,
+ followSymlinks: true,
dot: true,
+ cwd: dir,
})) {
const md = await ConfigMarkdown.parse(item).catch(async (err) => {
const message = ConfigMarkdown.FrontmatterError.isInstance(err)
@@ -462,13 +467,15 @@ export namespace Config {
return result
}
+ const PLUGIN_GLOB = new Bun.Glob("{plugin,plugins}/*.{ts,js}")
async function loadPlugin(dir: string) {
const plugins: string[] = []
- for (const item of await Glob.scan("{plugin,plugins}/*.{ts,js}", {
- cwd: dir,
+ for await (const item of PLUGIN_GLOB.scan({
absolute: true,
+ followSymlinks: true,
dot: true,
+ cwd: dir,
})) {
plugins.push(pathToFileURL(item).href)
}
diff --git a/packages/opencode/src/file/ignore.ts b/packages/opencode/src/file/ignore.ts
index 94ffaf5ce..7230f67af 100644
--- a/packages/opencode/src/file/ignore.ts
+++ b/packages/opencode/src/file/ignore.ts
@@ -1,5 +1,4 @@
import { sep } from "node:path"
-import { Glob } from "../util/glob"
export namespace FileIgnore {
const FOLDERS = new Set([
@@ -54,17 +53,19 @@ export namespace FileIgnore {
"**/.nyc_output/**",
]
+ const FILE_GLOBS = FILES.map((p) => new Bun.Glob(p))
+
export const PATTERNS = [...FILES, ...FOLDERS]
export function match(
filepath: string,
opts?: {
- extra?: string[]
- whitelist?: string[]
+ extra?: Bun.Glob[]
+ whitelist?: Bun.Glob[]
},
) {
- for (const pattern of opts?.whitelist || []) {
- if (Glob.match(pattern, filepath)) return false
+ for (const glob of opts?.whitelist || []) {
+ if (glob.match(filepath)) return false
}
const parts = filepath.split(sep)
@@ -73,8 +74,8 @@ export namespace FileIgnore {
}
const extra = opts?.extra || []
- for (const pattern of [...FILES, ...extra]) {
- if (Glob.match(pattern, filepath)) return true
+ for (const glob of [...FILE_GLOBS, ...extra]) {
+ if (glob.match(filepath)) return true
}
return false
diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts
index b4f858dc0..63c1c4cad 100644
--- a/packages/opencode/src/project/project.ts
+++ b/packages/opencode/src/project/project.ts
@@ -13,7 +13,6 @@ import { iife } from "@/util/iife"
import { GlobalBus } from "@/bus/global"
import { existsSync } from "fs"
import { git } from "../util/git"
-import { Glob } from "../util/glob"
export namespace Project {
const log = Log.create({ service: "project" })
@@ -263,11 +262,16 @@ export namespace Project {
if (input.vcs !== "git") return
if (input.icon?.override) return
if (input.icon?.url) return
- const matches = await Glob.scan("**/{favicon}.{ico,png,svg,jpg,jpeg,webp}", {
- cwd: input.worktree,
- absolute: true,
- include: "file",
- })
+ const glob = new Bun.Glob("**/{favicon}.{ico,png,svg,jpg,jpeg,webp}")
+ const matches = await Array.fromAsync(
+ glob.scan({
+ cwd: input.worktree,
+ absolute: true,
+ onlyFiles: true,
+ followSymlinks: false,
+ dot: false,
+ }),
+ )
const shortest = matches.sort((a, b) => a.length - b.length)[0]
if (!shortest) return
const buffer = await Filesystem.readBytes(shortest)
diff --git a/packages/opencode/src/session/instruction.ts b/packages/opencode/src/session/instruction.ts
index 86f73d0fd..d65ada278 100644
--- a/packages/opencode/src/session/instruction.ts
+++ b/packages/opencode/src/session/instruction.ts
@@ -6,7 +6,6 @@ import { Config } from "../config/config"
import { Instance } from "../project/instance"
import { Flag } from "@/flag/flag"
import { Log } from "../util/log"
-import { Glob } from "../util/glob"
import type { MessageV2 } from "./message-v2"
const log = Log.create({ service: "instruction" })
@@ -99,11 +98,13 @@ export namespace InstructionPrompt {
instruction = path.join(os.homedir(), instruction.slice(2))
}
const matches = path.isAbsolute(instruction)
- ? await Glob.scan(path.basename(instruction), {
- cwd: path.dirname(instruction),
- absolute: true,
- include: "file",
- }).catch(() => [])
+ ? await Array.fromAsync(
+ new Bun.Glob(path.basename(instruction)).scan({
+ cwd: path.dirname(instruction),
+ absolute: true,
+ onlyFiles: true,
+ }),
+ ).catch(() => [])
: await resolveRelative(instruction)
matches.forEach((p) => {
paths.add(path.resolve(p))
diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts
index 27065182f..42795b7eb 100644
--- a/packages/opencode/src/skill/skill.ts
+++ b/packages/opencode/src/skill/skill.ts
@@ -12,7 +12,6 @@ import { Flag } from "@/flag/flag"
import { Bus } from "@/bus"
import { Session } from "@/session"
import { Discovery } from "./discovery"
-import { Glob } from "../util/glob"
export namespace Skill {
const log = Log.create({ service: "skill" })
@@ -45,9 +44,10 @@ export namespace Skill {
// External skill directories to search for (project-level and global)
// These follow the directory layout used by Claude Code and other agents.
const EXTERNAL_DIRS = [".claude", ".agents"]
- const EXTERNAL_SKILL_PATTERN = "skills/**/SKILL.md"
- const OPENCODE_SKILL_PATTERN = "{skill,skills}/**/SKILL.md"
- const SKILL_PATTERN = "**/SKILL.md"
+ const EXTERNAL_SKILL_GLOB = new Bun.Glob("skills/**/SKILL.md")
+
+ const OPENCODE_SKILL_GLOB = new Bun.Glob("{skill,skills}/**/SKILL.md")
+ const SKILL_GLOB = new Bun.Glob("**/SKILL.md")
export const state = Instance.state(async () => {
const skills: Record<string, Info> = {}
@@ -88,12 +88,15 @@ export namespace Skill {
}
const scanExternal = async (root: string, scope: "global" | "project") => {
- return Glob.scan(EXTERNAL_SKILL_PATTERN, {
- cwd: root,
- absolute: true,
- include: "file",
- dot: true,
- })
+ return Array.fromAsync(
+ EXTERNAL_SKILL_GLOB.scan({
+ cwd: root,
+ absolute: true,
+ onlyFiles: true,
+ followSymlinks: true,
+ dot: true,
+ }),
+ )
.then((matches) => Promise.all(matches.map(addSkill)))
.catch((error) => {
log.error(`failed to scan ${scope} skills`, { dir: root, error })
@@ -120,12 +123,12 @@ export namespace Skill {
// Scan .opencode/skill/ directories
for (const dir of await Config.directories()) {
- const matches = await Glob.scan(OPENCODE_SKILL_PATTERN, {
+ for await (const match of OPENCODE_SKILL_GLOB.scan({
cwd: dir,
absolute: true,
- include: "file",
- })
- for (const match of matches) {
+ onlyFiles: true,
+ followSymlinks: true,
+ })) {
await addSkill(match)
}
}
@@ -139,12 +142,12 @@ export namespace Skill {
log.warn("skill path not found", { path: resolved })
continue
}
- const matches = await Glob.scan(SKILL_PATTERN, {
+ for await (const match of SKILL_GLOB.scan({
cwd: resolved,
absolute: true,
- include: "file",
- })
- for (const match of matches) {
+ onlyFiles: true,
+ followSymlinks: true,
+ })) {
await addSkill(match)
}
}
@@ -154,12 +157,12 @@ export namespace Skill {
const list = await Discovery.pull(url)
for (const dir of list) {
dirs.add(dir)
- const matches = await Glob.scan(SKILL_PATTERN, {
+ for await (const match of SKILL_GLOB.scan({
cwd: dir,
absolute: true,
- include: "file",
- })
- for (const match of matches) {
+ onlyFiles: true,
+ followSymlinks: true,
+ })) {
await addSkill(match)
}
}
diff --git a/packages/opencode/src/storage/json-migration.ts b/packages/opencode/src/storage/json-migration.ts
index 828ce4799..268442dcf 100644
--- a/packages/opencode/src/storage/json-migration.ts
+++ b/packages/opencode/src/storage/json-migration.ts
@@ -8,7 +8,6 @@ import { SessionShareTable } from "../share/share.sql"
import path from "path"
import { existsSync } from "fs"
import { Filesystem } from "../util/filesystem"
-import { Glob } from "../util/glob"
export namespace JsonMigration {
const log = Log.create({ service: "json-migration" })
@@ -72,7 +71,12 @@ export namespace JsonMigration {
const now = Date.now()
async function list(pattern: string) {
- return Glob.scan(pattern, { cwd: storageDir, absolute: true })
+ const items: string[] = []
+ const scan = new Bun.Glob(pattern)
+ for await (const file of scan.scan({ cwd: storageDir, absolute: true })) {
+ items.push(file)
+ }
+ return items
}
async function read(files: string[], start: number, end: number) {
diff --git a/packages/opencode/src/storage/storage.ts b/packages/opencode/src/storage/storage.ts
index a78ff04f4..691ce3c53 100644
--- a/packages/opencode/src/storage/storage.ts
+++ b/packages/opencode/src/storage/storage.ts
@@ -8,7 +8,6 @@ import { Lock } from "../util/lock"
import { $ } from "bun"
import { NamedError } from "@opencode-ai/util/error"
import z from "zod"
-import { Glob } from "../util/glob"
export namespace Storage {
const log = Log.create({ service: "storage" })
@@ -26,20 +25,17 @@ export namespace Storage {
async (dir) => {
const project = path.resolve(dir, "../project")
if (!(await Filesystem.isDir(project))) return
- const projectDirs = await Glob.scan("*", {
+ for await (const projectDir of new Bun.Glob("*").scan({
cwd: project,
- include: "all",
- })
- for (const projectDir of projectDirs) {
- const fullPath = path.join(project, projectDir)
- if (!(await Filesystem.isDir(fullPath))) continue
+ onlyFiles: false,
+ })) {
log.info(`migrating project ${projectDir}`)
let projectID = projectDir
const fullProjectDir = path.join(project, projectDir)
let worktree = "/"
if (projectID !== "global") {
- for (const msgFile of await Glob.scan("storage/session/message/*/*.json", {
+ for await (const msgFile of new Bun.Glob("storage/session/message/*/*.json").scan({
cwd: path.join(project, projectDir),
absolute: true,
})) {
@@ -75,7 +71,7 @@ export namespace Storage {
})
log.info(`migrating sessions for project ${projectID}`)
- for (const sessionFile of await Glob.scan("storage/session/info/*.json", {
+ for await (const sessionFile of new Bun.Glob("storage/session/info/*.json").scan({
cwd: fullProjectDir,
absolute: true,
})) {
@@ -87,7 +83,7 @@ export namespace Storage {
const session = await Filesystem.readJson<any>(sessionFile)
await Filesystem.writeJson(dest, session)
log.info(`migrating messages for session ${session.id}`)
- for (const msgFile of await Glob.scan(`storage/session/message/${session.id}/*.json`, {
+ for await (const msgFile of new Bun.Glob(`storage/session/message/${session.id}/*.json`).scan({
cwd: fullProjectDir,
absolute: true,
})) {
@@ -100,10 +96,12 @@ export namespace Storage {
await Filesystem.writeJson(dest, message)
log.info(`migrating parts for message ${message.id}`)
- for (const partFile of await Glob.scan(`storage/session/part/${session.id}/${message.id}/*.json`, {
- cwd: fullProjectDir,
- absolute: true,
- })) {
+ for await (const partFile of new Bun.Glob(`storage/session/part/${session.id}/${message.id}/*.json`).scan(
+ {
+ cwd: fullProjectDir,
+ absolute: true,
+ },
+ )) {
const dest = path.join(dir, "part", message.id, path.basename(partFile))
const part = await Filesystem.readJson(partFile)
log.info("copying", {
@@ -118,7 +116,7 @@ export namespace Storage {
}
},
async (dir) => {
- for (const item of await Glob.scan("session/*/*.json", {
+ for await (const item of new Bun.Glob("session/*/*.json").scan({
cwd: dir,
absolute: true,
})) {
@@ -204,13 +202,16 @@ export namespace Storage {
})
}
+ const glob = new Bun.Glob("**/*")
export async function list(prefix: string[]) {
const dir = await state().then((x) => x.dir)
try {
- const result = await Glob.scan("**/*", {
- cwd: path.join(dir, ...prefix),
- include: "file",
- }).then((results) => results.map((x) => [...prefix, ...x.slice(0, -5).split(path.sep)]))
+ const result = await Array.fromAsync(
+ glob.scan({
+ cwd: path.join(dir, ...prefix),
+ onlyFiles: true,
+ }),
+ ).then((results) => results.map((x) => [...prefix, ...x.slice(0, -5).split(path.sep)]))
result.sort()
return result
} catch {
diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts
index 649c495d2..3ff9cce89 100644
--- a/packages/opencode/src/tool/registry.ts
+++ b/packages/opencode/src/tool/registry.ts
@@ -27,16 +27,16 @@ import { LspTool } from "./lsp"
import { Truncate } from "./truncation"
import { PlanExitTool, PlanEnterTool } from "./plan"
import { ApplyPatchTool } from "./apply_patch"
-import { Glob } from "../util/glob"
export namespace ToolRegistry {
const log = Log.create({ service: "tool.registry" })
export const state = Instance.state(async () => {
const custom = [] as Tool.Info[]
+ const glob = new Bun.Glob("{tool,tools}/*.{js,ts}")
const matches = await Config.directories().then((dirs) =>
- dirs.flatMap((dir) => Glob.scanSync("{tool,tools}/*.{js,ts}", { cwd: dir, absolute: true, dot: true })),
+ dirs.flatMap((dir) => [...glob.scanSync({ cwd: dir, absolute: true, followSymlinks: true, dot: true })]),
)
if (matches.length) await Config.waitForDependencies()
for (const match of matches) {
diff --git a/packages/opencode/src/tool/truncation.ts b/packages/opencode/src/tool/truncation.ts
index 58b0cc13d..4cc524aee 100644
--- a/packages/opencode/src/tool/truncation.ts
+++ b/packages/opencode/src/tool/truncation.ts
@@ -6,7 +6,6 @@ import { PermissionNext } from "../permission/next"
import type { Agent } from "../agent/agent"
import { Scheduler } from "../scheduler"
import { Filesystem } from "../util/filesystem"
-import { Glob } from "../util/glob"
export namespace Truncate {
export const MAX_LINES = 2000
@@ -35,7 +34,8 @@ export namespace Truncate {
export async function cleanup() {
const cutoff = Identifier.timestamp(Identifier.create("tool", false, Date.now() - RETENTION_MS))
- const entries = await Glob.scan("tool_*", { cwd: DIR, include: "file" }).catch(() => [] as string[])
+ const glob = new Bun.Glob("tool_*")
+ const entries = await Array.fromAsync(glob.scan({ cwd: DIR, onlyFiles: true })).catch(() => [] as string[])
for (const entry of entries) {
if (Identifier.timestamp(entry) >= cutoff) continue
await fs.unlink(path.join(DIR, entry)).catch(() => {})
diff --git a/packages/opencode/src/util/filesystem.ts b/packages/opencode/src/util/filesystem.ts
index 3a1e8b8ec..575e61406 100644
--- a/packages/opencode/src/util/filesystem.ts
+++ b/packages/opencode/src/util/filesystem.ts
@@ -5,7 +5,6 @@ import { realpathSync } from "fs"
import { dirname, join, relative } from "path"
import { Readable } from "stream"
import { pipeline } from "stream/promises"
-import { Glob } from "./glob"
export namespace Filesystem {
// Fast sync version for metadata checks
@@ -157,13 +156,16 @@ export namespace Filesystem {
const result = []
while (true) {
try {
- const matches = await Glob.scan(pattern, {
+ const glob = new Bun.Glob(pattern)
+ for await (const match of glob.scan({
cwd: current,
absolute: true,
- include: "file",
+ onlyFiles: true,
+ followSymlinks: true,
dot: true,
- })
- result.push(...matches)
+ })) {
+ result.push(match)
+ }
} catch {
// Skip invalid glob patterns
}
diff --git a/packages/opencode/src/util/glob.ts b/packages/opencode/src/util/glob.ts
deleted file mode 100644
index e4df4c4e8..000000000
--- a/packages/opencode/src/util/glob.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { glob, globSync, type GlobOptions } from "glob"
-import { minimatch } from "minimatch"
-
-export namespace Glob {
- export interface Options {
- cwd?: string
- absolute?: boolean
- include?: "file" | "all"
- dot?: boolean
- symlink?: boolean
- }
-
- function toGlobOptions(options: Options): GlobOptions {
- return {
- cwd: options.cwd,
- absolute: options.absolute,
- dot: options.dot,
- follow: options.symlink ?? false,
- nodir: options.include === "file",
- }
- }
-
- export async function scan(pattern: string, options: Options = {}): Promise<string[]> {
- return glob(pattern, toGlobOptions(options)) as Promise<string[]>
- }
-
- export function scanSync(pattern: string, options: Options = {}): string[] {
- return globSync(pattern, toGlobOptions(options)) as string[]
- }
-
- export function match(pattern: string, filepath: string): boolean {
- return minimatch(filepath, pattern, { dot: true })
- }
-}
diff --git a/packages/opencode/src/util/log.ts b/packages/opencode/src/util/log.ts
index 2ca4c0a3d..c62d59299 100644
--- a/packages/opencode/src/util/log.ts
+++ b/packages/opencode/src/util/log.ts
@@ -3,7 +3,6 @@ import fs from "fs/promises"
import { createWriteStream } from "fs"
import { Global } from "../global"
import z from "zod"
-import { Glob } from "./glob"
export namespace Log {
export const Level = z.enum(["DEBUG", "INFO", "WARN", "ERROR"]).meta({ ref: "LogLevel", description: "Log level" })
@@ -78,11 +77,13 @@ export namespace Log {
}
async function cleanup(dir: string) {
- const files = await Glob.scan("????-??-??T??????.log", {
- cwd: dir,
- absolute: true,
- include: "file",
- })
+ const glob = new Bun.Glob("????-??-??T??????.log")
+ const files = await Array.fromAsync(
+ glob.scan({
+ cwd: dir,
+ absolute: true,
+ }),
+ )
if (files.length <= 5) return
const filesToDelete = files.slice(0, -10)
diff --git a/packages/opencode/test/util/glob.test.ts b/packages/opencode/test/util/glob.test.ts
deleted file mode 100644
index a12489655..000000000
--- a/packages/opencode/test/util/glob.test.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-import { describe, test, expect } from "bun:test"
-import path from "path"
-import fs from "fs/promises"
-import { Glob } from "../../src/util/glob"
-import { tmpdir } from "../fixture/fixture"
-
-describe("glob", () => {
- describe("glob()", () => {
- test("finds files matching pattern", async () => {
- await using tmp = await tmpdir()
- await fs.writeFile(path.join(tmp.path, "test.txt"), "content", "utf-8")
- await fs.writeFile(path.join(tmp.path, "other.txt"), "content", "utf-8")
- await fs.writeFile(path.join(tmp.path, "skip.md"), "content", "utf-8")
-
- const results = await Glob.scan("*.txt", { cwd: tmp.path })
-
- expect(results.sort()).toEqual(["other.txt", "test.txt"])
- })
-
- test("returns absolute paths when absolute option is true", async () => {
- await using tmp = await tmpdir()
- await fs.writeFile(path.join(tmp.path, "test.txt"), "content", "utf-8")
-
- const results = await Glob.scan("*.txt", { cwd: tmp.path, absolute: true })
-
- expect(results[0]).toStartWith(tmp.path)
- expect(path.isAbsolute(results[0])).toBe(true)
- })
-
- test("filters to only files when include is 'file'", async () => {
- await using tmp = await tmpdir()
- await fs.mkdir(path.join(tmp.path, "subdir"))
- await fs.writeFile(path.join(tmp.path, "file.txt"), "content", "utf-8")
-
- const results = await Glob.scan("*", { cwd: tmp.path, include: "file" })
-
- expect(results).toEqual(["file.txt"])
- })
-
- test("includes both files and directories when include is 'all'", async () => {
- await using tmp = await tmpdir()
- await fs.mkdir(path.join(tmp.path, "subdir"))
- await fs.writeFile(path.join(tmp.path, "file.txt"), "content", "utf-8")
-
- const results = await Glob.scan("*", { cwd: tmp.path, include: "all" })
-
- expect(results.sort()).toEqual(["file.txt", "subdir"])
- })
-
- test("handles nested patterns", async () => {
- await using tmp = await tmpdir()
- await fs.mkdir(path.join(tmp.path, "nested"), { recursive: true })
- await fs.writeFile(path.join(tmp.path, "nested", "deep.txt"), "content", "utf-8")
-
- const results = await Glob.scan("**/*.txt", { cwd: tmp.path })
-
- expect(results).toEqual(["nested/deep.txt"])
- })
-
- test("returns empty array for no matches", async () => {
- await using tmp = await tmpdir()
-
- const results = await Glob.scan("*.nonexistent", { cwd: tmp.path })
-
- expect(results).toEqual([])
- })
- })
-
- describe("match()", () => {
- test("matches simple patterns", () => {
- expect(Glob.match("*.txt", "file.txt")).toBe(true)
- expect(Glob.match("*.txt", "file.js")).toBe(false)
- })
-
- test("matches directory patterns", () => {
- expect(Glob.match("**/*.js", "src/index.js")).toBe(true)
- expect(Glob.match("**/*.js", "src/index.ts")).toBe(false)
- })
-
- test("matches dot files", () => {
- expect(Glob.match(".*", ".gitignore")).toBe(true)
- expect(Glob.match("**/*.md", ".github/README.md")).toBe(true)
- })
-
- test("matches brace expansion", () => {
- expect(Glob.match("*.{js,ts}", "file.js")).toBe(true)
- expect(Glob.match("*.{js,ts}", "file.ts")).toBe(true)
- expect(Glob.match("*.{js,ts}", "file.py")).toBe(false)
- })
- })
-})