feat: Replace unzip with @zip.js/zip.js for Windows compatibility (#662)
This commit is contained in:
4
bun.lock
4
bun.lock
@@ -33,6 +33,8 @@
|
|||||||
"@hono/zod-validator": "0.4.2",
|
"@hono/zod-validator": "0.4.2",
|
||||||
"@modelcontextprotocol/sdk": "1.15.1",
|
"@modelcontextprotocol/sdk": "1.15.1",
|
||||||
"@openauthjs/openauth": "0.4.3",
|
"@openauthjs/openauth": "0.4.3",
|
||||||
|
"@standard-schema/spec": "1.0.0",
|
||||||
|
"@zip.js/zip.js": "2.7.62",
|
||||||
"ai": "catalog:",
|
"ai": "catalog:",
|
||||||
"decimal.js": "10.5.0",
|
"decimal.js": "10.5.0",
|
||||||
"diff": "8.0.2",
|
"diff": "8.0.2",
|
||||||
@@ -749,6 +751,8 @@
|
|||||||
|
|
||||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||||
|
|
||||||
|
"@zip.js/zip.js": ["@zip.js/zip.js@2.7.62", "", {}, "sha512-OaLvZ8j4gCkLn048ypkZu29KX30r8/OfFF2w4Jo5WXFr+J04J+lzJ5TKZBVgFXhlvSkqNFQdfnY1Q8TMTCyBVA=="],
|
||||||
|
|
||||||
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||||
|
|
||||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||||
|
|||||||
@@ -31,6 +31,8 @@
|
|||||||
"@hono/zod-validator": "0.4.2",
|
"@hono/zod-validator": "0.4.2",
|
||||||
"@modelcontextprotocol/sdk": "1.15.1",
|
"@modelcontextprotocol/sdk": "1.15.1",
|
||||||
"@openauthjs/openauth": "0.4.3",
|
"@openauthjs/openauth": "0.4.3",
|
||||||
|
"@standard-schema/spec": "1.0.0",
|
||||||
|
"@zip.js/zip.js": "2.7.62",
|
||||||
"ai": "catalog:",
|
"ai": "catalog:",
|
||||||
"decimal.js": "10.5.0",
|
"decimal.js": "10.5.0",
|
||||||
"diff": "8.0.2",
|
"diff": "8.0.2",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { z } from "zod"
|
|||||||
import { NamedError } from "../util/error"
|
import { NamedError } from "../util/error"
|
||||||
import { lazy } from "../util/lazy"
|
import { lazy } from "../util/lazy"
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
|
import { ZipReader, BlobReader, BlobWriter } from "@zip.js/zip.js"
|
||||||
|
|
||||||
export namespace Fzf {
|
export namespace Fzf {
|
||||||
const log = Log.create({ service: "fzf" })
|
const log = Log.create({ service: "fzf" })
|
||||||
@@ -45,7 +46,10 @@ export namespace Fzf {
|
|||||||
log.info("found", { filepath })
|
log.info("found", { filepath })
|
||||||
return { 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)
|
const file = Bun.file(filepath)
|
||||||
if (!(await file.exists())) {
|
if (!(await file.exists())) {
|
||||||
@@ -53,15 +57,18 @@ export namespace Fzf {
|
|||||||
const arch = archMap[process.arch as keyof typeof archMap] ?? "amd64"
|
const arch = archMap[process.arch as keyof typeof archMap] ?? "amd64"
|
||||||
|
|
||||||
const config = PLATFORM[process.platform as keyof typeof PLATFORM]
|
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 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 filename = `fzf-${version}-${platformName}_${arch}.${config.extension}`
|
||||||
const url = `https://github.com/junegunn/fzf/releases/download/v${version}/${filename}`
|
const url = `https://github.com/junegunn/fzf/releases/download/v${version}/${filename}`
|
||||||
|
|
||||||
const response = await fetch(url)
|
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 buffer = await response.arrayBuffer()
|
||||||
const archivePath = path.join(Global.Path.bin, filename)
|
const archivePath = path.join(Global.Path.bin, filename)
|
||||||
@@ -80,17 +87,32 @@ export namespace Fzf {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (config.extension === "zip") {
|
if (config.extension === "zip") {
|
||||||
const proc = Bun.spawn(["unzip", "-j", archivePath, "fzf.exe", "-d", Global.Path.bin], {
|
const zipFileReader = new ZipReader(new BlobReader(new Blob([await Bun.file(archivePath).arrayBuffer()])));
|
||||||
cwd: Global.Path.bin,
|
const entries = await zipFileReader.getEntries();
|
||||||
stderr: "pipe",
|
let fzfEntry: any;
|
||||||
stdout: "ignore",
|
for (const entry of entries) {
|
||||||
})
|
if (entry.filename === "fzf.exe") {
|
||||||
await proc.exited
|
fzfEntry = entry;
|
||||||
if (proc.exitCode !== 0)
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fzfEntry) {
|
||||||
throw new ExtractionFailedError({
|
throw new ExtractionFailedError({
|
||||||
filepath: archivePath,
|
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)
|
await fs.unlink(archivePath)
|
||||||
if (process.platform !== "win32") await fs.chmod(filepath, 0o755)
|
if (process.platform !== "win32") await fs.chmod(filepath, 0o755)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { NamedError } from "../util/error"
|
|||||||
import { lazy } from "../util/lazy"
|
import { lazy } from "../util/lazy"
|
||||||
import { $ } from "bun"
|
import { $ } from "bun"
|
||||||
import { Fzf } from "./fzf"
|
import { Fzf } from "./fzf"
|
||||||
|
import { ZipReader, BlobReader, BlobWriter } from "@zip.js/zip.js"
|
||||||
|
|
||||||
export namespace Ripgrep {
|
export namespace Ripgrep {
|
||||||
const Stats = z.object({
|
const Stats = z.object({
|
||||||
@@ -34,27 +35,25 @@ export namespace Ripgrep {
|
|||||||
|
|
||||||
export const Match = z.object({
|
export const Match = z.object({
|
||||||
type: z.literal("match"),
|
type: z.literal("match"),
|
||||||
data: z
|
data: z.object({
|
||||||
.object({
|
path: z.object({
|
||||||
path: z.object({
|
text: z.string(),
|
||||||
text: z.string(),
|
}),
|
||||||
}),
|
lines: z.object({
|
||||||
lines: z.object({
|
text: z.string(),
|
||||||
text: z.string(),
|
}),
|
||||||
}),
|
line_number: z.number(),
|
||||||
line_number: z.number(),
|
absolute_offset: z.number(),
|
||||||
absolute_offset: z.number(),
|
submatches: z.array(
|
||||||
submatches: z.array(
|
z.object({
|
||||||
z.object({
|
match: z.object({
|
||||||
match: z.object({
|
text: z.string(),
|
||||||
text: z.string(),
|
|
||||||
}),
|
|
||||||
start: z.number(),
|
|
||||||
end: z.number(),
|
|
||||||
}),
|
}),
|
||||||
),
|
start: z.number(),
|
||||||
})
|
end: z.number(),
|
||||||
.openapi({ ref: "Match" }),
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
const End = z.object({
|
const End = z.object({
|
||||||
@@ -124,11 +123,15 @@ export namespace Ripgrep {
|
|||||||
const state = lazy(async () => {
|
const state = lazy(async () => {
|
||||||
let filepath = Bun.which("rg")
|
let filepath = Bun.which("rg")
|
||||||
if (filepath) return { filepath }
|
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)
|
const file = Bun.file(filepath)
|
||||||
if (!(await file.exists())) {
|
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]
|
const config = PLATFORM[platformKey]
|
||||||
if (!config) throw new UnsupportedPlatformError({ 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 url = `https://github.com/BurntSushi/ripgrep/releases/download/${version}/${filename}`
|
||||||
|
|
||||||
const response = await fetch(url)
|
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 buffer = await response.arrayBuffer()
|
||||||
const archivePath = path.join(Global.Path.bin, filename)
|
const archivePath = path.join(Global.Path.bin, filename)
|
||||||
@@ -161,17 +165,34 @@ export namespace Ripgrep {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (config.extension === "zip") {
|
if (config.extension === "zip") {
|
||||||
const proc = Bun.spawn(["unzip", "-j", archivePath, "*/rg.exe", "-d", Global.Path.bin], {
|
if (config.extension === "zip") {
|
||||||
cwd: Global.Path.bin,
|
const zipFileReader = new ZipReader(new BlobReader(new Blob([await Bun.file(archivePath).arrayBuffer()])));
|
||||||
stderr: "pipe",
|
const entries = await zipFileReader.getEntries();
|
||||||
stdout: "ignore",
|
let rgEntry: any;
|
||||||
})
|
for (const entry of entries) {
|
||||||
await proc.exited
|
if (entry.filename.endsWith("rg.exe")) {
|
||||||
if (proc.exitCode !== 0)
|
rgEntry = entry;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rgEntry) {
|
||||||
throw new ExtractionFailedError({
|
throw new ExtractionFailedError({
|
||||||
filepath: archivePath,
|
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)
|
await fs.unlink(archivePath)
|
||||||
if (!platformKey.endsWith("-win32")) await fs.chmod(filepath, 0o755)
|
if (!platformKey.endsWith("-win32")) await fs.chmod(filepath, 0o755)
|
||||||
@@ -187,16 +208,17 @@ export namespace Ripgrep {
|
|||||||
return filepath
|
return filepath
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function files(input: { cwd: string; query?: string; glob?: string[]; limit?: number }) {
|
export async function files(input: {
|
||||||
const commands = [`${$.escape(await filepath())} --files --follow --hidden --glob='!.git/*'`]
|
cwd: string
|
||||||
|
query?: string
|
||||||
if (input.glob) {
|
glob?: string
|
||||||
for (const g of input.glob) {
|
limit?: number
|
||||||
commands[0] += ` --glob='${g}'`
|
}) {
|
||||||
}
|
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.query)
|
||||||
|
commands.push(`${await Fzf.filepath()} --filter=${input.query}`)
|
||||||
if (input.limit) commands.push(`head -n ${input.limit}`)
|
if (input.limit) commands.push(`head -n ${input.limit}`)
|
||||||
const joined = commands.join(" | ")
|
const joined = commands.join(" | ")
|
||||||
const result = await $`${{ raw: joined }}`.cwd(input.cwd).nothrow().text()
|
const result = await $`${{ raw: joined }}`.cwd(input.cwd).nothrow().text()
|
||||||
@@ -303,8 +325,18 @@ export namespace Ripgrep {
|
|||||||
return lines.join("\n")
|
return lines.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function search(input: { cwd: string; pattern: string; glob?: string[]; limit?: number }) {
|
export async function search(input: {
|
||||||
const args = [`${await filepath()}`, "--json", "--hidden", "--glob='!.git/*'"]
|
cwd: string
|
||||||
|
pattern: string
|
||||||
|
glob?: string[]
|
||||||
|
limit?: number
|
||||||
|
}) {
|
||||||
|
const args = [
|
||||||
|
`${await filepath()}`,
|
||||||
|
"--json",
|
||||||
|
"--hidden",
|
||||||
|
"--glob='!.git/*'",
|
||||||
|
]
|
||||||
|
|
||||||
if (input.glob) {
|
if (input.glob) {
|
||||||
for (const g of input.glob) {
|
for (const g of input.glob) {
|
||||||
|
|||||||
Reference in New Issue
Block a user