fzf.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. import path from "path"
  2. import { Global } from "../global"
  3. import fs from "fs/promises"
  4. import z from "zod"
  5. import { NamedError } from "@opencode-ai/util/error"
  6. import { lazy } from "../util/lazy"
  7. import { Log } from "../util/log"
  8. import { ZipReader, BlobReader, BlobWriter } from "@zip.js/zip.js"
  9. export namespace Fzf {
  10. const log = Log.create({ service: "fzf" })
  11. const VERSION = "0.62.0"
  12. const PLATFORM = {
  13. darwin: { extension: "tar.gz" },
  14. linux: { extension: "tar.gz" },
  15. win32: { extension: "zip" },
  16. } as const
  17. export const ExtractionFailedError = NamedError.create(
  18. "FzfExtractionFailedError",
  19. z.object({
  20. filepath: z.string(),
  21. stderr: z.string(),
  22. }),
  23. )
  24. export const UnsupportedPlatformError = NamedError.create(
  25. "FzfUnsupportedPlatformError",
  26. z.object({
  27. platform: z.string(),
  28. }),
  29. )
  30. export const DownloadFailedError = NamedError.create(
  31. "FzfDownloadFailedError",
  32. z.object({
  33. url: z.string(),
  34. status: z.number(),
  35. }),
  36. )
  37. const state = lazy(async () => {
  38. let filepath = Bun.which("fzf")
  39. if (filepath) {
  40. log.info("found", { filepath })
  41. return { filepath }
  42. }
  43. filepath = path.join(Global.Path.bin, "fzf" + (process.platform === "win32" ? ".exe" : ""))
  44. const file = Bun.file(filepath)
  45. if (!(await file.exists())) {
  46. const archMap = { x64: "amd64", arm64: "arm64" } as const
  47. const arch = archMap[process.arch as keyof typeof archMap] ?? "amd64"
  48. const config = PLATFORM[process.platform as keyof typeof PLATFORM]
  49. if (!config) throw new UnsupportedPlatformError({ platform: process.platform })
  50. const version = VERSION
  51. const platformName = process.platform === "win32" ? "windows" : process.platform
  52. const filename = `fzf-${version}-${platformName}_${arch}.${config.extension}`
  53. const url = `https://github.com/junegunn/fzf/releases/download/v${version}/${filename}`
  54. const response = await fetch(url)
  55. if (!response.ok) throw new DownloadFailedError({ url, status: response.status })
  56. const buffer = await response.arrayBuffer()
  57. const archivePath = path.join(Global.Path.bin, filename)
  58. await Bun.write(archivePath, buffer)
  59. if (config.extension === "tar.gz") {
  60. const proc = Bun.spawn(["tar", "-xzf", archivePath, "fzf"], {
  61. cwd: Global.Path.bin,
  62. stderr: "pipe",
  63. stdout: "pipe",
  64. })
  65. await proc.exited
  66. if (proc.exitCode !== 0)
  67. throw new ExtractionFailedError({
  68. filepath,
  69. stderr: await Bun.readableStreamToText(proc.stderr),
  70. })
  71. }
  72. if (config.extension === "zip") {
  73. const zipFileReader = new ZipReader(new BlobReader(new Blob([await Bun.file(archivePath).arrayBuffer()])))
  74. const entries = await zipFileReader.getEntries()
  75. let fzfEntry: any
  76. for (const entry of entries) {
  77. if (entry.filename === "fzf.exe") {
  78. fzfEntry = entry
  79. break
  80. }
  81. }
  82. if (!fzfEntry) {
  83. throw new ExtractionFailedError({
  84. filepath: archivePath,
  85. stderr: "fzf.exe not found in zip archive",
  86. })
  87. }
  88. const fzfBlob = await fzfEntry.getData(new BlobWriter())
  89. if (!fzfBlob) {
  90. throw new ExtractionFailedError({
  91. filepath: archivePath,
  92. stderr: "Failed to extract fzf.exe from zip archive",
  93. })
  94. }
  95. await Bun.write(filepath, await fzfBlob.arrayBuffer())
  96. await zipFileReader.close()
  97. }
  98. await fs.unlink(archivePath)
  99. if (process.platform !== "win32") await fs.chmod(filepath, 0o755)
  100. }
  101. return {
  102. filepath,
  103. }
  104. })
  105. export async function filepath() {
  106. const { filepath } = await state()
  107. return filepath
  108. }
  109. }