From fc9d2fdc3ab1404a0daa30e41f2805c91828f0dd Mon Sep 17 00:00:00 2001 From: Arthur Date: Sun, 27 Sep 2026 17:41:43 +0200 Subject: [PATCH] fix(ai): install oMLX without Homebrew stopping to ask Homebrew 7 asks for confirmation before an install that brings dependencies, and oMLX brings several. The question appeared deep in `./stack up ai`'s output, after the admin had already chosen to set up AI, and a script or an agent running the command waited on it for good. The install hook now installs oMLX and Whisper's build tools with `HOMEBREW_NO_ASK=1`. Older Homebrew ignores the variable. --- stacklets/ai/hooks/on_install.py | 10 ++++++++-- tests/unit/stacklets/test_ai_omlx_install.py | 15 ++++++++++++--- tests/unit/stacklets/test_ai_start_guard.py | 2 +- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/stacklets/ai/hooks/on_install.py b/stacklets/ai/hooks/on_install.py index 63cf3a72..6263a1d5 100644 --- a/stacklets/ai/hooks/on_install.py +++ b/stacklets/ai/hooks/on_install.py @@ -24,6 +24,12 @@ # third-party tap until that tap is trusted, so the trust step in # `_install_omlx_formula` is what stands between a working `stack up ai` # and an install that dies at the first brew command. +# Homebrew 7 asks before an install that brings dependencies, and the +# question is buried in `stack up ai`'s output, where a script or an agent +# waits on it for good. The admin already said yes to setting up AI. Older +# Homebrew ignores the variable, where it would refuse `--yes`. +BREW_INSTALL = "HOMEBREW_NO_ASK=1 brew install" + OMLX_TAP = "jundot/omlx" OMLX_TAP_URL = "https://github.com/jundot/omlx" WHISPER_MODEL = "ggml-large-v3-turbo.bin" @@ -99,7 +105,7 @@ def _install_omlx_formula(ctx) -> None: except RuntimeError: dim(" (this Homebrew has no trust gate — nothing to do)") - ctx.shell_live("brew install omlx --with-grammar") + ctx.shell_live(f"{BREW_INSTALL} omlx --with-grammar") def install_omlx(ctx, state_dir: Path): @@ -183,7 +189,7 @@ def _install_whisper(ctx, data_dir: Path, state_dir: Path): if missing: ctx.step(f"Installing build dependencies: {' '.join(missing)}...") - ctx.shell_live(f"brew install {' '.join(missing)}") + ctx.shell_live(f"{BREW_INSTALL} {' '.join(missing)}") done("Dependencies ready") # Clone source diff --git a/tests/unit/stacklets/test_ai_omlx_install.py b/tests/unit/stacklets/test_ai_omlx_install.py index 0442bf53..3be260d3 100644 --- a/tests/unit/stacklets/test_ai_omlx_install.py +++ b/tests/unit/stacklets/test_ai_omlx_install.py @@ -72,7 +72,7 @@ def test_the_tap_is_trusted_before_the_install_runs(self): trust_at = next(i for i, c in enumerate(ctx.commands) if c.startswith("brew trust")) install_at = next(i for i, c in enumerate(ctx.commands) - if c.startswith("brew install omlx")) + if "brew install omlx" in c) assert trust_at < install_at, "trusting after installing is too late" def test_the_tap_is_added_before_it_is_trusted(self): @@ -90,7 +90,16 @@ def test_grammar_is_requested_so_json_mode_works(self): # formula installs a build whose JSON mode is broken. ctx = FakeCtx() _install_omlx_formula(ctx) - assert any(c == "brew install omlx --with-grammar" for c in ctx.commands) + assert any(c.endswith("brew install omlx --with-grammar") for c in ctx.commands) + + def test_homebrew_does_not_stop_to_ask(self): + """Homebrew 7 asks before an install that brings dependencies. Deep + in `stack up ai` nobody expects that question, and a script or an + agent running it waits on it for good.""" + ctx = FakeCtx() + _install_omlx_formula(ctx) + (install,) = [c for c in ctx.commands if "brew install" in c] + assert install.startswith("HOMEBREW_NO_ASK=1 ") def test_steps_are_separate_commands_not_one_chained_string(self): """Chained with `&&`, any failure reported the whole string, so a @@ -105,4 +114,4 @@ def test_a_homebrew_without_a_trust_gate_still_installs(self): nothing to clear, so its absence is success, not a failed install.""" ctx = FakeCtx(no_trust_subcommand=True) _install_omlx_formula(ctx) # must not raise - assert any(c.startswith("brew install omlx") for c in ctx.commands) + assert any("brew install omlx" in c for c in ctx.commands) diff --git a/tests/unit/stacklets/test_ai_start_guard.py b/tests/unit/stacklets/test_ai_start_guard.py index 140153d4..3a5e261b 100644 --- a/tests/unit/stacklets/test_ai_start_guard.py +++ b/tests/unit/stacklets/test_ai_start_guard.py @@ -103,7 +103,7 @@ def test_a_missing_engine_is_installed(self, monkeypatch): on_start.run(ctx) - assert "brew install omlx --with-grammar" in ctx.shell_calls + assert any(c.endswith("brew install omlx --with-grammar") for c in ctx.shell_calls) def test_an_installed_engine_starts_normally(self, monkeypatch): monkeypatch.setattr(on_start.shutil, "which",