mnml-rs 0.2.21

A NvChad-style terminal IDE in Rust — vim or standard editing, LSP, git, and an embedded HTTP client.
# Auto-update integrations — design (task #993)

Status: design draft — 2026-08-19. Implementation to follow when CI is
green and this doc is reviewed.

## Problem

Today the update worker (`src/app/integration_updates.rs`) polls the
crates.io API + git ls-remote every 6h and paints a `↑ Update
available` chip on the marketplace tab when an installed integration
binary has a newer version upstream. But the *installation* itself is
still a manual click — click the chip, wait for the shell to reappear,
paste `cargo install --locked --force <name>`, hit enter. Users who
maintain 10+ integrations end up either doing this ritual weekly or
just letting the chips accumulate.

## What auto-update should do

When the worker detects an update AND the user has opted in for that
integration, fire the install command in a background Pty and toast
the outcome. No modal, no confirm — the opt-in was the consent.

## Opt-in surface

Three levels, most specific wins:

1. **Per-integration override** in `~/.config/mnml/integrations/<id>.toml`:

   ```toml
   [integrations.mnml-forge-bitbucket]
   auto_update = true
   ```

   Setting on a specific integration overrides the global default.

2. **Global default** in `~/.config/mnml/config.toml`:

   ```toml
   [integrations]
   auto_update_cargo = false   # crates.io installs, default OFF
   auto_update_git   = false   # git installs — always OFF unless
                               # explicitly true, since git branches
                               # can move underneath you
   ```

3. **Shipped default**: `false` for everything. The chip stays visible;
   nothing auto-fires until the user opts in.

## Safety guardrails

- **Source-typed defaults.** The global toggle is split into
  `_cargo` and `_git` because they carry different risk profiles.
  Crates.io versions are semver-frozen and reviewed; git HEAD is
  whatever landed on `main` five minutes ago.
- **No skip-hooks / no --dangerous flags.** The install command is
  the same one the click path already runs: `cargo install --locked
  --force <name>`. Same permissions, same audit trail (crates2.json
  updates identically).
- **Rate cap.** At most one auto-install per integration per 24h.
  If a install fails, back off — don't retry until the next scheduled
  sweep.
- **Failure surfacing.** A non-zero exit from `cargo install` toasts
  the error + KEEPS the update chip visible so the user sees the
  problem. Success toasts + drops the chip on the next sweep.
- **Off by default** — no user gets auto-installed to without an
  explicit config change.
- **Per-integration disable is always honored** even when the global
  toggle is on.

## Wiring

- `Config::integrations.auto_update_cargo: bool` (default false)
- `Config::integrations.auto_update_git:   bool` (default false)
- `IntegrationManifestOverride::auto_update: Option<bool>` (already
  extensible via `[integrations.<id>]` — see #933).
- New helper `effective_auto_update(id, kind) -> bool` in
  `integration_updates.rs`, tests for the precedence chain.
- Worker: after a successful update-check sweep, iterate the results;
  for each with `is_update_available() && effective_auto_update()`,
  spawn `cargo install --locked --force <name>` in a Pty pane labelled
  `auto-update: <id>`. Track last-attempted time to enforce the 24h
  rate cap.
- Persist last-attempted timestamps in the same
  `integration-updates.json` cache the sweep already writes.

## UI

- **Settings overlay:** two toggle rows under an `── Integrations ──`
  section:
  - Auto-update from crates.io  ·  [off] / on
  - Auto-update from git         ·  [off] / on
- **Marketplace right-click menu:** per-integration item `Auto-update:
  ● / ○` — click toggles the per-integration override, persists to
  the manifest.
- **Toast on outcome:** `↑ auto-updated bitbucket 0.3.15 → 0.3.16`
  or `× auto-update failed for bitbucket: <trimmed cargo stderr>`.

## What's NOT in this design

- **Auto-uninstall** — never. A removed dep isn't the same as
  "user wants it gone".
- **Auto-downgrade** — never. `latest > current` is the only trigger.
- **Auto-restart mnml after a core update** — out of scope. Users may
  update `mnml-bridge` under their integrations; a core-mnml update is
  its own separate release channel.
- **Confirming semver-major bumps** — a stricter version-diff check
  is worth adding later, but v1 treats "current != latest" as
  "attempt install" and trusts cargo's `--locked` to prevent breaking
  changes from silently landing (lock file resolves once, doesn't
  cascade).

## Order of operations for a shipping PR

1. Land the config + override fields, no worker changes. Adds the two
   `[integrations] auto_update_*` keys + the per-manifest field. Tests
   for the resolver. Merge.
2. Land the worker branch that actually fires `cargo install`. Tests
   for the rate cap + failure-surface + skip-when-off. Merge.
3. Land the Settings row + marketplace right-click menu item. Merge.

Three small merges rather than one big one keeps blast radius small
per release, and each step is user-visible + reversible on its own.

## Open questions before implementation

- Where does the auto-install Pty pane live in the layout? Options:
  (a) tucked into a bottom dock so users can see progress without
  disrupting focus, (b) run headless (no visible pane) and only surface
  via toast. Leaning (b) — the whole point of auto-update is to be
  invisible when it works.
- Do we skip auto-update when the mnml AI-usage worker or other Rust-
  in-flight worker holds `.cargo/` locks? cargo will queue on the
  index lock naturally, but a stuck install would gum up the next
  sweep. Time out at 90s and log a warning if the install hasn't
  completed.