mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-20 07:44:23 +00:00
Compare commits
5 Commits
update-sta
...
feature/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
deaf9c956f | ||
|
|
3e0dc15b59 | ||
|
|
01b5e6487c | ||
|
|
9657d1bbfd | ||
|
|
bbfb7e95e0 |
18
bun.lock
18
bun.lock
@@ -15,6 +15,7 @@
|
||||
"@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",
|
||||
@@ -321,6 +322,7 @@
|
||||
"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:",
|
||||
@@ -2694,7 +2696,7 @@
|
||||
|
||||
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
|
||||
|
||||
"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": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="],
|
||||
|
||||
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
@@ -3074,7 +3076,7 @@
|
||||
|
||||
"lower-case": ["lower-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg=="],
|
||||
|
||||
"lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
|
||||
"lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="],
|
||||
|
||||
"lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="],
|
||||
|
||||
@@ -4786,14 +4788,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=="],
|
||||
@@ -4866,10 +4868,10 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
"unstorage/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="],
|
||||
|
||||
"utif2/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"vitest/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
||||
|
||||
"vitest/vite": ["vite@7.1.10", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA=="],
|
||||
@@ -5226,8 +5228,6 @@
|
||||
|
||||
"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,6 +5358,8 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
"@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",
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from "node:fs/promises"
|
||||
import net from "node:net"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
import { Process } from "../../opencode/src/util/process"
|
||||
|
||||
async function freePort() {
|
||||
return await new Promise<number>((resolve, reject) => {
|
||||
@@ -84,8 +85,8 @@ const runnerEnv = {
|
||||
PLAYWRIGHT_PORT: String(webPort),
|
||||
} satisfies Record<string, string>
|
||||
|
||||
let seed: ReturnType<typeof Bun.spawn> | undefined
|
||||
let runner: ReturnType<typeof Bun.spawn> | undefined
|
||||
let seed: Process.Child | undefined
|
||||
let runner: Process.Child | undefined
|
||||
let server: { stop: () => Promise<void> | void } | undefined
|
||||
let inst: { Instance: { disposeAll: () => Promise<void> | void } } | undefined
|
||||
let cleaned = false
|
||||
@@ -131,7 +132,7 @@ process.once("unhandledRejection", (error) => {
|
||||
let code = 1
|
||||
|
||||
try {
|
||||
seed = Bun.spawn(["bun", "script/seed-e2e.ts"], {
|
||||
seed = Process.spawn(["bun", "script/seed-e2e.ts"], {
|
||||
cwd: opencodeDir,
|
||||
env: serverEnv,
|
||||
stdout: "inherit",
|
||||
@@ -160,7 +161,7 @@ try {
|
||||
console.log(`opencode server listening on http://127.0.0.1:${serverPort}`)
|
||||
|
||||
await waitForHealth(`http://127.0.0.1:${serverPort}/global/health`)
|
||||
runner = Bun.spawn(["bun", "test:e2e", ...extraArgs], {
|
||||
runner = Process.spawn(["bun", "test:e2e", ...extraArgs], {
|
||||
cwd: appDir,
|
||||
env: runnerEnv,
|
||||
stdout: "inherit",
|
||||
|
||||
@@ -107,6 +107,7 @@
|
||||
"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:",
|
||||
|
||||
@@ -8,16 +8,17 @@ import { readableStreamToText } from "bun"
|
||||
import { Lock } from "../util/lock"
|
||||
import { PackageRegistry } from "./registry"
|
||||
import { proxied } from "@/util/proxied"
|
||||
import { Process } from "../util/process"
|
||||
|
||||
export namespace BunProc {
|
||||
const log = Log.create({ service: "bun" })
|
||||
|
||||
export async function run(cmd: string[], options?: Bun.SpawnOptions.OptionsObject<any, any, any>) {
|
||||
export async function run(cmd: string[], options?: Process.Options) {
|
||||
log.info("running", {
|
||||
cmd: [which(), ...cmd],
|
||||
...options,
|
||||
})
|
||||
const result = Bun.spawn([which(), ...cmd], {
|
||||
const result = Process.spawn([which(), ...cmd], {
|
||||
...options,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
@@ -28,16 +29,8 @@ export namespace BunProc {
|
||||
},
|
||||
})
|
||||
const code = await result.exited
|
||||
const stdout = result.stdout
|
||||
? typeof result.stdout === "number"
|
||||
? result.stdout
|
||||
: await readableStreamToText(result.stdout)
|
||||
: undefined
|
||||
const stderr = result.stderr
|
||||
? typeof result.stderr === "number"
|
||||
? result.stderr
|
||||
: await readableStreamToText(result.stderr)
|
||||
: undefined
|
||||
const stdout = result.stdout ? await readableStreamToText(result.stdout) : undefined
|
||||
const stderr = result.stderr ? await readableStreamToText(result.stderr) : undefined
|
||||
log.info("done", {
|
||||
code,
|
||||
stdout,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { readableStreamToText, semver } from "bun"
|
||||
import { Log } from "../util/log"
|
||||
import { Process } from "../util/process"
|
||||
|
||||
export namespace PackageRegistry {
|
||||
const log = Log.create({ service: "bun" })
|
||||
@@ -9,7 +10,7 @@ export namespace PackageRegistry {
|
||||
}
|
||||
|
||||
export async function info(pkg: string, field: string, cwd?: string): Promise<string | null> {
|
||||
const result = Bun.spawn([which(), "info", pkg, field], {
|
||||
const result = Process.spawn([which(), "info", pkg, field], {
|
||||
cwd,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Global } from "../../global"
|
||||
import { Plugin } from "../../plugin"
|
||||
import { Instance } from "../../project/instance"
|
||||
import type { Hooks } from "@opencode-ai/plugin"
|
||||
import { Process } from "../../util/process"
|
||||
|
||||
type PluginAuth = NonNullable<Hooks["auth"]>
|
||||
|
||||
@@ -263,8 +264,7 @@ export const AuthLoginCommand = cmd({
|
||||
if (args.url) {
|
||||
const wellknown = await fetch(`${args.url}/.well-known/opencode`).then((x) => x.json() as any)
|
||||
prompts.log.info(`Running \`${wellknown.auth.command.join(" ")}\``)
|
||||
const proc = Bun.spawn({
|
||||
cmd: wellknown.auth.command,
|
||||
const proc = Process.spawn(wellknown.auth.command, {
|
||||
stdout: "pipe",
|
||||
})
|
||||
const exit = await proc.exited
|
||||
@@ -273,6 +273,11 @@ export const AuthLoginCommand = cmd({
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
if (!proc.stdout) {
|
||||
prompts.log.error("Failed")
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
const token = await new Response(proc.stdout).text()
|
||||
await Auth.set(args.url, {
|
||||
type: "wellknown",
|
||||
|
||||
@@ -6,6 +6,7 @@ import { UI } from "../ui"
|
||||
import { Locale } from "../../util/locale"
|
||||
import { Flag } from "../../flag/flag"
|
||||
import { Filesystem } from "../../util/filesystem"
|
||||
import { Process } from "../../util/process"
|
||||
import { EOL } from "os"
|
||||
import path from "path"
|
||||
|
||||
@@ -102,13 +103,17 @@ export const SessionListCommand = cmd({
|
||||
const shouldPaginate = process.stdout.isTTY && !args.maxCount && args.format === "table"
|
||||
|
||||
if (shouldPaginate) {
|
||||
const proc = Bun.spawn({
|
||||
cmd: pagerCmd(),
|
||||
const proc = Process.spawn(pagerCmd(), {
|
||||
stdin: "pipe",
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
})
|
||||
|
||||
if (!proc.stdin) {
|
||||
console.log(output)
|
||||
return
|
||||
}
|
||||
|
||||
proc.stdin.write(output)
|
||||
proc.stdin.end()
|
||||
await proc.exited
|
||||
|
||||
@@ -3,6 +3,7 @@ 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" }
|
||||
@@ -391,7 +392,6 @@ 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,11 +405,11 @@ async function getCustomThemes() {
|
||||
|
||||
const result: Record<string, ThemeJson> = {}
|
||||
for (const dir of directories) {
|
||||
for await (const item of CUSTOM_THEME_GLOB.scan({
|
||||
absolute: true,
|
||||
followSymlinks: true,
|
||||
dot: true,
|
||||
for (const item of await Glob.scan("themes/*.json", {
|
||||
cwd: dir,
|
||||
absolute: true,
|
||||
dot: true,
|
||||
symlink: true,
|
||||
})) {
|
||||
const name = path.basename(item, ".json")
|
||||
result[name] = await Filesystem.readJson(item)
|
||||
|
||||
@@ -5,6 +5,7 @@ import { lazy } from "../../../../util/lazy.js"
|
||||
import { tmpdir } from "os"
|
||||
import path from "path"
|
||||
import { Filesystem } from "../../../../util/filesystem"
|
||||
import { Process } from "../../../../util/process"
|
||||
|
||||
/**
|
||||
* Writes text to clipboard via OSC 52 escape sequence.
|
||||
@@ -87,7 +88,8 @@ export namespace Clipboard {
|
||||
if (process.env["WAYLAND_DISPLAY"] && Bun.which("wl-copy")) {
|
||||
console.log("clipboard: using wl-copy")
|
||||
return async (text: string) => {
|
||||
const proc = Bun.spawn(["wl-copy"], { stdin: "pipe", stdout: "ignore", stderr: "ignore" })
|
||||
const proc = Process.spawn(["wl-copy"], { stdin: "pipe", stdout: "ignore", stderr: "ignore" })
|
||||
if (!proc.stdin) return
|
||||
proc.stdin.write(text)
|
||||
proc.stdin.end()
|
||||
await proc.exited.catch(() => {})
|
||||
@@ -96,11 +98,12 @@ export namespace Clipboard {
|
||||
if (Bun.which("xclip")) {
|
||||
console.log("clipboard: using xclip")
|
||||
return async (text: string) => {
|
||||
const proc = Bun.spawn(["xclip", "-selection", "clipboard"], {
|
||||
const proc = Process.spawn(["xclip", "-selection", "clipboard"], {
|
||||
stdin: "pipe",
|
||||
stdout: "ignore",
|
||||
stderr: "ignore",
|
||||
})
|
||||
if (!proc.stdin) return
|
||||
proc.stdin.write(text)
|
||||
proc.stdin.end()
|
||||
await proc.exited.catch(() => {})
|
||||
@@ -109,11 +112,12 @@ export namespace Clipboard {
|
||||
if (Bun.which("xsel")) {
|
||||
console.log("clipboard: using xsel")
|
||||
return async (text: string) => {
|
||||
const proc = Bun.spawn(["xsel", "--clipboard", "--input"], {
|
||||
const proc = Process.spawn(["xsel", "--clipboard", "--input"], {
|
||||
stdin: "pipe",
|
||||
stdout: "ignore",
|
||||
stderr: "ignore",
|
||||
})
|
||||
if (!proc.stdin) return
|
||||
proc.stdin.write(text)
|
||||
proc.stdin.end()
|
||||
await proc.exited.catch(() => {})
|
||||
@@ -125,7 +129,7 @@ export namespace Clipboard {
|
||||
console.log("clipboard: using powershell")
|
||||
return async (text: string) => {
|
||||
// Pipe via stdin to avoid PowerShell string interpolation ($env:FOO, $(), etc.)
|
||||
const proc = Bun.spawn(
|
||||
const proc = Process.spawn(
|
||||
[
|
||||
"powershell.exe",
|
||||
"-NonInteractive",
|
||||
@@ -140,6 +144,7 @@ export namespace Clipboard {
|
||||
},
|
||||
)
|
||||
|
||||
if (!proc.stdin) return
|
||||
proc.stdin.write(text)
|
||||
proc.stdin.end()
|
||||
await proc.exited.catch(() => {})
|
||||
|
||||
@@ -4,6 +4,7 @@ import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { CliRenderer } from "@opentui/core"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { Process } from "@/util/process"
|
||||
|
||||
export namespace Editor {
|
||||
export async function open(opts: { value: string; renderer: CliRenderer }): Promise<string | undefined> {
|
||||
@@ -17,8 +18,7 @@ export namespace Editor {
|
||||
opts.renderer.suspend()
|
||||
opts.renderer.currentRenderBuffer.clear()
|
||||
const parts = editor.split(" ")
|
||||
const proc = Bun.spawn({
|
||||
cmd: [...parts, filepath],
|
||||
const proc = Process.spawn([...parts, filepath], {
|
||||
stdin: "inherit",
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
|
||||
@@ -28,6 +28,7 @@ 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"
|
||||
@@ -351,14 +352,13 @@ 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 await (const item of COMMAND_GLOB.scan({
|
||||
absolute: true,
|
||||
followSymlinks: true,
|
||||
dot: true,
|
||||
for (const item of await Glob.scan("{command,commands}/**/*.md", {
|
||||
cwd: dir,
|
||||
absolute: true,
|
||||
dot: true,
|
||||
symlink: true,
|
||||
})) {
|
||||
const md = await ConfigMarkdown.parse(item).catch(async (err) => {
|
||||
const message = ConfigMarkdown.FrontmatterError.isInstance(err)
|
||||
@@ -390,15 +390,14 @@ 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 await (const item of AGENT_GLOB.scan({
|
||||
absolute: true,
|
||||
followSymlinks: true,
|
||||
dot: true,
|
||||
for (const item of await Glob.scan("{agent,agents}/**/*.md", {
|
||||
cwd: dir,
|
||||
absolute: true,
|
||||
dot: true,
|
||||
symlink: true,
|
||||
})) {
|
||||
const md = await ConfigMarkdown.parse(item).catch(async (err) => {
|
||||
const message = ConfigMarkdown.FrontmatterError.isInstance(err)
|
||||
@@ -430,14 +429,13 @@ 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 await (const item of MODE_GLOB.scan({
|
||||
absolute: true,
|
||||
followSymlinks: true,
|
||||
dot: true,
|
||||
for (const item of await Glob.scan("{mode,modes}/*.md", {
|
||||
cwd: dir,
|
||||
absolute: true,
|
||||
dot: true,
|
||||
symlink: true,
|
||||
})) {
|
||||
const md = await ConfigMarkdown.parse(item).catch(async (err) => {
|
||||
const message = ConfigMarkdown.FrontmatterError.isInstance(err)
|
||||
@@ -467,15 +465,14 @@ export namespace Config {
|
||||
return result
|
||||
}
|
||||
|
||||
const PLUGIN_GLOB = new Bun.Glob("{plugin,plugins}/*.{ts,js}")
|
||||
async function loadPlugin(dir: string) {
|
||||
const plugins: string[] = []
|
||||
|
||||
for await (const item of PLUGIN_GLOB.scan({
|
||||
absolute: true,
|
||||
followSymlinks: true,
|
||||
dot: true,
|
||||
for (const item of await Glob.scan("{plugin,plugins}/*.{ts,js}", {
|
||||
cwd: dir,
|
||||
absolute: true,
|
||||
dot: true,
|
||||
symlink: true,
|
||||
})) {
|
||||
plugins.push(pathToFileURL(item).href)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { sep } from "node:path"
|
||||
import { Glob } from "../util/glob"
|
||||
|
||||
export namespace FileIgnore {
|
||||
const FOLDERS = new Set([
|
||||
@@ -53,19 +54,17 @@ 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?: Bun.Glob[]
|
||||
whitelist?: Bun.Glob[]
|
||||
extra?: string[]
|
||||
whitelist?: string[]
|
||||
},
|
||||
) {
|
||||
for (const glob of opts?.whitelist || []) {
|
||||
if (glob.match(filepath)) return false
|
||||
for (const pattern of opts?.whitelist || []) {
|
||||
if (Glob.match(pattern, filepath)) return false
|
||||
}
|
||||
|
||||
const parts = filepath.split(sep)
|
||||
@@ -74,8 +73,8 @@ export namespace FileIgnore {
|
||||
}
|
||||
|
||||
const extra = opts?.extra || []
|
||||
for (const glob of [...FILE_GLOBS, ...extra]) {
|
||||
if (glob.match(filepath)) return true
|
||||
for (const pattern of [...FILES, ...extra]) {
|
||||
if (Glob.match(pattern, filepath)) return true
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
@@ -7,6 +7,7 @@ import { NamedError } from "@opencode-ai/util/error"
|
||||
import { lazy } from "../util/lazy"
|
||||
import { $ } from "bun"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { Process } from "../util/process"
|
||||
|
||||
import { ZipReader, BlobReader, BlobWriter } from "@zip.js/zip.js"
|
||||
import { Log } from "@/util/log"
|
||||
@@ -153,7 +154,7 @@ export namespace Ripgrep {
|
||||
if (platformKey.endsWith("-darwin")) args.push("--include=*/rg")
|
||||
if (platformKey.endsWith("-linux")) args.push("--wildcards", "*/rg")
|
||||
|
||||
const proc = Bun.spawn(args, {
|
||||
const proc = Process.spawn(args, {
|
||||
cwd: Global.Path.bin,
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
@@ -162,7 +163,7 @@ export namespace Ripgrep {
|
||||
if (proc.exitCode !== 0)
|
||||
throw new ExtractionFailedError({
|
||||
filepath,
|
||||
stderr: await Bun.readableStreamToText(proc.stderr),
|
||||
stderr: proc.stderr ? await Bun.readableStreamToText(proc.stderr) : "",
|
||||
})
|
||||
}
|
||||
if (config.extension === "zip") {
|
||||
@@ -227,8 +228,7 @@ export namespace Ripgrep {
|
||||
}
|
||||
}
|
||||
|
||||
// Bun.spawn should throw this, but it incorrectly reports that the executable does not exist.
|
||||
// See https://github.com/oven-sh/bun/issues/24012
|
||||
// Guard against invalid cwd to provide a consistent ENOENT error.
|
||||
if (!(await fs.stat(input.cwd).catch(() => undefined))?.isDirectory()) {
|
||||
throw Object.assign(new Error(`No such file or directory: '${input.cwd}'`), {
|
||||
code: "ENOENT",
|
||||
@@ -237,14 +237,17 @@ export namespace Ripgrep {
|
||||
})
|
||||
}
|
||||
|
||||
const proc = Bun.spawn(args, {
|
||||
const proc = Process.spawn(args, {
|
||||
cwd: input.cwd,
|
||||
stdout: "pipe",
|
||||
stderr: "ignore",
|
||||
maxBuffer: 1024 * 1024 * 20,
|
||||
signal: input.signal,
|
||||
})
|
||||
|
||||
if (!proc.stdout) {
|
||||
throw new Error("Process output not available")
|
||||
}
|
||||
|
||||
const reader = proc.stdout.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ""
|
||||
|
||||
@@ -2,6 +2,7 @@ import { readableStreamToText } from "bun"
|
||||
import { BunProc } from "../bun"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { Process } from "../util/process"
|
||||
import { Flag } from "@/flag/flag"
|
||||
|
||||
export interface Info {
|
||||
@@ -213,11 +214,12 @@ export const rlang: Info = {
|
||||
if (airPath == null) return false
|
||||
|
||||
try {
|
||||
const proc = Bun.spawn(["air", "--help"], {
|
||||
const proc = Process.spawn(["air", "--help"], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
})
|
||||
await proc.exited
|
||||
if (!proc.stdout) return false
|
||||
const output = await readableStreamToText(proc.stdout)
|
||||
|
||||
// Check for "Air: An R language server and formatter"
|
||||
@@ -238,7 +240,7 @@ export const uvformat: Info = {
|
||||
async enabled() {
|
||||
if (await ruff.enabled()) return false
|
||||
if (Bun.which("uv") !== null) {
|
||||
const proc = Bun.spawn(["uv", "format", "--help"], { stderr: "pipe", stdout: "pipe" })
|
||||
const proc = Process.spawn(["uv", "format", "--help"], { stderr: "pipe", stdout: "pipe" })
|
||||
const code = await proc.exited
|
||||
return code === 0
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import * as Formatter from "./formatter"
|
||||
import { Config } from "../config/config"
|
||||
import { mergeDeep } from "remeda"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Process } from "../util/process"
|
||||
|
||||
export namespace Format {
|
||||
const log = Log.create({ service: "format" })
|
||||
@@ -110,13 +111,15 @@ export namespace Format {
|
||||
for (const item of await getFormatter(ext)) {
|
||||
log.info("running", { command: item.command })
|
||||
try {
|
||||
const proc = Bun.spawn({
|
||||
cmd: item.command.map((x) => x.replace("$FILE", file)),
|
||||
cwd: Instance.directory,
|
||||
env: { ...process.env, ...item.environment },
|
||||
stdout: "ignore",
|
||||
stderr: "ignore",
|
||||
})
|
||||
const proc = Process.spawn(
|
||||
item.command.map((x) => x.replace("$FILE", file)),
|
||||
{
|
||||
cwd: Instance.directory,
|
||||
env: { ...process.env, ...item.environment },
|
||||
stdout: "ignore",
|
||||
stderr: "ignore",
|
||||
},
|
||||
)
|
||||
const exit = await proc.exited
|
||||
if (exit !== 0)
|
||||
log.error("failed", {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Filesystem } from "../util/filesystem"
|
||||
import { Instance } from "../project/instance"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { Archive } from "../util/archive"
|
||||
import { Process } from "../util/process"
|
||||
|
||||
export namespace LSPServer {
|
||||
const log = Log.create({ service: "lsp.server" })
|
||||
@@ -133,7 +134,7 @@ export namespace LSPServer {
|
||||
)
|
||||
if (!(await Filesystem.exists(js))) {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
await Bun.spawn([BunProc.which(), "install", "@vue/language-server"], {
|
||||
await Process.spawn([BunProc.which(), "install", "@vue/language-server"], {
|
||||
cwd: Global.Path.bin,
|
||||
env: {
|
||||
...process.env,
|
||||
@@ -263,14 +264,16 @@ export namespace LSPServer {
|
||||
}
|
||||
|
||||
if (lintBin) {
|
||||
const proc = Bun.spawn([lintBin, "--help"], { stdout: "pipe" })
|
||||
const proc = Process.spawn([lintBin, "--help"], { stdout: "pipe" })
|
||||
await proc.exited
|
||||
const help = await readableStreamToText(proc.stdout)
|
||||
if (help.includes("--lsp")) {
|
||||
return {
|
||||
process: spawn(lintBin, ["--lsp"], {
|
||||
cwd: root,
|
||||
}),
|
||||
if (proc.stdout) {
|
||||
const help = await readableStreamToText(proc.stdout)
|
||||
if (help.includes("--lsp")) {
|
||||
return {
|
||||
process: spawn(lintBin, ["--lsp"], {
|
||||
cwd: root,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -372,8 +375,7 @@ export namespace LSPServer {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
|
||||
log.info("installing gopls")
|
||||
const proc = Bun.spawn({
|
||||
cmd: ["go", "install", "golang.org/x/tools/gopls@latest"],
|
||||
const proc = Process.spawn(["go", "install", "golang.org/x/tools/gopls@latest"], {
|
||||
env: { ...process.env, GOBIN: Global.Path.bin },
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
@@ -414,8 +416,7 @@ export namespace LSPServer {
|
||||
}
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
log.info("installing rubocop")
|
||||
const proc = Bun.spawn({
|
||||
cmd: ["gem", "install", "rubocop", "--bindir", Global.Path.bin],
|
||||
const proc = Process.spawn(["gem", "install", "rubocop", "--bindir", Global.Path.bin], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
stdin: "pipe",
|
||||
@@ -513,7 +514,7 @@ export namespace LSPServer {
|
||||
const js = path.join(Global.Path.bin, "node_modules", "pyright", "dist", "pyright-langserver.js")
|
||||
if (!(await Filesystem.exists(js))) {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
await Bun.spawn([BunProc.which(), "install", "pyright"], {
|
||||
await Process.spawn([BunProc.which(), "install", "pyright"], {
|
||||
cwd: Global.Path.bin,
|
||||
env: {
|
||||
...process.env,
|
||||
@@ -746,8 +747,7 @@ export namespace LSPServer {
|
||||
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
log.info("installing csharp-ls via dotnet tool")
|
||||
const proc = Bun.spawn({
|
||||
cmd: ["dotnet", "tool", "install", "csharp-ls", "--tool-path", Global.Path.bin],
|
||||
const proc = Process.spawn(["dotnet", "tool", "install", "csharp-ls", "--tool-path", Global.Path.bin], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
stdin: "pipe",
|
||||
@@ -786,8 +786,7 @@ export namespace LSPServer {
|
||||
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
log.info("installing fsautocomplete via dotnet tool")
|
||||
const proc = Bun.spawn({
|
||||
cmd: ["dotnet", "tool", "install", "fsautocomplete", "--tool-path", Global.Path.bin],
|
||||
const proc = Process.spawn(["dotnet", "tool", "install", "fsautocomplete", "--tool-path", Global.Path.bin], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
stdin: "pipe",
|
||||
@@ -1047,7 +1046,7 @@ export namespace LSPServer {
|
||||
const js = path.join(Global.Path.bin, "node_modules", "svelte-language-server", "bin", "server.js")
|
||||
if (!(await Filesystem.exists(js))) {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
await Bun.spawn([BunProc.which(), "install", "svelte-language-server"], {
|
||||
await Process.spawn([BunProc.which(), "install", "svelte-language-server"], {
|
||||
cwd: Global.Path.bin,
|
||||
env: {
|
||||
...process.env,
|
||||
@@ -1094,7 +1093,7 @@ export namespace LSPServer {
|
||||
const js = path.join(Global.Path.bin, "node_modules", "@astrojs", "language-server", "bin", "nodeServer.js")
|
||||
if (!(await Filesystem.exists(js))) {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
await Bun.spawn([BunProc.which(), "install", "@astrojs/language-server"], {
|
||||
await Process.spawn([BunProc.which(), "install", "@astrojs/language-server"], {
|
||||
cwd: Global.Path.bin,
|
||||
env: {
|
||||
...process.env,
|
||||
@@ -1339,7 +1338,7 @@ export namespace LSPServer {
|
||||
const exists = await Filesystem.exists(js)
|
||||
if (!exists) {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
await Bun.spawn([BunProc.which(), "install", "yaml-language-server"], {
|
||||
await Process.spawn([BunProc.which(), "install", "yaml-language-server"], {
|
||||
cwd: Global.Path.bin,
|
||||
env: {
|
||||
...process.env,
|
||||
@@ -1518,7 +1517,7 @@ export namespace LSPServer {
|
||||
const js = path.join(Global.Path.bin, "node_modules", "intelephense", "lib", "intelephense.js")
|
||||
if (!(await Filesystem.exists(js))) {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
await Bun.spawn([BunProc.which(), "install", "intelephense"], {
|
||||
await Process.spawn([BunProc.which(), "install", "intelephense"], {
|
||||
cwd: Global.Path.bin,
|
||||
env: {
|
||||
...process.env,
|
||||
@@ -1615,7 +1614,7 @@ export namespace LSPServer {
|
||||
const js = path.join(Global.Path.bin, "node_modules", "bash-language-server", "out", "cli.js")
|
||||
if (!(await Filesystem.exists(js))) {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
await Bun.spawn([BunProc.which(), "install", "bash-language-server"], {
|
||||
await Process.spawn([BunProc.which(), "install", "bash-language-server"], {
|
||||
cwd: Global.Path.bin,
|
||||
env: {
|
||||
...process.env,
|
||||
@@ -1827,7 +1826,7 @@ export namespace LSPServer {
|
||||
const js = path.join(Global.Path.bin, "node_modules", "dockerfile-language-server-nodejs", "lib", "server.js")
|
||||
if (!(await Filesystem.exists(js))) {
|
||||
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
|
||||
await Bun.spawn([BunProc.which(), "install", "dockerfile-language-server-nodejs"], {
|
||||
await Process.spawn([BunProc.which(), "install", "dockerfile-language-server-nodejs"], {
|
||||
cwd: Global.Path.bin,
|
||||
env: {
|
||||
...process.env,
|
||||
|
||||
@@ -13,6 +13,7 @@ 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" })
|
||||
@@ -262,16 +263,11 @@ export namespace Project {
|
||||
if (input.vcs !== "git") return
|
||||
if (input.icon?.override) return
|
||||
if (input.icon?.url) return
|
||||
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 matches = await Glob.scan("**/favicon.{ico,png,svg,jpg,jpeg,webp}", {
|
||||
cwd: input.worktree,
|
||||
absolute: true,
|
||||
include: "file",
|
||||
})
|
||||
const shortest = matches.sort((a, b) => a.length - b.length)[0]
|
||||
if (!shortest) return
|
||||
const buffer = await Filesystem.readBytes(shortest)
|
||||
|
||||
@@ -6,6 +6,7 @@ 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" })
|
||||
@@ -98,13 +99,11 @@ export namespace InstructionPrompt {
|
||||
instruction = path.join(os.homedir(), instruction.slice(2))
|
||||
}
|
||||
const matches = path.isAbsolute(instruction)
|
||||
? await Array.fromAsync(
|
||||
new Bun.Glob(path.basename(instruction)).scan({
|
||||
cwd: path.dirname(instruction),
|
||||
absolute: true,
|
||||
onlyFiles: true,
|
||||
}),
|
||||
).catch(() => [])
|
||||
? await Glob.scan(path.basename(instruction), {
|
||||
cwd: path.dirname(instruction),
|
||||
absolute: true,
|
||||
include: "file",
|
||||
}).catch(() => [])
|
||||
: await resolveRelative(instruction)
|
||||
matches.forEach((p) => {
|
||||
paths.add(path.resolve(p))
|
||||
|
||||
@@ -12,6 +12,7 @@ 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" })
|
||||
@@ -44,10 +45,9 @@ 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_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")
|
||||
const EXTERNAL_SKILL_PATTERN = "skills/**/SKILL.md"
|
||||
const OPENCODE_SKILL_PATTERN = "{skill,skills}/**/SKILL.md"
|
||||
const SKILL_PATTERN = "**/SKILL.md"
|
||||
|
||||
export const state = Instance.state(async () => {
|
||||
const skills: Record<string, Info> = {}
|
||||
@@ -88,15 +88,13 @@ export namespace Skill {
|
||||
}
|
||||
|
||||
const scanExternal = async (root: string, scope: "global" | "project") => {
|
||||
return Array.fromAsync(
|
||||
EXTERNAL_SKILL_GLOB.scan({
|
||||
cwd: root,
|
||||
absolute: true,
|
||||
onlyFiles: true,
|
||||
followSymlinks: true,
|
||||
dot: true,
|
||||
}),
|
||||
)
|
||||
return Glob.scan(EXTERNAL_SKILL_PATTERN, {
|
||||
cwd: root,
|
||||
absolute: true,
|
||||
include: "file",
|
||||
dot: true,
|
||||
symlink: true,
|
||||
})
|
||||
.then((matches) => Promise.all(matches.map(addSkill)))
|
||||
.catch((error) => {
|
||||
log.error(`failed to scan ${scope} skills`, { dir: root, error })
|
||||
@@ -123,12 +121,13 @@ export namespace Skill {
|
||||
|
||||
// Scan .opencode/skill/ directories
|
||||
for (const dir of await Config.directories()) {
|
||||
for await (const match of OPENCODE_SKILL_GLOB.scan({
|
||||
const matches = await Glob.scan(OPENCODE_SKILL_PATTERN, {
|
||||
cwd: dir,
|
||||
absolute: true,
|
||||
onlyFiles: true,
|
||||
followSymlinks: true,
|
||||
})) {
|
||||
include: "file",
|
||||
symlink: true,
|
||||
})
|
||||
for (const match of matches) {
|
||||
await addSkill(match)
|
||||
}
|
||||
}
|
||||
@@ -142,12 +141,13 @@ export namespace Skill {
|
||||
log.warn("skill path not found", { path: resolved })
|
||||
continue
|
||||
}
|
||||
for await (const match of SKILL_GLOB.scan({
|
||||
const matches = await Glob.scan(SKILL_PATTERN, {
|
||||
cwd: resolved,
|
||||
absolute: true,
|
||||
onlyFiles: true,
|
||||
followSymlinks: true,
|
||||
})) {
|
||||
include: "file",
|
||||
symlink: true,
|
||||
})
|
||||
for (const match of matches) {
|
||||
await addSkill(match)
|
||||
}
|
||||
}
|
||||
@@ -157,12 +157,13 @@ export namespace Skill {
|
||||
const list = await Discovery.pull(url)
|
||||
for (const dir of list) {
|
||||
dirs.add(dir)
|
||||
for await (const match of SKILL_GLOB.scan({
|
||||
const matches = await Glob.scan(SKILL_PATTERN, {
|
||||
cwd: dir,
|
||||
absolute: true,
|
||||
onlyFiles: true,
|
||||
followSymlinks: true,
|
||||
})) {
|
||||
include: "file",
|
||||
symlink: true,
|
||||
})
|
||||
for (const match of matches) {
|
||||
await addSkill(match)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ 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" })
|
||||
@@ -71,12 +72,7 @@ export namespace JsonMigration {
|
||||
const now = Date.now()
|
||||
|
||||
async function list(pattern: string) {
|
||||
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
|
||||
return Glob.scan(pattern, { cwd: storageDir, absolute: true })
|
||||
}
|
||||
|
||||
async function read(files: string[], start: number, end: number) {
|
||||
|
||||
@@ -8,6 +8,7 @@ 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" })
|
||||
@@ -25,17 +26,20 @@ export namespace Storage {
|
||||
async (dir) => {
|
||||
const project = path.resolve(dir, "../project")
|
||||
if (!(await Filesystem.isDir(project))) return
|
||||
for await (const projectDir of new Bun.Glob("*").scan({
|
||||
const projectDirs = await Glob.scan("*", {
|
||||
cwd: project,
|
||||
onlyFiles: false,
|
||||
})) {
|
||||
include: "all",
|
||||
})
|
||||
for (const projectDir of projectDirs) {
|
||||
const fullPath = path.join(project, projectDir)
|
||||
if (!(await Filesystem.isDir(fullPath))) continue
|
||||
log.info(`migrating project ${projectDir}`)
|
||||
let projectID = projectDir
|
||||
const fullProjectDir = path.join(project, projectDir)
|
||||
let worktree = "/"
|
||||
|
||||
if (projectID !== "global") {
|
||||
for await (const msgFile of new Bun.Glob("storage/session/message/*/*.json").scan({
|
||||
for (const msgFile of await Glob.scan("storage/session/message/*/*.json", {
|
||||
cwd: path.join(project, projectDir),
|
||||
absolute: true,
|
||||
})) {
|
||||
@@ -71,7 +75,7 @@ export namespace Storage {
|
||||
})
|
||||
|
||||
log.info(`migrating sessions for project ${projectID}`)
|
||||
for await (const sessionFile of new Bun.Glob("storage/session/info/*.json").scan({
|
||||
for (const sessionFile of await Glob.scan("storage/session/info/*.json", {
|
||||
cwd: fullProjectDir,
|
||||
absolute: true,
|
||||
})) {
|
||||
@@ -83,7 +87,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 await (const msgFile of new Bun.Glob(`storage/session/message/${session.id}/*.json`).scan({
|
||||
for (const msgFile of await Glob.scan(`storage/session/message/${session.id}/*.json`, {
|
||||
cwd: fullProjectDir,
|
||||
absolute: true,
|
||||
})) {
|
||||
@@ -96,12 +100,10 @@ export namespace Storage {
|
||||
await Filesystem.writeJson(dest, message)
|
||||
|
||||
log.info(`migrating parts for message ${message.id}`)
|
||||
for await (const partFile of new Bun.Glob(`storage/session/part/${session.id}/${message.id}/*.json`).scan(
|
||||
{
|
||||
cwd: fullProjectDir,
|
||||
absolute: true,
|
||||
},
|
||||
)) {
|
||||
for (const partFile of await Glob.scan(`storage/session/part/${session.id}/${message.id}/*.json`, {
|
||||
cwd: fullProjectDir,
|
||||
absolute: true,
|
||||
})) {
|
||||
const dest = path.join(dir, "part", message.id, path.basename(partFile))
|
||||
const part = await Filesystem.readJson(partFile)
|
||||
log.info("copying", {
|
||||
@@ -116,7 +118,7 @@ export namespace Storage {
|
||||
}
|
||||
},
|
||||
async (dir) => {
|
||||
for await (const item of new Bun.Glob("session/*/*.json").scan({
|
||||
for (const item of await Glob.scan("session/*/*.json", {
|
||||
cwd: dir,
|
||||
absolute: true,
|
||||
})) {
|
||||
@@ -202,16 +204,13 @@ 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 Array.fromAsync(
|
||||
glob.scan({
|
||||
cwd: path.join(dir, ...prefix),
|
||||
onlyFiles: true,
|
||||
}),
|
||||
).then((results) => results.map((x) => [...prefix, ...x.slice(0, -5).split(path.sep)]))
|
||||
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)]))
|
||||
result.sort()
|
||||
return result
|
||||
} catch {
|
||||
|
||||
@@ -2,6 +2,7 @@ import z from "zod"
|
||||
import { Tool } from "./tool"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import { Ripgrep } from "../file/ripgrep"
|
||||
import { Process } from "../util/process"
|
||||
|
||||
import DESCRIPTION from "./grep.txt"
|
||||
import { Instance } from "../project/instance"
|
||||
@@ -44,12 +45,16 @@ export const GrepTool = Tool.define("grep", {
|
||||
}
|
||||
args.push(searchPath)
|
||||
|
||||
const proc = Bun.spawn([rgPath, ...args], {
|
||||
const proc = Process.spawn([rgPath, ...args], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
signal: ctx.abort,
|
||||
})
|
||||
|
||||
if (!proc.stdout || !proc.stderr) {
|
||||
throw new Error("Process output not available")
|
||||
}
|
||||
|
||||
const output = await new Response(proc.stdout).text()
|
||||
const errorOutput = await new Response(proc.stderr).text()
|
||||
const exitCode = await proc.exited
|
||||
|
||||
@@ -27,16 +27,18 @@ 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({ cwd: dir, absolute: true, followSymlinks: true, dot: true })]),
|
||||
dirs.flatMap((dir) =>
|
||||
Glob.scanSync("{tool,tools}/*.{js,ts}", { cwd: dir, absolute: true, dot: true, symlink: true }),
|
||||
),
|
||||
)
|
||||
if (matches.length) await Config.waitForDependencies()
|
||||
for (const match of matches) {
|
||||
|
||||
@@ -6,6 +6,7 @@ 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
|
||||
@@ -34,8 +35,7 @@ export namespace Truncate {
|
||||
|
||||
export async function cleanup() {
|
||||
const cutoff = Identifier.timestamp(Identifier.create("tool", false, Date.now() - RETENTION_MS))
|
||||
const glob = new Bun.Glob("tool_*")
|
||||
const entries = await Array.fromAsync(glob.scan({ cwd: DIR, onlyFiles: true })).catch(() => [] as string[])
|
||||
const entries = await Glob.scan("tool_*", { cwd: DIR, include: "file" }).catch(() => [] as string[])
|
||||
for (const entry of entries) {
|
||||
if (Identifier.timestamp(entry) >= cutoff) continue
|
||||
await fs.unlink(path.join(DIR, entry)).catch(() => {})
|
||||
|
||||
@@ -5,6 +5,7 @@ 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
|
||||
@@ -156,16 +157,13 @@ export namespace Filesystem {
|
||||
const result = []
|
||||
while (true) {
|
||||
try {
|
||||
const glob = new Bun.Glob(pattern)
|
||||
for await (const match of glob.scan({
|
||||
const matches = await Glob.scan(pattern, {
|
||||
cwd: current,
|
||||
absolute: true,
|
||||
onlyFiles: true,
|
||||
followSymlinks: true,
|
||||
include: "file",
|
||||
dot: true,
|
||||
})) {
|
||||
result.push(match)
|
||||
}
|
||||
})
|
||||
result.push(...matches)
|
||||
} catch {
|
||||
// Skip invalid glob patterns
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { $ } from "bun"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { Process } from "./process"
|
||||
|
||||
export interface GitResult {
|
||||
exitCode: number
|
||||
@@ -14,12 +15,12 @@ export interface GitResult {
|
||||
* Uses Bun's lightweight `$` shell by default. When the process is running
|
||||
* as an ACP client, child processes inherit the parent's stdin pipe which
|
||||
* carries protocol data – on Windows this causes git to deadlock. In that
|
||||
* case we fall back to `Bun.spawn` with `stdin: "ignore"`.
|
||||
* case we fall back to `Process.spawn` with `stdin: "ignore"`.
|
||||
*/
|
||||
export async function git(args: string[], opts: { cwd: string; env?: Record<string, string> }): Promise<GitResult> {
|
||||
if (Flag.OPENCODE_CLIENT === "acp") {
|
||||
try {
|
||||
const proc = Bun.spawn(["git", ...args], {
|
||||
const proc = Process.spawn(["git", ...args], {
|
||||
stdin: "ignore",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
|
||||
34
packages/opencode/src/util/glob.ts
Normal file
34
packages/opencode/src/util/glob.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
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 !== "all",
|
||||
}
|
||||
}
|
||||
|
||||
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 })
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ 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" })
|
||||
@@ -77,13 +78,11 @@ export namespace Log {
|
||||
}
|
||||
|
||||
async function cleanup(dir: string) {
|
||||
const glob = new Bun.Glob("????-??-??T??????.log")
|
||||
const files = await Array.fromAsync(
|
||||
glob.scan({
|
||||
cwd: dir,
|
||||
absolute: true,
|
||||
}),
|
||||
)
|
||||
const files = await Glob.scan("????-??-??T??????.log", {
|
||||
cwd: dir,
|
||||
absolute: true,
|
||||
include: "file",
|
||||
})
|
||||
if (files.length <= 5) return
|
||||
|
||||
const filesToDelete = files.slice(0, -10)
|
||||
|
||||
73
packages/opencode/src/util/process.ts
Normal file
73
packages/opencode/src/util/process.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { spawn as childSpawn } from "child_process"
|
||||
import { Readable } from "stream"
|
||||
|
||||
export namespace Process {
|
||||
export type Stdio = "inherit" | "pipe" | "ignore"
|
||||
|
||||
export interface Options {
|
||||
cwd?: string
|
||||
env?: NodeJS.ProcessEnv
|
||||
stdin?: Stdio
|
||||
stdout?: Stdio
|
||||
stderr?: Stdio
|
||||
signal?: AbortSignal
|
||||
}
|
||||
|
||||
export interface Child {
|
||||
stdin: NodeJS.WritableStream | null
|
||||
stdout: ReadableStream<Uint8Array> | null
|
||||
stderr: ReadableStream<Uint8Array> | null
|
||||
exited: Promise<number>
|
||||
kill(signal?: NodeJS.Signals | number): boolean
|
||||
readonly exitCode: number | null
|
||||
readonly pid: number | undefined
|
||||
}
|
||||
|
||||
export function spawn(cmd: string[], options: Options = {}): Child {
|
||||
if (cmd.length === 0) throw new Error("Command is required")
|
||||
options.signal?.throwIfAborted()
|
||||
|
||||
const proc = childSpawn(cmd[0], cmd.slice(1), {
|
||||
cwd: options.cwd,
|
||||
env: options.env,
|
||||
stdio: [options.stdin ?? "ignore", options.stdout ?? "inherit", options.stderr ?? "inherit"],
|
||||
})
|
||||
|
||||
const abort = () => {
|
||||
if (proc.killed) return
|
||||
proc.kill()
|
||||
}
|
||||
|
||||
let code: number | null = null
|
||||
const exited = new Promise<number>((resolve, reject) => {
|
||||
const done = () => options.signal?.removeEventListener("abort", abort)
|
||||
proc.once("exit", (exitCode, signal) => {
|
||||
done()
|
||||
code = exitCode ?? (signal ? 1 : 0)
|
||||
resolve(code)
|
||||
})
|
||||
proc.once("error", (error) => {
|
||||
done()
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
|
||||
if (options.signal) {
|
||||
options.signal.addEventListener("abort", abort, { once: true })
|
||||
}
|
||||
|
||||
return {
|
||||
stdin: proc.stdin,
|
||||
stdout: proc.stdout ? (Readable.toWeb(proc.stdout) as unknown as ReadableStream<Uint8Array>) : null,
|
||||
stderr: proc.stderr ? (Readable.toWeb(proc.stderr) as unknown as ReadableStream<Uint8Array>) : null,
|
||||
exited,
|
||||
kill: (signal) => proc.kill(signal),
|
||||
get exitCode() {
|
||||
return code ?? proc.exitCode
|
||||
},
|
||||
get pid() {
|
||||
return proc.pid
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
164
packages/opencode/test/util/glob.test.ts
Normal file
164
packages/opencode/test/util/glob.test.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
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("scan()", () => {
|
||||
test("finds files matching pattern", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await fs.writeFile(path.join(tmp.path, "a.txt"), "", "utf-8")
|
||||
await fs.writeFile(path.join(tmp.path, "b.txt"), "", "utf-8")
|
||||
await fs.writeFile(path.join(tmp.path, "c.md"), "", "utf-8")
|
||||
|
||||
const results = await Glob.scan("*.txt", { cwd: tmp.path })
|
||||
|
||||
expect(results.sort()).toEqual(["a.txt", "b.txt"])
|
||||
})
|
||||
|
||||
test("returns absolute paths when absolute option is true", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await fs.writeFile(path.join(tmp.path, "file.txt"), "", "utf-8")
|
||||
|
||||
const results = await Glob.scan("*.txt", { cwd: tmp.path, absolute: true })
|
||||
|
||||
expect(results[0]).toBe(path.join(tmp.path, "file.txt"))
|
||||
})
|
||||
|
||||
test("excludes directories by default", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await fs.mkdir(path.join(tmp.path, "subdir"))
|
||||
await fs.writeFile(path.join(tmp.path, "file.txt"), "", "utf-8")
|
||||
|
||||
const results = await Glob.scan("*", { cwd: tmp.path })
|
||||
|
||||
expect(results).toEqual(["file.txt"])
|
||||
})
|
||||
|
||||
test("excludes directories 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"), "", "utf-8")
|
||||
|
||||
const results = await Glob.scan("*", { cwd: tmp.path, include: "file" })
|
||||
|
||||
expect(results).toEqual(["file.txt"])
|
||||
})
|
||||
|
||||
test("includes 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"), "", "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"), "", "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([])
|
||||
})
|
||||
|
||||
test("does not follow symlinks by default", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await fs.mkdir(path.join(tmp.path, "realdir"))
|
||||
await fs.writeFile(path.join(tmp.path, "realdir", "file.txt"), "", "utf-8")
|
||||
await fs.symlink(path.join(tmp.path, "realdir"), path.join(tmp.path, "linkdir"))
|
||||
|
||||
const results = await Glob.scan("**/*.txt", { cwd: tmp.path })
|
||||
|
||||
expect(results).toEqual(["realdir/file.txt"])
|
||||
})
|
||||
|
||||
test("follows symlinks when symlink option is true", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await fs.mkdir(path.join(tmp.path, "realdir"))
|
||||
await fs.writeFile(path.join(tmp.path, "realdir", "file.txt"), "", "utf-8")
|
||||
await fs.symlink(path.join(tmp.path, "realdir"), path.join(tmp.path, "linkdir"))
|
||||
|
||||
const results = await Glob.scan("**/*.txt", { cwd: tmp.path, symlink: true })
|
||||
|
||||
expect(results.sort()).toEqual(["linkdir/file.txt", "realdir/file.txt"])
|
||||
})
|
||||
|
||||
test("includes dotfiles when dot option is true", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await fs.writeFile(path.join(tmp.path, ".hidden"), "", "utf-8")
|
||||
await fs.writeFile(path.join(tmp.path, "visible"), "", "utf-8")
|
||||
|
||||
const results = await Glob.scan("*", { cwd: tmp.path, dot: true })
|
||||
|
||||
expect(results.sort()).toEqual([".hidden", "visible"])
|
||||
})
|
||||
|
||||
test("excludes dotfiles when dot option is false", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await fs.writeFile(path.join(tmp.path, ".hidden"), "", "utf-8")
|
||||
await fs.writeFile(path.join(tmp.path, "visible"), "", "utf-8")
|
||||
|
||||
const results = await Glob.scan("*", { cwd: tmp.path, dot: false })
|
||||
|
||||
expect(results).toEqual(["visible"])
|
||||
})
|
||||
})
|
||||
|
||||
describe("scanSync()", () => {
|
||||
test("finds files matching pattern synchronously", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await fs.writeFile(path.join(tmp.path, "a.txt"), "", "utf-8")
|
||||
await fs.writeFile(path.join(tmp.path, "b.txt"), "", "utf-8")
|
||||
|
||||
const results = Glob.scanSync("*.txt", { cwd: tmp.path })
|
||||
|
||||
expect(results.sort()).toEqual(["a.txt", "b.txt"])
|
||||
})
|
||||
|
||||
test("respects options", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await fs.mkdir(path.join(tmp.path, "subdir"))
|
||||
await fs.writeFile(path.join(tmp.path, "file.txt"), "", "utf-8")
|
||||
|
||||
const results = Glob.scanSync("*", { cwd: tmp.path, include: "all" })
|
||||
|
||||
expect(results.sort()).toEqual(["file.txt", "subdir"])
|
||||
})
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,7 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { Process } from "../packages/opencode/src/util/process"
|
||||
|
||||
async function sendToPostHog(event: string, properties: Record<string, any>) {
|
||||
const key = process.env["POSTHOG_KEY"]
|
||||
|
||||
@@ -59,7 +61,7 @@ async function fetchNpmDownloads(packageName: string): Promise<number> {
|
||||
console.warn(`Failed to fetch npm downloads for ${packageName}: ${response.status}`)
|
||||
return 0
|
||||
}
|
||||
const data: NpmDownloadsRange = await response.json()
|
||||
const data = (await response.json()) as NpmDownloadsRange
|
||||
return data.downloads.reduce((total, day) => total + day.downloads, 0)
|
||||
} catch (error) {
|
||||
console.warn(`Error fetching npm downloads for ${packageName}:`, error)
|
||||
@@ -80,7 +82,7 @@ async function fetchReleases(): Promise<Release[]> {
|
||||
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const batch: Release[] = await response.json()
|
||||
const batch = (await response.json()) as Release[]
|
||||
if (batch.length === 0) break
|
||||
|
||||
releases.push(...batch)
|
||||
@@ -137,12 +139,14 @@ async function save(githubTotal: number, npmDownloads: number) {
|
||||
const lines = content.trim().split("\n")
|
||||
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
const line = lines[i].trim()
|
||||
const item = lines[i]
|
||||
if (!item) continue
|
||||
const line = item.trim()
|
||||
if (line.startsWith("|") && !line.includes("Date") && !line.includes("---")) {
|
||||
const match = line.match(
|
||||
/\|\s*[\d-]+\s*\|\s*([\d,]+)\s*(?:\([^)]*\))?\s*\|\s*([\d,]+)\s*(?:\([^)]*\))?\s*\|\s*([\d,]+)\s*(?:\([^)]*\))?\s*\|/,
|
||||
)
|
||||
if (match) {
|
||||
if (match?.[1] && match[2] && match[3]) {
|
||||
previousGithub = parseInt(match[1].replace(/,/g, ""))
|
||||
previousNpm = parseInt(match[2].replace(/,/g, ""))
|
||||
previousTotal = parseInt(match[3].replace(/,/g, ""))
|
||||
@@ -181,7 +185,7 @@ async function save(githubTotal: number, npmDownloads: number) {
|
||||
}
|
||||
|
||||
await Bun.write(file, content + line)
|
||||
await Bun.spawn(["bunx", "prettier", "--write", file]).exited
|
||||
await Process.spawn(["bunx", "prettier", "--write", file]).exited
|
||||
|
||||
console.log(
|
||||
`\nAppended stats to ${file}: GitHub ${githubTotal.toLocaleString()}${githubChangeStr}, npm ${npmDownloads.toLocaleString()}${npmChangeStr}, Total ${total.toLocaleString()}${totalChangeStr}`,
|
||||
|
||||
Reference in New Issue
Block a user