fzf.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  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 "../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(
  44. Global.Path.bin,
  45. "fzf" + (process.platform === "win32" ? ".exe" : ""),
  46. )
  47. const file = Bun.file(filepath)
  48. if (!(await file.exists())) {
  49. const archMap = { x64: "amd64", arm64: "arm64" } as const
  50. const arch = archMap[process.arch as keyof typeof archMap] ?? "amd64"
  51. const config = PLATFORM[process.platform as keyof typeof PLATFORM]
  52. if (!config)
  53. throw new UnsupportedPlatformError({ platform: process.platform })
  54. const version = VERSION
  55. const platformName =
  56. process.platform === "win32" ? "windows" : process.platform
  57. const filename = `fzf-${version}-${platformName}_${arch}.${config.extension}`
  58. const url = `https://github.com/junegunn/fzf/releases/download/v${version}/${filename}`
  59. const response = await fetch(url)
  60. if (!response.ok)
  61. throw new DownloadFailedError({ url, status: response.status })
  62. const buffer = await response.arrayBuffer()
  63. const archivePath = path.join(Global.Path.bin, filename)
  64. await Bun.write(archivePath, buffer)
  65. if (config.extension === "tar.gz") {
  66. const proc = Bun.spawn(["tar", "-xzf", archivePath, "fzf"], {
  67. cwd: Global.Path.bin,
  68. stderr: "pipe",
  69. stdout: "pipe",
  70. })
  71. await proc.exited
  72. if (proc.exitCode !== 0)
  73. throw new ExtractionFailedError({
  74. filepath,
  75. stderr: await Bun.readableStreamToText(proc.stderr),
  76. })
  77. }
  78. if (config.extension === "zip") {
  79. const zipFileReader = new ZipReader(new BlobReader(new Blob([await Bun.file(archivePath).arrayBuffer()])));
  80. const entries = await zipFileReader.getEntries();
  81. let fzfEntry: any;
  82. for (const entry of entries) {
  83. if (entry.filename === "fzf.exe") {
  84. fzfEntry = entry;
  85. break;
  86. }
  87. }
  88. if (!fzfEntry) {
  89. throw new ExtractionFailedError({
  90. filepath: archivePath,
  91. stderr: "fzf.exe not found in zip archive",
  92. });
  93. }
  94. const fzfBlob = await fzfEntry.getData(new BlobWriter());
  95. if (!fzfBlob) {
  96. throw new ExtractionFailedError({
  97. filepath: archivePath,
  98. stderr: "Failed to extract fzf.exe from zip archive",
  99. });
  100. }
  101. await Bun.write(filepath, await fzfBlob.arrayBuffer());
  102. await zipFileReader.close();
  103. }
  104. await fs.unlink(archivePath)
  105. if (process.platform !== "win32") await fs.chmod(filepath, 0o755)
  106. }
  107. return {
  108. filepath,
  109. }
  110. })
  111. export async function filepath() {
  112. const { filepath } = await state()
  113. return filepath
  114. }
  115. }