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:
@@ -1,12 +1,23 @@
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import { getOAuthProviders, type OAuthProvider } from "@oh-my-pi/pi-ai";
|
||||
import type { Component } from "@oh-my-pi/pi-tui";
|
||||
import { Input, Loader, Spacer, Text } from "@oh-my-pi/pi-tui";
|
||||
import { getAgentDbPath, getProjectDir } from "@oh-my-pi/pi-utils";
|
||||
import { getAgentDbPath, getConfigDirName, getProjectDir } from "@oh-my-pi/pi-utils";
|
||||
import { invalidate as invalidateFsCache } from "../../capability/fs";
|
||||
import { getRoleInfo } from "../../config/model-registry";
|
||||
import { settings } from "../../config/settings";
|
||||
import { DebugSelectorComponent } from "../../debug";
|
||||
import { disableProvider, enableProvider } from "../../discovery";
|
||||
import { clearClaudePluginRootsCache } from "../../discovery/helpers";
|
||||
import {
|
||||
getInstalledPluginsRegistryPath,
|
||||
getMarketplacesCacheDir,
|
||||
getMarketplacesRegistryPath,
|
||||
getPluginsCacheDir,
|
||||
MarketplaceManager,
|
||||
} from "../../extensibility/plugins/marketplace";
|
||||
import {
|
||||
getAvailableThemes,
|
||||
getSymbolTheme,
|
||||
@@ -27,6 +38,7 @@ import { ExtensionDashboard } from "../components/extensions";
|
||||
import { HistorySearchComponent } from "../components/history-search";
|
||||
import { ModelSelectorComponent } from "../components/model-selector";
|
||||
import { OAuthSelectorComponent } from "../components/oauth-selector";
|
||||
import { PluginSelectorComponent } from "../components/plugin-selector";
|
||||
import { SessionSelectorComponent } from "../components/session-selector";
|
||||
import { SettingsSelectorComponent } from "../components/settings-selector";
|
||||
import { ToolExecutionComponent } from "../components/tool-execution";
|
||||
@@ -414,6 +426,90 @@ export class SelectorController {
|
||||
});
|
||||
}
|
||||
|
||||
async showPluginSelector(mode: "install" | "uninstall" = "install"): Promise<void> {
|
||||
const mgr = new MarketplaceManager({
|
||||
marketplacesRegistryPath: getMarketplacesRegistryPath(),
|
||||
installedRegistryPath: getInstalledPluginsRegistryPath(),
|
||||
marketplacesCacheDir: getMarketplacesCacheDir(),
|
||||
pluginsCacheDir: getPluginsCacheDir(),
|
||||
clearPluginRootsCache: () => {
|
||||
const home = os.homedir();
|
||||
invalidateFsCache(path.join(home, ".claude", "plugins", "installed_plugins.json"));
|
||||
invalidateFsCache(path.join(home, getConfigDirName(), "plugins", "installed_plugins.json"));
|
||||
clearClaudePluginRootsCache();
|
||||
},
|
||||
});
|
||||
|
||||
const [marketplaces, installed] = await Promise.all([mgr.listMarketplaces(), mgr.listInstalledPlugins()]);
|
||||
const installedIds = new Set(installed.map(p => p.id));
|
||||
|
||||
if (mode === "uninstall") {
|
||||
// Show only installed plugins for uninstall
|
||||
const items = installed.map(p => {
|
||||
const entry = p.entries[0];
|
||||
const atIdx = p.id.lastIndexOf("@");
|
||||
const pluginName = atIdx > 0 ? p.id.slice(0, atIdx) : p.id;
|
||||
const mkt = atIdx > 0 ? p.id.slice(atIdx + 1) : "unknown";
|
||||
return {
|
||||
plugin: { name: pluginName, version: entry?.version, description: undefined as string | undefined },
|
||||
marketplace: mkt,
|
||||
};
|
||||
});
|
||||
this.showSelector(done => {
|
||||
const selector = new PluginSelectorComponent(0, items, new Set(), {
|
||||
onSelect: async (name, marketplace) => {
|
||||
done();
|
||||
const pluginId = `${name}@${marketplace}`;
|
||||
this.ctx.showStatus(`Uninstalling ${pluginId}...`);
|
||||
this.ctx.ui.requestRender();
|
||||
try {
|
||||
await mgr.uninstallPlugin(pluginId);
|
||||
this.ctx.showStatus(`Uninstalled ${pluginId}`);
|
||||
} catch (err) {
|
||||
this.ctx.showStatus(`Uninstall failed: ${err}`);
|
||||
}
|
||||
this.ctx.ui.requestRender();
|
||||
},
|
||||
onCancel: () => done(),
|
||||
});
|
||||
return { component: selector, focus: selector.getSelectList() };
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Install mode: show all available plugins from all marketplaces
|
||||
const allPlugins: Array<{
|
||||
plugin: { name: string; version?: string; description?: string };
|
||||
marketplace: string;
|
||||
}> = [];
|
||||
for (const mkt of marketplaces) {
|
||||
const plugins = await mgr.listAvailablePlugins(mkt.name);
|
||||
for (const plugin of plugins) {
|
||||
allPlugins.push({ plugin, marketplace: mkt.name });
|
||||
}
|
||||
}
|
||||
|
||||
this.showSelector(done => {
|
||||
const selector = new PluginSelectorComponent(marketplaces.length, allPlugins, installedIds, {
|
||||
onSelect: async (name, marketplace) => {
|
||||
done();
|
||||
this.ctx.showStatus(`Installing ${name} from ${marketplace}...`);
|
||||
this.ctx.ui.requestRender();
|
||||
try {
|
||||
const force = installedIds.has(`${name}@${marketplace}`);
|
||||
await mgr.installPlugin(name, marketplace, { force });
|
||||
this.ctx.showStatus(`Installed ${name} from ${marketplace}`);
|
||||
} catch (err) {
|
||||
this.ctx.showStatus(`Install failed: ${err}`);
|
||||
}
|
||||
this.ctx.ui.requestRender();
|
||||
},
|
||||
onCancel: () => done(),
|
||||
});
|
||||
return { component: selector, focus: selector.getSelectList() };
|
||||
});
|
||||
}
|
||||
|
||||
showUserMessageSelector(): void {
|
||||
const userMessages = this.ctx.session.getUserMessagesForBranching();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user