index.ts 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. import { App } from "../app/app"
  2. import path from "path"
  3. import { Global } from "../global"
  4. import fs from "fs/promises"
  5. import { z } from "zod"
  6. import { NamedError } from "../util/error"
  7. export namespace Ripgrep {
  8. const PLATFORM = {
  9. darwin: { platform: "apple-darwin", extension: "tar.gz" },
  10. linux: { platform: "unknown-linux-musl", extension: "tar.gz" },
  11. win32: { platform: "pc-windows-msvc", extension: "zip" },
  12. } as const
  13. export const ExtractionFailedError = NamedError.create(
  14. "RipgrepExtractionFailedError",
  15. z.object({
  16. filepath: z.string(),
  17. stderr: z.string(),
  18. }),
  19. )
  20. export const UnsupportedPlatformError = NamedError.create(
  21. "RipgrepUnsupportedPlatformError",
  22. z.object({
  23. platform: z.string(),
  24. }),
  25. )
  26. export const DownloadFailedError = NamedError.create(
  27. "RipgrepDownloadFailedError",
  28. z.object({
  29. url: z.string(),
  30. status: z.number(),
  31. }),
  32. )
  33. const state = App.state("ripgrep", async () => {
  34. const filepath = path.join(
  35. Global.Path.bin,
  36. "rg" + (process.platform === "win32" ? ".exe" : ""),
  37. )
  38. const file = Bun.file(filepath)
  39. if (!(await file.exists())) {
  40. const archMap = { x64: "x86_64", arm64: "aarch64" } as const
  41. const arch = archMap[process.arch as keyof typeof archMap] ?? process.arch
  42. const config = PLATFORM[process.platform as keyof typeof PLATFORM]
  43. if (!config)
  44. throw new UnsupportedPlatformError({ platform: process.platform })
  45. const version = "14.1.1"
  46. const filename = `ripgrep-${version}-${arch}-${config.platform}.${config.extension}`
  47. const url = `https://github.com/BurntSushi/ripgrep/releases/download/${version}/${filename}`
  48. const response = await fetch(url)
  49. if (!response.ok)
  50. throw new DownloadFailedError({ url, status: response.status })
  51. const buffer = await response.arrayBuffer()
  52. const archivePath = path.join(Global.Path.bin, filename)
  53. await Bun.write(archivePath, buffer)
  54. if (config.extension === "tar.gz") {
  55. const proc = Bun.spawn(
  56. [
  57. "tar",
  58. "-xzf",
  59. archivePath,
  60. "--strip-components=1",
  61. "--wildcards",
  62. "*/rg",
  63. ],
  64. {
  65. cwd: Global.Path.bin,
  66. stderr: "pipe",
  67. stdout: "pipe",
  68. },
  69. )
  70. await proc.exited
  71. if (proc.exitCode !== 0)
  72. throw new ExtractionFailedError({
  73. filepath,
  74. stderr: await Bun.readableStreamToText(proc.stderr),
  75. })
  76. }
  77. if (config.extension === "zip") {
  78. const proc = Bun.spawn(
  79. ["unzip", "-j", archivePath, "*/rg.exe", "-d", Global.Path.bin],
  80. {
  81. cwd: Global.Path.bin,
  82. stderr: "ignore",
  83. stdout: "ignore",
  84. },
  85. )
  86. await proc.exited
  87. if (proc.exitCode !== 0)
  88. throw new ExtractionFailedError({
  89. filepath: archivePath,
  90. stderr: await Bun.readableStreamToText(proc.stderr),
  91. })
  92. }
  93. await fs.unlink(archivePath)
  94. if (process.platform !== "win32") await fs.chmod(filepath, 0o755)
  95. }
  96. return {
  97. filepath,
  98. }
  99. })
  100. export async function filepath() {
  101. const { filepath } = await state()
  102. return filepath
  103. }
  104. }