compute_updates reads each marketplace's head once (snapshot_head, no
snapshot clone) and opens each cache once, sharing trees across installs
through GitTree::at. refresh_marketplaces(Some(id)) re-pins only that
marketplace (mk::set_pins with only) and returns only its snapshot, which
the frontend merges by id.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The single global repo lock becomes one lock per marketplace, and
refresh_pins takes each marketplace's lock only while setting its pins, so
a slow fetch no longer queues installs and refreshes of other marketplaces.
refresh_marketplace now takes the lock first and reads the settings store
under it (a closure, not a copy captured earlier); a marketplace removed
meanwhile gets no cache and no snapshot. Removing a marketplace deletes its
snapshot and cache under the same lock (remove_marketplace_cache).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ProjectsStore gains update_marketplace_fields / update_all_marketplace_fields,
which read-modify-write installs and opt-outs under the store's own lock.
Install, uninstall, update, forget and set_global_item_disabled use them
instead of writing back a whole Project read earlier, so a concurrent
start's status/container_id change is no longer overwritten.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Install and update now share ops::installable_at_head: the reviewed head
must still be the head and the item's catalog entry there must be valid.
The old update check (item_files) never parsed hook.json, so an upstream
hook with an unknown event could be pinned and then held by every sync.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Install and update pinned whatever the marketplace head was when the
click landed, so a background refresh between review and click could
pin content nobody saw (including a hook's shell commands).
install_marketplace_item and update_marketplace_item now take
expected_commit and refuse with "changed since you reviewed this item —
review it again" unless it is still the head. The UI passes the head the
selected item was read at (Browse), the head frozen with a pending hook
confirm (whose commands are frozen too), and the head of the accepted
diff (Installed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The import preview counts global plugin installs and warns on them:
a plugin can bring hooks and MCP servers into every container and an
imported install skips the confirm step, like a hook.
- Item keys, hosts and branches in errors are quoted with {:?} and
capped, since they can come from an import file.
- After an import, caches and snapshots of marketplaces the import
dropped are removed (under the repo lock) and pins are refreshed for
the imported installs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 21 marketplace commands, registered and granted; marketplace fields
kept store-owned in update_settings/update_project; a background refresh
of every marketplace at app start.
- apply_marketplace_now emits marketplace-sync-finished per project (F4).
- Settings export carries marketplace account tokens in ExportedSecrets
(account id -> token) and import restores them; imported accounts,
marketplaces and global installs are validated with the commands' own
rules before anything is written. The import preview discloses the
marketplace count, token count and global hook installs, and warns on
the latter (F10).
- refresh_pins and cache removal hold the repo lock (F11).
- ops::validate_host/validate_branch delegate to auth::valid_host and
git::valid_branch (F13).
- A finished gh container login frees only its own cancel slot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>