index.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. import z from "zod"
  2. import { Global } from "../global"
  3. import { Log } from "../util/log"
  4. import path from "path"
  5. import { NamedError } from "@opencode-ai/util/error"
  6. import { readableStreamToText } from "bun"
  7. import { createRequire } from "module"
  8. import { Lock } from "../util/lock"
  9. export namespace BunProc {
  10. const log = Log.create({ service: "bun" })
  11. const req = createRequire(import.meta.url)
  12. export async function run(cmd: string[], options?: Bun.SpawnOptions.OptionsObject<any, any, any>) {
  13. log.info("running", {
  14. cmd: [which(), ...cmd],
  15. ...options,
  16. })
  17. const result = Bun.spawn([which(), ...cmd], {
  18. ...options,
  19. stdout: "pipe",
  20. stderr: "pipe",
  21. env: {
  22. ...process.env,
  23. ...options?.env,
  24. BUN_BE_BUN: "1",
  25. },
  26. })
  27. const code = await result.exited
  28. const stdout = result.stdout
  29. ? typeof result.stdout === "number"
  30. ? result.stdout
  31. : await readableStreamToText(result.stdout)
  32. : undefined
  33. const stderr = result.stderr
  34. ? typeof result.stderr === "number"
  35. ? result.stderr
  36. : await readableStreamToText(result.stderr)
  37. : undefined
  38. log.info("done", {
  39. code,
  40. stdout,
  41. stderr,
  42. })
  43. if (code !== 0) {
  44. throw new Error(`Command failed with exit code ${result.exitCode}`)
  45. }
  46. return result
  47. }
  48. export function which() {
  49. return process.execPath
  50. }
  51. export const InstallFailedError = NamedError.create(
  52. "BunInstallFailedError",
  53. z.object({
  54. pkg: z.string(),
  55. version: z.string(),
  56. }),
  57. )
  58. export async function install(pkg: string, version = "latest") {
  59. // Use lock to ensure only one install at a time
  60. using _ = await Lock.write("bun-install")
  61. const mod = path.join(Global.Path.cache, "node_modules", pkg)
  62. const pkgjson = Bun.file(path.join(Global.Path.cache, "package.json"))
  63. const parsed = await pkgjson.json().catch(async () => {
  64. const result = { dependencies: {} }
  65. await Bun.write(pkgjson.name!, JSON.stringify(result, null, 2))
  66. return result
  67. })
  68. if (parsed.dependencies[pkg] === version) return mod
  69. const proxied = !!(
  70. process.env.HTTP_PROXY ||
  71. process.env.HTTPS_PROXY ||
  72. process.env.http_proxy ||
  73. process.env.https_proxy
  74. )
  75. // Build command arguments
  76. const args = [
  77. "add",
  78. "--force",
  79. "--exact",
  80. // TODO: get rid of this case (see: https://github.com/oven-sh/bun/issues/19936)
  81. ...(proxied ? ["--no-cache"] : []),
  82. "--cwd",
  83. Global.Path.cache,
  84. pkg + "@" + version,
  85. ]
  86. // Let Bun handle registry resolution:
  87. // - If .npmrc files exist, Bun will use them automatically
  88. // - If no .npmrc files exist, Bun will default to https://registry.npmjs.org
  89. // - No need to pass --registry flag
  90. log.info("installing package using Bun's default registry resolution", {
  91. pkg,
  92. version,
  93. })
  94. await BunProc.run(args, {
  95. cwd: Global.Path.cache,
  96. }).catch((e) => {
  97. throw new InstallFailedError(
  98. { pkg, version },
  99. {
  100. cause: e,
  101. },
  102. )
  103. })
  104. // Resolve actual version from installed package when using "latest"
  105. // This ensures subsequent starts use the cached version until explicitly updated
  106. let resolvedVersion = version
  107. if (version === "latest") {
  108. const installedPkgJson = Bun.file(path.join(mod, "package.json"))
  109. const installedPkg = await installedPkgJson.json().catch(() => null)
  110. if (installedPkg?.version) {
  111. resolvedVersion = installedPkg.version
  112. }
  113. }
  114. parsed.dependencies[pkg] = resolvedVersion
  115. await Bun.write(pkgjson.name!, JSON.stringify(parsed, null, 2))
  116. return mod
  117. }
  118. }