From a6bb6e4e00d76f6f055c1f8c82aee55c2e4015af Mon Sep 17 00:00:00 2001 From: Daniel Holanda Date: Thu, 20 Aug 2026 18:01:32 -0700 Subject: [PATCH] Ensure correct version of Lemonade is installed --- skills/local-ai-use/SKILL.md | 60 +++++---- skills/local-ai-use/scripts/setup_local_ai.py | 120 ++++++++++-------- 2 files changed, 100 insertions(+), 80 deletions(-) diff --git a/skills/local-ai-use/SKILL.md b/skills/local-ai-use/SKILL.md index 8840ef8..d97d94c 100644 --- a/skills/local-ai-use/SKILL.md +++ b/skills/local-ai-use/SKILL.md @@ -40,12 +40,12 @@ The skill does three things: > **Requires modern Lemonade (v10.1.0 or newer).** Modern Lemonade unified > everything under one `lemonade` CLI (`lemonade status`, `lemonade pull`, ...) -> driving an always-on `lemond` service. This skill targets that and installs -> Lemonade only from the official installers (see below) — never via -> `pip install lemonade-sdk`, which is a separate, older release line. If an -> older/incompatible `lemonade` is already on the PATH (from any channel — an -> old `.msi`/`.deb` or a pip install), it will shadow the modern CLI; uninstall -> it first (see the removal commands in Step 1a) before running this skill. +> driving an always-on `lemond` service. `lemonade` is the only valid CLI, and +> this skill installs Lemonade only from the [official install +> paths](https://lemonade-server.ai/docs/guide/install/) listed in Step 1a. If +> an older `lemonade` is already on the `PATH` it will shadow the modern CLI; +> uninstall it first (see the removal commands in Step 1a) before running this +> skill. Models are **not** downloaded during setup. Each default model is pulled lazily, on first use, by the routing rule (e.g. the first image request pulls @@ -69,12 +69,12 @@ instead. ## Prerequisites - **OS:** Windows 11 x64, Ubuntu/Debian x64, or macOS (beta). -- **Lemonade:** the setup script installs it if missing. It downloads and - silently installs the latest version (Windows `lemonade.msi`, the - Ubuntu/Debian `ppa:lemonade-team/stable` PPA, or the macOS `.pkg`). The - `lemond` service auto-starts after install; the script waits for it rather - than launching it. On Linux/macOS the install needs `sudo`. Pass - `--no-install` if the user wants to install it themselves instead. +- **Lemonade:** the setup script installs the latest version if missing, using + `winget` on Windows, the `ppa:lemonade-team/stable` PPA on Ubuntu/Debian, and + the Homebrew cask on macOS (see Step 1a for the fallbacks). The `lemond` + service auto-starts after install; the script waits for it rather than + launching it. On Linux the install needs `sudo`. Pass `--no-install` if the + user wants to install it themselves instead. - **Disk:** ~8 GB free for the three default models (SD-Turbo + Whisper-Tiny + kokoro-v1), plus ~0.1 GB for the installer itself. - **Network:** required for the install download and the first `lemonade pull` @@ -119,28 +119,32 @@ so you can do it by hand or debug it: **1a. Is a modern `lemonade` CLI installed?** Run `lemonade status`. The check is by *capability*, not by name: modern Lemonade prints `Server is running...` or `Server is not running`. If instead you get an "invalid choice" / usage -error, the `lemonade` on `PATH` is an old, incompatible build that predates the -unified CLI (v10.1.0) — do **not** use it. It could have come from any install -channel, so remove it based on how it was installed, then re-run this skill (or -install Lemonade manually): +error, the `lemonade` on `PATH` is an old build that predates the unified CLI +(v10.1.0) — do **not** use it. Remove it, then re-run this skill: -| Installed via | Uninstall with | +| OS | Uninstall the old build with | |---|---| -| Windows `.msi` | `winget uninstall -e --id AMD.LemonadeServer`, or Settings > Apps > Installed apps > Lemonade Server > Uninstall | -| Ubuntu/Debian apt/PPA | `sudo apt remove lemonade-server` | -| pip | `pip uninstall lemonade-sdk` | -| macOS `.pkg` | Delete the installed `Lemonade.app` / remove the package receipt | +| Windows | `winget uninstall -e --id AMD.LemonadeServer`, or Settings > Apps > Installed apps > Lemonade Server > Uninstall | +| Ubuntu/Debian | `sudo apt remove lemonade-server` | +| macOS | `brew uninstall --cask lemonade-server`, or delete the installed `Lemonade.app` and its `.pkg` receipt | Never try to drive or auto-remove it for the user. If no `lemonade` is found at all, install the latest version on the user's -behalf: +behalf. Use the package manager first; the download is the fallback when the +package manager is absent. Full matrix, including Arch, Fedora, Debian, Snap, +and Docker, is in the [install docs](https://lemonade-server.ai/docs/guide/install/). -| OS | Install | -|---|---| -| Windows | Download `lemonade.msi` from the [latest release](https://github.com/lemonade-sdk/lemonade/releases/latest/download/lemonade.msi) and run `msiexec /i lemonade.msi /qn` (silent, per-user, no elevation). | -| Ubuntu/Debian | `sudo add-apt-repository -y ppa:lemonade-team/stable && sudo apt-get update && sudo apt-get install -y lemonade-server` (the apt package is `lemonade-server`; the CLI you then run is `lemonade`) | -| macOS (beta) | Download the `Lemonade--Darwin.pkg` from the latest release and run `sudo installer -pkg Lemonade--Darwin.pkg -target /`. | +| OS | Install | Fallback | +|---|---|---| +| Windows | `winget install -e --id AMD.LemonadeServer` | Download [`lemonade.msi`](https://github.com/lemonade-sdk/lemonade/releases/latest/download/lemonade.msi) and run `msiexec /i lemonade.msi /qn` (silent, per-user, no elevation). | +| Ubuntu | `sudo add-apt-repository -y ppa:lemonade-team/stable && sudo apt-get update && sudo apt-get install -y lemonade-server` | `sudo snap install lemonade-server` | +| macOS | `brew install --cask lemonade-server` | Download `Lemonade--Darwin.pkg` from the [latest release](https://github.com/lemonade-sdk/lemonade/releases/latest) and run `sudo installer -pkg Lemonade--Darwin.pkg -target /`. | + +The Ubuntu apt package is named `lemonade-server`, but the CLI it installs is +`lemonade`. The browser UI is served at `http://localhost:13305` with no extra +package; add `sudo apt install lemonade-desktop` only if the user wants the +desktop frontend. After a Windows install the CLI lands in `%LOCALAPPDATA%\lemonade_server` and is added to the *user* PATH (new shells only); the setup script probes that @@ -250,7 +254,7 @@ machine. | Symptom | Cause | Recovery | |---|---|---| | `lemonade: command not found` | CLI not installed | Re-run `python scripts/setup_local_ai.py` (auto-installs the latest version). If it just installed on Windows, open a new shell so the user PATH refreshes, or the script will find it under `%LOCALAPPDATA%\lemonade_server`. | -| `status` gives an "invalid choice" / usage error | An old, incompatible `lemonade` (pre-v10.1.0, from any install channel) is shadowing the modern CLI | Uninstall it the way it was installed (see the Step 1a table: `winget uninstall -e --id AMD.LemonadeServer` / `sudo apt remove lemonade-server` / `pip uninstall lemonade-sdk`), then re-run the setup script or install Lemonade from the docs link. | +| `status` gives an "invalid choice" / usage error | An old `lemonade` (pre-v10.1.0) is shadowing the modern CLI | Uninstall it (see the Step 1a table: `winget uninstall -e --id AMD.LemonadeServer` / `sudo apt remove lemonade-server` / `brew uninstall --cask lemonade-server`), then re-run the setup script. | | `Server is not running` | `lemond` service stopped | Start it via the OS service manager — `sudo systemctl start lemond` / `systemctl --user start lemond` (Linux), `launchctl load /Library/LaunchDaemons/com.lemonade.server.plist` (macOS), or the tray app / `Start-Service lemond` (Windows). There is no `lemonade serve`. | | `POST /v1/images/generations` returns 404 model not found | Image model not downloaded | `lemonade pull SD-Turbo` and retry. | | `lemonade pull` keeps printing `Progress: NN%` but never finishes | Download target is a bad path (out of space, no write permission, quota, read-only mount). The write error may surface only in the server log while the console keeps showing progress | Check the target and free space first: `GET /api/v1/system-info` reports `models_dir` and `model_storage.free_bytes`. If a pull stalls, read the recent lines of the server log (typically `lemonade-server.log` in the OS temp dir) for the real error (e.g. a download/write failure like `CURL code 23`, or an out-of-space message), then point the download at a writable disk with room. | diff --git a/skills/local-ai-use/scripts/setup_local_ai.py b/skills/local-ai-use/scripts/setup_local_ai.py index 52fc02a..a5e6112 100644 --- a/skills/local-ai-use/scripts/setup_local_ai.py +++ b/skills/local-ai-use/scripts/setup_local_ai.py @@ -23,14 +23,12 @@ /AGENTS.md, between stable BEGIN/END markers so re-runs replace the block in place rather than appending. -Modern Lemonade (v10.1.0+) unified everything under the single `lemonade` -CLI (subcommands `status`, `pull`, `run`, ...) driving an always-on `lemond` -service. Older/incompatible builds of `lemonade` -- whichever way they were -installed (an old .msi/.deb, or the pip `lemonade-sdk` package) -- lack the -service-control subcommands and are NOT supported here. The CLI check below -therefore probes *capability* (does `lemonade status` work?) rather than -trusting the binary name, so an old incompatible CLI on PATH is reported -clearly instead of being driven into a 90-second dead end. +Modern Lemonade unified everything under a single `lemonade` CLI (subcommands +`status`, `pull`, `run`, ...) driving an always-on `lemond` service. That is +the only CLI this skill supports. An older build left on PATH lacks those +subcommands, so the check below probes *capability* (does `lemonade status` +work?) rather than trusting the binary name, and reports a stale CLI clearly +instead of driving it into a 90-second dead end. Setup never downloads models: the default image/TTS/STT models are pulled on first use, by the installed AGENTS.md rule (see its failure @@ -82,10 +80,14 @@ SKILL_DIR = Path(__file__).resolve().parent.parent RULE_TEMPLATE = SKILL_DIR / "templates" / "local-ai-rule.md" -# The *full* Windows installer: Lemonade plus the desktop app and the -# always-on `lemond` service. `releases/latest/download/` always -# resolves to the newest published asset of that exact name, so we never have -# to pin a version. +# Windows: winget is the current recommended install and keeps Lemonade +# upgradable in place, so we try it first. +WINDOWS_WINGET_ID = "AMD.LemonadeServer" + +# Fallback when winget is unavailable: the *full* Windows installer, Lemonade +# plus the desktop app and the always-on `lemond` service. +# `releases/latest/download/` always resolves to the newest published +# asset of that exact name, so we never have to pin a version. WINDOWS_MSI_URL = ( "https://github.com/lemonade-sdk/lemonade/releases/latest/download/lemonade.msi" ) @@ -96,6 +98,11 @@ os.environ.get("LOCALAPPDATA", str(Path.home() / "AppData" / "Local")) ) / "lemonade_server" +# macOS: the Homebrew cask is the current recommended install. It also avoids +# the unauthenticated GitHub API call the .pkg fallback needs, which is rate +# limited per source IP. +MACOS_BREW_CASK = "lemonade-server" + # GitHub release metadata, used to resolve the versioned macOS .pkg asset # (its filename embeds the version, so there is no stable latest/download URL). GITHUB_LATEST_RELEASE_API = ( @@ -112,10 +119,9 @@ "sudo apt-get install -y lemonade-server" ) -# Modern Lemonade exposes a single `lemonade` CLI (it drives the always-on -# `lemond` service). We deliberately do NOT fall back to the deprecated -# `lemonade-server` CLI or the pip "eval" build; instead we verify the CLI we -# find is actually the modern one via a capability probe (see find_cli). +# Modern Lemonade exposes exactly one CLI, `lemonade`, driving the always-on +# `lemond` service. There is no second binary to fall back to, so instead of +# trying other names we verify this one is the modern build (see find_cli). CLI_NAME = "lemonade" # Docs URL to point users at when they must install/upgrade Lemonade by hand. @@ -171,13 +177,11 @@ def is_modern_cli(cli: str) -> bool: """True if `cli` is the modern Lemonade CLI (drives the `lemond` service). Capability probe, not a name check: the modern `lemonade` CLI exposes a - `status` subcommand that reports on the service. An older or otherwise - incompatible `lemonade` on PATH -- regardless of how it was installed (an - old .msi/.deb, or the pip `lemonade-sdk` build) -- does not recognise the - subcommand and errors with an argparse "invalid choice" instead. Running - `lemonade status` is cheap, does not mutate anything, and tells us both - that the CLI is modern AND whether the service is already up, so we reuse - it as the single discriminator. + `status` subcommand that reports on the service. An older `lemonade` on + PATH does not recognise the subcommand and errors with an argparse + "invalid choice" instead. Running `lemonade status` is cheap, does not + mutate anything, and tells us both that the CLI is modern AND whether the + service is already up, so we reuse it as the single discriminator. A modern CLI prints "Server is running..." or "Server is not running" (exiting 0 or 1 accordingly). We key off that phrasing rather than the @@ -206,8 +210,7 @@ def find_cli() -> tuple[str | None, str | None]: Returns ``(modern_cli, stale_cli)``: - ``(path, None)`` a modern, capable `lemonade` CLI was found -> use it. - - ``(None, path)`` a `lemonade` executable exists but is NOT the modern - CLI (old pip "eval" build, or otherwise incompatible) + - ``(None, path)`` a `lemonade` executable exists but is an older build -> the caller should guide the user to upgrade. - ``(None, None)`` no `lemonade` executable found at all -> install it. """ @@ -257,7 +260,25 @@ def _run(cmd: list[str] | str, *, shell: bool = False) -> None: def _install_windows() -> None: - """Silently install the full lemonade.msi (server + desktop app).""" + """Install with winget, falling back to the signed full MSI.""" + if shutil.which("winget") is not None: + try: + _run( + [ + "winget", + "install", + "-e", + "--id", + WINDOWS_WINGET_ID, + "--accept-package-agreements", + "--accept-source-agreements", + "--disable-interactivity", + ] + ) + _print("Lemonade installed.") + return + except RuntimeError as exc: + _print(f"winget install did not complete ({exc}); trying the MSI.") msi = Path(tempfile.gettempdir()) / "lemonade.msi" _download(WINDOWS_MSI_URL, msi) # /qn = silent, per-user (no elevation needed). The MSI registers the CLI @@ -267,7 +288,7 @@ def _install_windows() -> None: def _install_linux() -> None: - """Install the stable PPA server plus the desktop frontend on apt distros.""" + """Install from the stable PPA on apt distros.""" if shutil.which("apt-get") is None: raise RuntimeError( "Automatic install only supports apt-based distros (Ubuntu/Debian). " @@ -283,7 +304,14 @@ def _install_linux() -> None: def _install_macos() -> None: - """Download the latest signed .pkg and install it system-wide.""" + """Install the Homebrew cask, falling back to the signed .pkg.""" + if shutil.which("brew") is not None: + try: + _run(["brew", "install", "--cask", MACOS_BREW_CASK]) + _print("Lemonade installed.") + return + except RuntimeError as exc: + _print(f"Homebrew install did not complete ({exc}); trying the .pkg.") pkg_url = _resolve_macos_pkg_url() pkg = Path(tempfile.gettempdir()) / "Lemonade.pkg" _download(pkg_url, pkg) @@ -336,32 +364,21 @@ def service_start_hint() -> str: def uninstall_hint() -> str: - """OS-specific ways to remove an old/incompatible Lemonade. - - An incompatible `lemonade` could come from any install channel, so we do - not assume pip. We list the removal path for each channel and let the user - apply whichever one matches how they installed it. - """ + """OS-specific ways to remove an old Lemonade that shadows the modern CLI.""" system = platform.system() if system == "Linux": - return ( - "remove it however it was installed: `sudo apt remove lemonade-server` " - "(apt/PPA) or `pip uninstall lemonade-sdk` (pip)" - ) + return "`sudo apt remove lemonade-server`, or the package manager it came from" if system == "Darwin": # macOS return ( - "remove it however it was installed: delete the installed " - "`Lemonade.app`/receipt from a .pkg install, or " - "`pip uninstall lemonade-sdk` (pip)" + f"`brew uninstall --cask {MACOS_BREW_CASK}`, or delete the installed " + "`Lemonade.app` and its .pkg receipt" ) if system == "Windows": return ( - "remove it however it was installed: for an .msi install, run " - "`winget uninstall -e --id AMD.LemonadeServer` or uninstall Lemonade " - "Server from Settings > Apps > Installed apps; or, if it came from " - "pip, `pip uninstall lemonade-sdk`" + f"`winget uninstall -e --id {WINDOWS_WINGET_ID}`, or uninstall " + "Lemonade Server from Settings > Apps > Installed apps" ) - return "remove the old Lemonade using your platform's package manager or `pip uninstall lemonade-sdk`" + return f"remove it with your platform's package manager; see {INSTALL_DOCS_URL}" def wait_for_server(host: str, port: int, timeout_s: float = 90.0) -> bool: @@ -449,6 +466,7 @@ def upsert_agents_md( ) if not target.exists(): + workspace.mkdir(parents=True, exist_ok=True) target.write_text( "# Agent instructions\n\n" "Project-scoped rules picked up automatically by Cursor, Claude Code,\n" @@ -524,13 +542,11 @@ def main(argv: list[str] | None = None) -> int: cli, stale = find_cli() - # An old/incompatible `lemonade` on PATH shadows the modern CLI. It may - # have come from any install channel (old .msi/.deb or pip), so we never - # assume one. We also never drive or auto-remove it -- we guide the user, - # because a shadowing binary will keep hiding a freshly installed modern - # CLI until it is removed. + # An old `lemonade` on PATH shadows the modern CLI. We never drive or + # auto-remove it -- we guide the user, because a shadowing binary will keep + # hiding a freshly installed modern CLI until it is removed. if cli is None and stale is not None: - _print(f"FAIL: found an old/incompatible Lemonade CLI at {stale}.") + _print(f"FAIL: found an old Lemonade CLI at {stale}.") _print( "It is missing the modern `lemonade status` command, so it predates " "the unified CLI (v10.1.0) and cannot be used by this skill."