Marketplace: review a plugin's catalog entry and confirm what it runs (PR review #3, #4)

A plugin's update diff now includes its marketplace.json entry as a
pretty-printed "marketplace.json entry" file, so inline hooks, MCP servers
and commands are reviewed like any file. CatalogItem gains
plugin_components (entry / plugin.json runnable keys, hooks/hooks.json,
.mcp.json, commands/), and installing a plugin now goes through
PluginConfirmModal listing them. The import-preview warnings describe
that confirmation accurately.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-27 13:08:15 -07:00
co-authored by Claude Opus 5.5
parent d51b54774b
commit da65d51f09
11 changed files with 365 additions and 38 deletions
@@ -5,6 +5,7 @@ import type { MarketplaceApi } from "../../hooks/useMarketplace";
import type { CatalogItem, InstallScope, MarketplaceItemRef } from "../../lib/types";
import Toggle from "../ui/Toggle";
import HookConfirmModal from "./HookConfirmModal";
import PluginConfirmModal from "./PluginConfirmModal";
const STATE_LABEL: Record<ProjectItemState, string> = {
none: "",
@@ -25,8 +26,8 @@ interface Props {
headCommit: string | null;
}
/** A hook install waiting for confirmation, frozen at the moment it was asked for. */
interface PendingHook {
/** A hook or plugin install waiting for confirmation, frozen at the moment it was asked for. */
interface PendingConfirm {
scope: InstallScope;
item: CatalogItem;
commit: string;
@@ -36,7 +37,7 @@ export default function InstallControls({ mp, item, marketplaceId, headCommit }:
const appSettings = useAppState((s) => s.appSettings);
const projects = useAppState((s) => s.projects);
const filterId = useAppState((s) => s.marketplaceFilterProjectId);
const [pendingHook, setPendingHook] = useState<PendingHook | null>(null);
const [pending, setPending] = useState<PendingConfirm | null>(null);
const [busy, setBusy] = useState(false);
const ref: MarketplaceItemRef = { marketplace_id: marketplaceId, kind: item.kind, key: item.key };
@@ -58,10 +59,10 @@ export default function InstallControls({ mp, item, marketplaceId, headCommit }:
}
};
/** Every install goes through here so a hook is always confirmed first. */
/** Every install goes through here so a hook or plugin is always confirmed first. */
const install = (scope: InstallScope) => {
if (item.kind === "hook") {
setPendingHook({ scope, item, commit });
if (item.kind === "hook" || item.kind === "plugin") {
setPending({ scope, item, commit });
return;
}
void run(() => mp.install(ref, scope, commit));
@@ -123,18 +124,23 @@ export default function InstallControls({ mp, item, marketplaceId, headCommit }:
{projects.length === 0 && (
<p className="text-xs text-[var(--text-secondary)]">No projects yet — “All projects” also covers projects added later.</p>
)}
{pendingHook && (
<HookConfirmModal
item={pendingHook.item}
commit={pendingHook.commit}
onCancel={() => setPendingHook(null)}
onConfirm={() => {
const { scope, commit: reviewed } = pendingHook;
setPendingHook(null);
{pending &&
(() => {
const confirm = () => {
const { scope, commit: reviewed } = pending;
setPending(null);
void run(() => mp.install(ref, scope, reviewed));
}}
/>
)}
};
const Confirm = pending.item.kind === "plugin" ? PluginConfirmModal : HookConfirmModal;
return (
<Confirm
item={pending.item}
commit={pending.commit}
onCancel={() => setPending(null)}
onConfirm={confirm}
/>
);
})()}
</div>
);
}