Plugins: add Claude marketplace registry installs (#48058)

* Changelog: note Claude marketplace plugin support

* Plugins: add Claude marketplace installs

* E2E: cover marketplace plugin installs in Docker
This commit is contained in:
Vincent Koc
2026-03-16 01:46:07 -07:00
committed by GitHub
parent 9ee0fb52e9
commit ff2e864c98
14 changed files with 1641 additions and 142 deletions

View File

@@ -11,6 +11,11 @@ import { enablePluginInConfig } from "../plugins/enable.js";
import { installPluginFromNpmSpec, installPluginFromPath } from "../plugins/install.js";
import { recordPluginInstall } from "../plugins/installs.js";
import { clearPluginManifestRegistryCache } from "../plugins/manifest-registry.js";
import {
installPluginFromMarketplace,
listMarketplacePlugins,
resolveMarketplaceInstallShortcut,
} from "../plugins/marketplace.js";
import type { PluginRecord } from "../plugins/registry.js";
import { applyExclusiveSlotSelection } from "../plugins/slots.js";
import { resolvePluginSourceRoots, formatPluginSourceForTable } from "../plugins/source-display.js";
@@ -46,6 +51,10 @@ export type PluginUpdateOptions = {
dryRun?: boolean;
};
export type PluginMarketplaceListOptions = {
json?: boolean;
};
export type PluginUninstallOptions = {
keepFiles?: boolean;
keepConfig?: boolean;
@@ -203,9 +212,65 @@ async function installBundledPluginSource(params: {
async function runPluginInstallCommand(params: {
raw: string;
opts: { link?: boolean; pin?: boolean };
opts: { link?: boolean; pin?: boolean; marketplace?: string };
}) {
const { raw, opts } = params;
const shorthand = !params.opts.marketplace
? await resolveMarketplaceInstallShortcut(params.raw)
: null;
if (shorthand?.ok === false) {
defaultRuntime.error(shorthand.error);
process.exit(1);
}
const raw = shorthand?.ok ? shorthand.plugin : params.raw;
const opts = {
...params.opts,
marketplace:
params.opts.marketplace ?? (shorthand?.ok ? shorthand.marketplaceSource : undefined),
};
if (opts.marketplace) {
if (opts.link) {
defaultRuntime.error("`--link` is not supported with `--marketplace`.");
process.exit(1);
}
if (opts.pin) {
defaultRuntime.error("`--pin` is not supported with `--marketplace`.");
process.exit(1);
}
const cfg = loadConfig();
const result = await installPluginFromMarketplace({
marketplace: opts.marketplace,
plugin: raw,
logger: createPluginInstallLogger(),
});
if (!result.ok) {
defaultRuntime.error(result.error);
process.exit(1);
}
clearPluginManifestRegistryCache();
let next = enablePluginInConfig(cfg, result.pluginId).config;
next = recordPluginInstall(next, {
pluginId: result.pluginId,
source: "marketplace",
installPath: result.targetDir,
version: result.version,
marketplaceName: result.marketplaceName,
marketplaceSource: result.marketplaceSource,
marketplacePlugin: result.marketplacePlugin,
});
const slotResult = applySlotSelectionForPlugin(next, result.pluginId);
next = slotResult.config;
await writeConfigFile(next);
logSlotWarnings(slotResult.warnings);
defaultRuntime.log(`Installed plugin: ${result.pluginId}`);
defaultRuntime.log(`Restart the gateway to load plugins.`);
return;
}
const fileSpec = resolveFileNpmSpecToLocalPath(raw);
if (fileSpec && !fileSpec.ok) {
defaultRuntime.error(fileSpec.error);
@@ -734,17 +799,24 @@ export function registerPluginsCli(program: Command) {
plugins
.command("install")
.description("Install a plugin (path, archive, or npm spec)")
.argument("<path-or-spec>", "Path (.ts/.js/.zip/.tgz/.tar.gz) or an npm package spec")
.description("Install a plugin (path, archive, npm spec, or marketplace entry)")
.argument(
"<path-or-spec-or-plugin>",
"Path (.ts/.js/.zip/.tgz/.tar.gz), npm package spec, or marketplace plugin name",
)
.option("-l, --link", "Link a local path instead of copying", false)
.option("--pin", "Record npm installs as exact resolved <name>@<version>", false)
.action(async (raw: string, opts: { link?: boolean; pin?: boolean }) => {
.option(
"--marketplace <source>",
"Install a Claude marketplace plugin from a local repo/path or git/GitHub source",
)
.action(async (raw: string, opts: { link?: boolean; pin?: boolean; marketplace?: string }) => {
await runPluginInstallCommand({ raw, opts });
});
plugins
.command("update")
.description("Update installed plugins (npm installs only)")
.description("Update installed plugins (npm and marketplace installs)")
.argument("[id]", "Plugin id (omit with --all)")
.option("--all", "Update all tracked plugins", false)
.option("--dry-run", "Show what would change without writing", false)
@@ -755,7 +827,7 @@ export function registerPluginsCli(program: Command) {
if (targets.length === 0) {
if (opts.all) {
defaultRuntime.log("No npm-installed plugins to update.");
defaultRuntime.log("No tracked plugins to update.");
return;
}
defaultRuntime.error("Provide a plugin id or use --all.");
@@ -839,4 +911,54 @@ export function registerPluginsCli(program: Command) {
lines.push(`${theme.muted("Docs:")} ${docs}`);
defaultRuntime.log(lines.join("\n"));
});
const marketplace = plugins
.command("marketplace")
.description("Inspect Claude-compatible plugin marketplaces");
marketplace
.command("list")
.description("List plugins published by a marketplace source")
.argument("<source>", "Local marketplace path/repo or git/GitHub source")
.option("--json", "Print JSON")
.action(async (source: string, opts: PluginMarketplaceListOptions) => {
const result = await listMarketplacePlugins({
marketplace: source,
logger: createPluginInstallLogger(),
});
if (!result.ok) {
defaultRuntime.error(result.error);
process.exit(1);
}
if (opts.json) {
defaultRuntime.log(
JSON.stringify(
{
source: result.sourceLabel,
name: result.manifest.name,
version: result.manifest.version,
plugins: result.manifest.plugins,
},
null,
2,
),
);
return;
}
if (result.manifest.plugins.length === 0) {
defaultRuntime.log(`No plugins found in marketplace ${result.sourceLabel}.`);
return;
}
defaultRuntime.log(
`${theme.heading("Marketplace")} ${theme.muted(result.manifest.name ?? result.sourceLabel)}`,
);
for (const plugin of result.manifest.plugins) {
const suffix = plugin.version ? theme.muted(` v${plugin.version}`) : "";
const desc = plugin.description ? ` - ${theme.muted(plugin.description)}` : "";
defaultRuntime.log(`${theme.command(plugin.name)}${suffix}${desc}`);
}
});
}