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": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], - "glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="], + "glob": ["glob@11.1.0", "", { "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": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -3076,7 +3074,7 @@ "lower-case": ["lower-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg=="], - "lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], + "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], "lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="], @@ -4788,14 +4786,14 @@ "openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="], - "openid-client/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], - "p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + "path-scurry/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], + "pixelmatch/pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="], "pkg-up/find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="], @@ -4868,9 +4866,9 @@ "unifont/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], - "utif2/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], + "unstorage/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], - "vite-plugin-icons-spritesheet/glob": ["glob@11.1.0", "", { "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": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], "vitest/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], @@ -5228,6 +5226,8 @@ "astro/unstorage/h3": ["h3@1.15.5", "", { "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": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], + "astro/unstorage/ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], "aws-sdk/xml2js/sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="], @@ -5358,8 +5358,6 @@ "type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "vite-plugin-icons-spritesheet/glob/minimatch": ["minimatch@10.2.1", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A=="], - "wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="], "wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "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 = {} 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 = {} - 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 = {} - 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 = {} - 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 = {} @@ -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(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 { - return glob(pattern, toGlobOptions(options)) as Promise - } - - 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) - }) - }) -})