index.ts 3.9 KB

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