Files

9.5 KiB

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 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.

  • 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.toolsManagedBinary[] 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 — 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

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.

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:
    {
      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:

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).