Files

139 lines
9.5 KiB
Markdown

# BinaryManager Reference
BinaryManager is the single lifecycle service responsible for acquiring and managing third-party CLI binaries (uv, bun, ripgrep, claude-code, gh, etc.). It wraps [mise](https://mise.jdx.dev) as the only acquisition backend.
> **Why mise, no custom backend interface?** mise already ships a polyglot tool grammar (`npm:`, `pipx:`, `github:`, `http:`, plus its built-in registry). Building a `BinaryBackend` abstraction over the top would be a shallow wrapper that re-implements grammar mise already owns. We delete more code by importing mise's primitives directly than by hiding them behind our own seam.
## Quick links
- Implementation: `src/main/services/BinaryManager.ts`
- IPC channels: `src/shared/ipc/schemas/binary.ts` (`binary.*`)
- Persisted state: `feature.binary.tools` preference + `feature.binary.state_file` path
- Preset catalog: `src/shared/data/presets/binaryTools.ts`
- Renderer entry point: `src/renderer/pages/settings/McpSettings/EnvironmentDependencies.tsx`
## Scope: what belongs and what doesn't
> BinaryManager manages **single, relocatable CLI binaries installable via mise's backends**. Multi-file server packages, tools requiring host hardware detection, or tools that generate their own configuration belong with their domain service.
| Tool | Status | Reason |
|---|---|---|
| uv, bun, ripgrep | **In** — bundled + mise-managed | Single relocatable binaries |
| fd, rtk | **In** — mise-managed | Single relocatable binaries installed on demand |
| claude-code, gh, opencode, gemini-cli, etc. | **In** — mise-managed | Installable via `npm:` / `pipx:` / mise registry |
| OvmsManager | **Out** — domain service | OS-specific multi-file tarball, hardware detection, generated config |
| Tesseract (`feature.ocr.tesseract`) | **Out** — data/models | Not a CLI binary; OCR data files live with `TesseractRuntimeService` |
When adding a new tool, ask: *can mise install this as a single binary?* If yes, it goes in BinaryManager. If it needs hardware checks, multi-file extraction, or post-install patching, it stays with its domain service.
## Persisted / contract surface
These are the stable boundaries that survive across versions and renderer reloads. Treat them as the public API:
| Surface | Value | Used by |
|---|---|---|
| Preference key | `feature.binary.tools``ManagedBinary[]` | Renderer custom-tool list |
| Path key | `feature.binary.data``~/.cherrystudio/binary-manager` | mise install root |
| Path key | `feature.binary.state_file``~/.cherrystudio/binary-manager/state.json` | Install state on disk |
| Path key | `cherry.bin``~/.cherrystudio/bin` | Bundled-binary extraction target |
| IPC | `binary.install_tool`, `binary.remove_tool`, `binary.get_state`, `binary.search_registry`, `binary.get_tool_dir`, `binary.probe_bundled` | Renderer → main |
| IPC events | `binary.state_changed`, `binary.reconcile_failed` | Main → renderer |
| Types | `ManagedBinary`, `BinaryState`, `ToolInstallState` (`src/shared/data/preference/preferenceTypes.ts`) | Both sides |
`ManagedBinary` is `{ name, tool, version? }` where `tool` is a mise tool spec (`npm:foo`, `pipx:bar`, `gh`, `claude`, …). Adding new fields requires regenerating preference schemas via `cd v2-refactor-temp/tools/data-classify && npm run generate`.
> **No v1→v2 migrator.** v2 data is throwaway per [CLAUDE.md](../../../CLAUDE.md) — the v2 pref key (`feature.binary.tools`) has no predecessor in v1, so there is intentionally nothing to migrate.
## Path resolution: one resolver, two sources
```text
getBinaryPath(name) → mise shim → cherry.bin → binary name (PATH fallback)
──────── ────────── ─────────────────────────────
mise-managed bundled resolved by user shell at exec
```
`getBinaryPath()` in `src/main/utils/binaryResolver.ts` is the **only** path resolver. Direct `os.homedir() + HOME_CHERRY_DIR` joins are forbidden — use `application.getPath('cherry.bin')` / `application.getPath('feature.binary.data')` instead.
## Why state is a file, not DataApi / Preference
BinaryManager state is operational cache for installed shim metadata, not user-authored business data. It must be readable before renderer windows exist, written atomically alongside the tool manager's filesystem operations, and safe to rebuild from `mise` plus the user's `feature.binary.tools` preference if lost. A small JSON file keeps that operational state close to the binaries it describes without adding a SQLite/DataApi boundary for non-business data.
## State contract: bundled vs mise-managed
Three sources for a tool to be available, in order of precedence:
| State | Detected by | UI label |
|---|---|---|
| **managed (mise)** | `BinaryState.tools[name]` is set after `mise use -g` | "v1.2.3" version chip |
| **available (bundled)** | `binary:probe-bundled` finds the binary in `cherry.bin` after extraction | "bundled" chip + "Install via mise" CTA |
| **not installed** | Neither of the above | "Install" CTA |
**Why we don't seed `BinaryState` on extraction:** BinaryState is the authoritative record of "user actively installed via mise". Writing extraction artifacts into it would conflate two sources (build-time bundled vs runtime user-installed), force a `source` discriminator on every entry, and cause state drift every time a release ships with a new bundled version. The probe-bundled IPC keeps the two sources orthogonal: BinaryState answers "what did the user install?", the filesystem probe answers "what shipped in the box?".
The bundled set is currently `bun`, `uv`, `rg`. mise itself is also bundled but is internal infrastructure, not user-visible. RTK is installed on demand from Settings → Plugins instead of being extracted automatically at startup.
**Precedence when both sources are present.** `getBinarySearchDirs()` lists the mise shims directory before `cherry.bin`, so if a user clicks *Install via mise* on a bundled tool (e.g. `uv`), the mise-managed version wins at `getBinaryPath('uv')` and consumers immediately use the newer copy. The bundled copy stays on disk as a fallback when the mise shim is absent or broken; the UI re-probes after install and updates the "managed / bundled" label accordingly.
## GitHub rate-limit opt-in
mise's `github:` backend (used by `github:larksuite/cli`, `github:sharkdp/fd`, etc.) hits the GitHub releases API to resolve versions. The unauthenticated limit is 60 req/hour per IP — easily exhausted behind shared NAT (offices, mainland-China ISPs, Codespaces, CI).
`BinaryManager.buildIsolatedEnv()` does **not** forward the ambient `GITHUB_TOKEN` / `GH_TOKEN` from the user's shell, to avoid leaking a general-purpose dev token into mise's process env without consent. Users who hit the rate limit can opt in by setting `CHERRY_GITHUB_TOKEN` in their shell before launching Cherry; it is forwarded to mise as `GITHUB_TOKEN`, raising the limit to 5000 req/hour.
```bash
export CHERRY_GITHUB_TOKEN=ghp_xxx # optional, only needed if installs fail with HTTP 403
```
## China mirror behavior
`BinaryManager.buildIsolatedEnv()` calls `isUserInChina()` and, when true, injects mirror URLs into the mise subprocess env:
- `NPM_CONFIG_REGISTRY=https://registry.npmmirror.com`
- `PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple`
These are passthrough — if the user already has either var in their shell env, the user value wins. Mirror selection happens once per app launch and applies to all `npm:` / `pipx:` backends without per-tool configuration.
## Adding a new managed binary
**Preset (built-in tool, appears in the predefined list):**
1. Add an entry to `PRESETS_BINARY_TOOLS` in `src/shared/data/presets/binaryTools.ts`:
```ts
{
name: 'gh', // executable name (also the mise shim name)
displayName: 'GitHub CLI',
tool: 'gh', // mise tool spec — registry entry, npm:..., pipx:..., etc.
icon: 'simple-icons:github', // optional iconify id
repoUrl: 'https://github.com/cli/cli',
homepage: 'https://cli.github.com/' // optional
}
```
2. Add a description translation key under `settings.plugins.tools.<name>` in `src/renderer/i18n/locales/en-us.json`, then run `pnpm i18n:sync`.
3. No code change in BinaryManager — the renderer picks it up via the preset list.
**Custom (user-added from the settings UI):**
1. User clicks "Add Tool" and selects a registry result.
2. Renderer writes to `feature.binary.tools` preference after `binary.install_tool` succeeds; BinaryManager reconciles saved tools during startup.
**To bundle the binary at build time** (so it's available without mise install — only for tools small enough to ship):
1. Add the tool to `scripts/download-binaries.js` with platform-specific URLs and SHA256 checksums.
2. Add it to the module-level `BUNDLED_TOOLS` array in `BinaryManager.ts` — a single source consumed by both `extractBundledBinaries()` (boot extraction) and `probeBundled()` (UI "bundled" state), so one entry wires up both.
## Consumer pattern
From other main-process services:
```ts
const result = await application.get('BinaryManager').installTool({
name: 'gh',
tool: 'gh'
})
// result is { version: string }
```
Examples: `OpenClawService.install()` calls `installTool({name: 'openclaw', tool: 'npm:openclaw'})`; `CodeCliService.run()` calls `installTool()` lazily when the executable isn't on disk.
Do not re-implement install/uninstall logic in your service — delegate to BinaryManager and keep your service focused on runtime orchestration (config generation, process spawning, health checks).