summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorClay Warren <[email protected]>2025-07-24 15:49:04 -0500
committerGitHub <[email protected]>2025-07-24 16:49:04 -0400
commitd50ae8e4d4d6fd762c620e4d6d1a4edc6abd0585 (patch)
tree83795bebc824dabe69f1921d76e510d47c6a4cec /packages
parente9074e60cffa373df44916626c13f616c6ca0db1 (diff)
downloadopencode-d50ae8e4d4d6fd762c620e4d6d1a4edc6abd0585.tar.gz
opencode-d50ae8e4d4d6fd762c620e4d6d1a4edc6abd0585.zip
feat: Replace unzip with @zip.js/zip.js for Windows compatibility (#662)
Diffstat (limited to 'packages')
-rw-r--r--packages/opencode/package.json2
-rw-r--r--packages/opencode/src/file/fzf.ts50
-rw-r--r--packages/opencode/src/file/ripgrep.ts122
3 files changed, 115 insertions, 59 deletions
diff --git a/packages/opencode/package.json b/packages/opencode/package.json
index 8541e0184..f278c636c 100644
--- a/packages/opencode/package.json
+++ b/packages/opencode/package.json
@@ -31,6 +31,8 @@
"@hono/zod-validator": "0.4.2",
"@modelcontextprotocol/sdk": "1.15.1",
"@openauthjs/openauth": "0.4.3",
+ "@standard-schema/spec": "1.0.0",
+ "@zip.js/zip.js": "2.7.62",
"ai": "catalog:",
"decimal.js": "10.5.0",
"diff": "8.0.2",
diff --git a/packages/opencode/src/file/fzf.ts b/packages/opencode/src/file/fzf.ts
index 1376af8cf..7a481b0fe 100644
--- a/packages/opencode/src/file/fzf.ts
+++ b/packages/opencode/src/file/fzf.ts
@@ -5,6 +5,7 @@ import { z } from "zod"
import { NamedError } from "../util/error"
import { lazy } from "../util/lazy"
import { Log } from "../util/log"
+import { ZipReader, BlobReader, BlobWriter } from "@zip.js/zip.js"
export namespace Fzf {
const log = Log.create({ service: "fzf" })
@@ -45,7 +46,10 @@ export namespace Fzf {
log.info("found", { filepath })
return { filepath }
}
- filepath = path.join(Global.Path.bin, "fzf" + (process.platform === "win32" ? ".exe" : ""))
+ filepath = path.join(
+ Global.Path.bin,
+ "fzf" + (process.platform === "win32" ? ".exe" : ""),
+ )
const file = Bun.file(filepath)
if (!(await file.exists())) {
@@ -53,15 +57,18 @@ export namespace Fzf {
const arch = archMap[process.arch as keyof typeof archMap] ?? "amd64"
const config = PLATFORM[process.platform as keyof typeof PLATFORM]
- if (!config) throw new UnsupportedPlatformError({ platform: process.platform })
+ if (!config)
+ throw new UnsupportedPlatformError({ platform: process.platform })
const version = VERSION
- const platformName = process.platform === "win32" ? "windows" : process.platform
+ const platformName =
+ process.platform === "win32" ? "windows" : process.platform
const filename = `fzf-${version}-${platformName}_${arch}.${config.extension}`
const url = `https://github.com/junegunn/fzf/releases/download/v${version}/${filename}`
const response = await fetch(url)
- if (!response.ok) throw new DownloadFailedError({ url, status: response.status })
+ if (!response.ok)
+ throw new DownloadFailedError({ url, status: response.status })
const buffer = await response.arrayBuffer()
const archivePath = path.join(Global.Path.bin, filename)
@@ -80,17 +87,32 @@ export namespace Fzf {
})
}
if (config.extension === "zip") {
- const proc = Bun.spawn(["unzip", "-j", archivePath, "fzf.exe", "-d", Global.Path.bin], {
- cwd: Global.Path.bin,
- stderr: "pipe",
- stdout: "ignore",
- })
- await proc.exited
- if (proc.exitCode !== 0)
+ const zipFileReader = new ZipReader(new BlobReader(new Blob([await Bun.file(archivePath).arrayBuffer()])));
+ const entries = await zipFileReader.getEntries();
+ let fzfEntry: any;
+ for (const entry of entries) {
+ if (entry.filename === "fzf.exe") {
+ fzfEntry = entry;
+ break;
+ }
+ }
+
+ if (!fzfEntry) {
throw new ExtractionFailedError({
filepath: archivePath,
- stderr: await Bun.readableStreamToText(proc.stderr),
- })
+ stderr: "fzf.exe not found in zip archive",
+ });
+ }
+
+ const fzfBlob = await fzfEntry.getData(new BlobWriter());
+ if (!fzfBlob) {
+ throw new ExtractionFailedError({
+ filepath: archivePath,
+ stderr: "Failed to extract fzf.exe from zip archive",
+ });
+ }
+ await Bun.write(filepath, await fzfBlob.arrayBuffer());
+ await zipFileReader.close();
}
await fs.unlink(archivePath)
if (process.platform !== "win32") await fs.chmod(filepath, 0o755)
@@ -105,4 +127,4 @@ export namespace Fzf {
const { filepath } = await state()
return filepath
}
-}
+} \ No newline at end of file
diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts
index 05ebbe7d4..a802b887d 100644
--- a/packages/opencode/src/file/ripgrep.ts
+++ b/packages/opencode/src/file/ripgrep.ts
@@ -7,6 +7,7 @@ import { NamedError } from "../util/error"
import { lazy } from "../util/lazy"
import { $ } from "bun"
import { Fzf } from "./fzf"
+import { ZipReader, BlobReader, BlobWriter } from "@zip.js/zip.js"
export namespace Ripgrep {
const Stats = z.object({
@@ -34,27 +35,25 @@ export namespace Ripgrep {
export const Match = z.object({
type: z.literal("match"),
- data: z
- .object({
- path: z.object({
- text: z.string(),
- }),
- lines: z.object({
- text: z.string(),
- }),
- line_number: z.number(),
- absolute_offset: z.number(),
- submatches: z.array(
- z.object({
- match: z.object({
- text: z.string(),
- }),
- start: z.number(),
- end: z.number(),
+ data: z.object({
+ path: z.object({
+ text: z.string(),
+ }),
+ lines: z.object({
+ text: z.string(),
+ }),
+ line_number: z.number(),
+ absolute_offset: z.number(),
+ submatches: z.array(
+ z.object({
+ match: z.object({
+ text: z.string(),
}),
- ),
- })
- .openapi({ ref: "Match" }),
+ start: z.number(),
+ end: z.number(),
+ }),
+ ),
+ }),
})
const End = z.object({
@@ -124,11 +123,15 @@ export namespace Ripgrep {
const state = lazy(async () => {
let filepath = Bun.which("rg")
if (filepath) return { filepath }
- filepath = path.join(Global.Path.bin, "rg" + (process.platform === "win32" ? ".exe" : ""))
+ filepath = path.join(
+ Global.Path.bin,
+ "rg" + (process.platform === "win32" ? ".exe" : ""),
+ )
const file = Bun.file(filepath)
if (!(await file.exists())) {
- const platformKey = `${process.arch}-${process.platform}` as keyof typeof PLATFORM
+ const platformKey =
+ `${process.arch}-${process.platform}` as keyof typeof PLATFORM
const config = PLATFORM[platformKey]
if (!config) throw new UnsupportedPlatformError({ platform: platformKey })
@@ -137,7 +140,8 @@ export namespace Ripgrep {
const url = `https://github.com/BurntSushi/ripgrep/releases/download/${version}/${filename}`
const response = await fetch(url)
- if (!response.ok) throw new DownloadFailedError({ url, status: response.status })
+ if (!response.ok)
+ throw new DownloadFailedError({ url, status: response.status })
const buffer = await response.arrayBuffer()
const archivePath = path.join(Global.Path.bin, filename)
@@ -161,17 +165,34 @@ export namespace Ripgrep {
})
}
if (config.extension === "zip") {
- const proc = Bun.spawn(["unzip", "-j", archivePath, "*/rg.exe", "-d", Global.Path.bin], {
- cwd: Global.Path.bin,
- stderr: "pipe",
- stdout: "ignore",
- })
- await proc.exited
- if (proc.exitCode !== 0)
+ if (config.extension === "zip") {
+ const zipFileReader = new ZipReader(new BlobReader(new Blob([await Bun.file(archivePath).arrayBuffer()])));
+ const entries = await zipFileReader.getEntries();
+ let rgEntry: any;
+ for (const entry of entries) {
+ if (entry.filename.endsWith("rg.exe")) {
+ rgEntry = entry;
+ break;
+ }
+ }
+
+ if (!rgEntry) {
throw new ExtractionFailedError({
filepath: archivePath,
- stderr: await Bun.readableStreamToText(proc.stderr),
- })
+ stderr: "rg.exe not found in zip archive",
+ });
+ }
+
+ const rgBlob = await rgEntry.getData(new BlobWriter());
+ if (!rgBlob) {
+ throw new ExtractionFailedError({
+ filepath: archivePath,
+ stderr: "Failed to extract rg.exe from zip archive",
+ });
+ }
+ await Bun.write(filepath, await rgBlob.arrayBuffer());
+ await zipFileReader.close();
+ }
}
await fs.unlink(archivePath)
if (!platformKey.endsWith("-win32")) await fs.chmod(filepath, 0o755)
@@ -187,16 +208,17 @@ export namespace Ripgrep {
return filepath
}
- export async function files(input: { cwd: string; query?: string; glob?: string[]; limit?: number }) {
- const commands = [`${$.escape(await filepath())} --files --follow --hidden --glob='!.git/*'`]
-
- if (input.glob) {
- for (const g of input.glob) {
- commands[0] += ` --glob='${g}'`
- }
- }
-
- if (input.query) commands.push(`${await Fzf.filepath()} --filter=${input.query}`)
+ export async function files(input: {
+ cwd: string
+ query?: string
+ glob?: string
+ limit?: number
+ }) {
+ const commands = [
+ `${await filepath()} --files --hidden --glob='!.git/*' ${input.glob ? `--glob='${input.glob}'` : ``}`,
+ ]
+ if (input.query)
+ commands.push(`${await Fzf.filepath()} --filter=${input.query}`)
if (input.limit) commands.push(`head -n ${input.limit}`)
const joined = commands.join(" | ")
const result = await $`${{ raw: joined }}`.cwd(input.cwd).nothrow().text()
@@ -303,8 +325,18 @@ export namespace Ripgrep {
return lines.join("\n")
}
- export async function search(input: { cwd: string; pattern: string; glob?: string[]; limit?: number }) {
- const args = [`${await filepath()}`, "--json", "--hidden", "--glob='!.git/*'"]
+ export async function search(input: {
+ cwd: string
+ pattern: string
+ glob?: string[]
+ limit?: number
+ }) {
+ const args = [
+ `${await filepath()}`,
+ "--json",
+ "--hidden",
+ "--glob='!.git/*'",
+ ]
if (input.glob) {
for (const g of input.glob) {
@@ -333,4 +365,4 @@ export namespace Ripgrep {
.filter((r) => r.type === "match")
.map((r) => r.data)
}
-}
+} \ No newline at end of file