feat: add marketplace plugin system

Add Claude Code-compatible marketplace plugin infrastructure:

- Registry: types, ID helpers, atomic read/write for marketplaces.json
  and installed_plugins.json (Claude Code format with version: 2)
- Fetcher: classifySource (6 ordered rules), parseMarketplaceCatalog,
  fetchMarketplace (local sources; git/http stubs for Phase 2)
- Resolver + Cache: resolvePluginSource with pathIsWithin containment,
  cachePlugin, removeCachedPlugin, cleanOrphanedCache
- MarketplaceManager: orchestrates add/remove/update marketplaces,
  install/uninstall/enable plugins, clearPluginRootsCache on mutation
- CLI: omp plugin marketplace add|remove|update|list,
  omp plugin discover, classifyInstallTarget for name@marketplace
- Discovery: listClaudePluginRoots reads OMP registry alongside
  Claude's, OMP authoritative for duplicate plugin IDs
- Args: --plugin-dir repeatable flag (parsing only, runtime wiring TBD)
- Slash command: /reload-plugins clears fs + roots cache
- Test fixtures and 130 tests across 8 test files
This commit is contained in:
Miroslav Drbal
2026-03-28 13:36:11 +01:00
committed by can1357
parent bc2b99f9b0
commit 4e199f93f7
48 changed files with 4239 additions and 96 deletions
+137 -1
View File
@@ -7,6 +7,13 @@
import { APP_NAME } from "@oh-my-pi/pi-utils";
import chalk from "chalk";
import { PluginManager, parseSettingValue, validateSetting } from "../extensibility/plugins";
import {
getInstalledPluginsRegistryPath,
getMarketplacesCacheDir,
getMarketplacesRegistryPath,
getPluginsCacheDir,
MarketplaceManager,
} from "../extensibility/plugins/marketplace/index.js";
import { theme } from "../modes/theme/theme";
// =============================================================================
@@ -22,7 +29,9 @@ export type PluginAction =
| "features"
| "config"
| "enable"
| "disable";
| "disable"
| "marketplace"
| "discover";
export interface PluginCommandArgs {
action: PluginAction;
@@ -53,6 +62,8 @@ const VALID_ACTIONS: PluginAction[] = [
"config",
"enable",
"disable",
"marketplace",
"discover",
];
/**
@@ -108,6 +119,8 @@ export function parsePluginArgs(args: string[]): PluginCommandArgs | undefined {
return result;
}
export { classifyInstallTarget } from "./classify-install-target.js";
// =============================================================================
// Command Handlers
// =============================================================================
@@ -146,6 +159,127 @@ export async function runPluginCommand(cmd: PluginCommandArgs): Promise<void> {
case "disable":
await handleDisable(manager, cmd.args, cmd.flags);
break;
case "marketplace":
await handleMarketplace(cmd.args, cmd.flags);
break;
case "discover":
await handleDiscover(cmd.args, cmd.flags);
break;
}
}
// =============================================================================
// Marketplace Handlers
// =============================================================================
function makeMarketplaceManager(): MarketplaceManager {
return new MarketplaceManager({
marketplacesRegistryPath: getMarketplacesRegistryPath(),
installedRegistryPath: getInstalledPluginsRegistryPath(),
marketplacesCacheDir: getMarketplacesCacheDir(),
pluginsCacheDir: getPluginsCacheDir(),
});
}
async function handleMarketplace(args: string[], _flags: PluginCommandArgs["flags"]): Promise<void> {
const subcommand = args[0] ?? "list";
const manager = makeMarketplaceManager();
switch (subcommand) {
case "add": {
const source = args[1];
if (!source) {
console.error(chalk.red(`Usage: ${APP_NAME} plugin marketplace add <source>`));
process.exit(1);
}
try {
await manager.addMarketplace(source);
console.log(chalk.green(`${theme.status.success} Added marketplace: ${source}`));
} catch (err) {
console.error(chalk.red(`${theme.status.error} Failed to add marketplace: ${err}`));
process.exit(1);
}
break;
}
case "remove":
case "rm": {
const name = args[1];
if (!name) {
console.error(chalk.red(`Usage: ${APP_NAME} plugin marketplace remove <name>`));
process.exit(1);
}
try {
await manager.removeMarketplace(name);
console.log(chalk.green(`${theme.status.success} Removed marketplace: ${name}`));
} catch (err) {
console.error(chalk.red(`${theme.status.error} Failed to remove marketplace: ${err}`));
process.exit(1);
}
break;
}
case "update": {
try {
const name = args[1];
if (name) {
await manager.updateMarketplace(name);
console.log(chalk.green(`${theme.status.success} Updated marketplace: ${name}`));
} else {
const results = await manager.updateAllMarketplaces();
console.log(chalk.green(`${theme.status.success} Updated ${results.length} marketplace(s)`));
}
} catch (err) {
console.error(chalk.red(`${theme.status.error} Failed to update marketplace: ${err}`));
process.exit(1);
}
break;
}
default: {
if (subcommand !== "list") {
console.error(chalk.red(`Unknown marketplace subcommand: ${subcommand}`));
console.error(chalk.dim("Valid subcommands: add, remove, update, list"));
process.exit(1);
}
try {
const marketplaces = await manager.listMarketplaces();
if (marketplaces.length === 0) {
console.log(chalk.dim("No marketplaces configured"));
console.log(chalk.dim(`\nAdd one with: ${APP_NAME} plugin marketplace add <source>`));
return;
}
console.log(chalk.bold("Configured Marketplaces:\n"));
for (const mp of marketplaces) {
console.log(` ${chalk.cyan(mp.name)} ${chalk.dim(mp.sourceUri)}`);
}
} catch (err) {
console.error(chalk.red(`${theme.status.error} Failed to list marketplaces: ${err}`));
process.exit(1);
}
break;
}
}
}
async function handleDiscover(args: string[], _flags: PluginCommandArgs["flags"]): Promise<void> {
const marketplace = args[0];
const manager = makeMarketplaceManager();
try {
const plugins = await manager.listAvailablePlugins(marketplace);
if (plugins.length === 0) {
console.log(chalk.dim(marketplace ? `No plugins found in ${marketplace}` : "No plugins available"));
return;
}
console.log(chalk.bold(`Available Plugins${marketplace ? ` (${marketplace})` : ""}:\n`));
for (const plugin of plugins) {
console.log(` ${chalk.cyan(plugin.name)}${plugin.version ? `@${plugin.version}` : ""}`);
if (plugin.description) {
console.log(chalk.dim(` ${plugin.description}`));
}
}
} catch (err) {
console.error(chalk.red(`${theme.status.error} Failed to discover plugins: ${err}`));
process.exit(1);
}
}
@@ -630,6 +764,8 @@ ${chalk.bold("Commands:")}
config <cmd> <pkg> [key] [val] Manage plugin settings
enable <pkg> Enable a disabled plugin
disable <pkg> Disable plugin without uninstalling
marketplace <cmd> Manage marketplace sources (add, remove, update, list)
discover [marketplace] Browse available marketplace plugins
${chalk.bold("Feature Syntax:")}
pkg Install with default features