fix(install): find bun at standard install locations when PATH misses it (#762)

Fixes #707. shutil.which first, then ~/.bun/bin and the usual Homebrew, Linuxbrew, ~/.local/bin and /usr/local/bin locations, bun.exe on Windows, executable check on POSIX; the resolved path is used for every bun call and its directory is prepended to PATH for bun's own children.
This commit is contained in:
Ahmed authored and GitHub committed 2026-11-03 20:20:27 +01:00
1 parent a813602972
commit 87d1857139
2 files changed
+91 -4

No files matched your search

+53
View File
@@ -121,5 +121,58 @@ class InstallPortalClis(unittest.TestCase):
self.assertIn(f"would install {cli.parent.name}", proc.stdout)
class FindBun(unittest.TestCase):
def setUp(self):
directory = TemporaryDirectory()
self.addCleanup(directory.cleanup)
self.root = Path(directory.name)
def test_find_bun_prefers_path(self):
import shutil
orig_which = shutil.which
shutil.which = lambda name: "/custom/bin/bun" if name == "bun" else None
self.addCleanup(setattr, shutil, "which", orig_which)
self.assertEqual(install_portal_clis.find_bun(), "/custom/bin/bun")
def test_find_bun_falls_back_to_candidate_directory(self):
import shutil
orig_which = shutil.which
shutil.which = lambda name: None
self.addCleanup(setattr, shutil, "which", orig_which)
binary = "bun.exe" if sys.platform == "win32" else "bun"
fake_bin = self.root / ".bun" / "bin"
fake_bin.mkdir(parents=True)
fake_bun = fake_bin / binary
fake_bun.write_text("#!/bin/sh\n", encoding="utf-8")
if sys.platform != "win32":
fake_bun.chmod(0o755)
found = install_portal_clis.find_bun(candidate_dirs=(str(fake_bin),))
self.assertEqual(found, str(fake_bun))
def test_find_bun_returns_none_when_missing(self):
import shutil
orig_which = shutil.which
shutil.which = lambda name: None
self.addCleanup(setattr, shutil, "which", orig_which)
self.assertIsNone(install_portal_clis.find_bun(candidate_dirs=(str(self.root / "empty"),)))
def test_missing_bun_message_suggests_export(self):
output = io.StringIO()
orig_find = install_portal_clis.find_bun
install_portal_clis.find_bun = lambda *args, **kwargs: None
self.addCleanup(setattr, install_portal_clis, "find_bun", orig_find)
with redirect_stdout(output):
rc = install_portal_clis.main([])
self.assertEqual(rc, 1)
text = output.getvalue()
self.assertIn("bun was not found on PATH", text)
self.assertIn("export PATH=", text)
if __name__ == "__main__":
unittest.main()
+38 -4
View File
@@ -30,6 +30,7 @@ install failed.
import argparse
import json
import os
import shutil
import subprocess
import sys
@@ -96,6 +97,30 @@ def install(cli_dirs: list[Path], bun: str, dry_run: bool, root: Path = ROOT, ru
return 0
_BUN_CANDIDATE_DIRS = (
"~/.bun/bin", # default for bun.sh installer (curl -fsSL https://bun.sh/install | bash)
"/opt/homebrew/bin", # Homebrew on Apple Silicon macOS
"/usr/local/bin", # Homebrew on Intel macOS / standard Linux
"/home/linuxbrew/.linuxbrew/bin", # Linuxbrew system
"~/.linuxbrew/bin", # Linuxbrew user
"~/.local/bin", # pipx / user local
"/usr/bin", # distro packages
)
def find_bun(candidate_dirs: tuple[str, ...] = _BUN_CANDIDATE_DIRS) -> str | None:
"""Return `bun` on PATH, or at a standard install location."""
found = shutil.which("bun")
if found:
return found
binary = "bun.exe" if sys.platform == "win32" else "bun"
for dir_path in candidate_dirs:
candidate = Path(dir_path).expanduser() / binary
if candidate.is_file() and (sys.platform == "win32" or os.access(candidate, os.X_OK)):
return str(candidate)
return None
def _force_utf8_output() -> None:
"""Write UTF-8 whatever the host's default encoding is (a fork's portal name may not be ASCII)."""
for stream in (sys.stdout, sys.stderr):
@@ -104,20 +129,29 @@ def _force_utf8_output() -> None:
reconfigure(encoding="utf-8")
def main() -> int:
def main(argv=None) -> int:
_force_utf8_output()
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument("--dry-run", action="store_true", help="list what would be installed, run nothing")
args = ap.parse_args()
args = ap.parse_args(argv)
cli_dirs = discover()
if not cli_dirs:
print("No portal CLIs found under .agents/skills/*/cli/ - nothing to install.")
return 0
bun = shutil.which("bun")
bun = find_bun()
if not bun and not args.dry_run:
print("bun is not on PATH. Install it from https://bun.sh, open a new shell, and re-run.")
print(
"bun was not found on PATH and was not found at any of the usual install locations.\n"
' - installed via bun.sh? export PATH="$HOME/.bun/bin:$PATH"\n'
" - installed via Homebrew/Linuxbrew? brew install bun\n"
" - installed elsewhere? reinstall to a directory on PATH"
)
return 1
if bun:
bun_dir = str(Path(bun).parent)
if bun_dir not in os.environ.get("PATH", "").split(os.pathsep):
os.environ["PATH"] = bun_dir + os.pathsep + os.environ.get("PATH", "")
return install(cli_dirs, bun or "bun", args.dry_run)