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
87 lines
2.3 KiB
TypeScript
87 lines
2.3 KiB
TypeScript
/**
|
|
* Interactive marketplace plugin selector.
|
|
*
|
|
* Shows available plugins from all configured marketplaces in a SelectList.
|
|
* Selecting a plugin triggers installation. Esc cancels.
|
|
*/
|
|
import { Container, type SelectItem, SelectList } from "@oh-my-pi/pi-tui";
|
|
import { getSelectListTheme } from "../theme/theme";
|
|
import { DynamicBorder } from "./dynamic-border";
|
|
|
|
export interface PluginSelectorCallbacks {
|
|
onSelect: (pluginName: string, marketplace: string) => void;
|
|
onCancel: () => void;
|
|
}
|
|
|
|
export interface PluginItem {
|
|
plugin: { name: string; version?: string; description?: string };
|
|
marketplace: string;
|
|
}
|
|
|
|
export class PluginSelectorComponent extends Container {
|
|
#selectList: SelectList;
|
|
|
|
constructor(
|
|
marketplaceCount: number,
|
|
plugins: PluginItem[],
|
|
installedIds: Set<string>,
|
|
callbacks: PluginSelectorCallbacks,
|
|
) {
|
|
super();
|
|
|
|
const items: SelectItem[] = plugins.map(({ plugin, marketplace }) => {
|
|
const id = `${plugin.name}@${marketplace}`;
|
|
const installed = installedIds.has(id);
|
|
const version = plugin.version ? `@${plugin.version}` : "";
|
|
const status = installed ? " [installed]" : "";
|
|
|
|
return {
|
|
value: id,
|
|
label: `${plugin.name}${version}${status}`,
|
|
description: plugin.description,
|
|
hint: marketplace,
|
|
};
|
|
});
|
|
|
|
if (items.length === 0) {
|
|
items.push({
|
|
value: "__empty__",
|
|
label: "No plugins available",
|
|
description:
|
|
marketplaceCount === 0
|
|
? "Add a marketplace first: /marketplace add <source>"
|
|
: "Configured marketplaces have no plugins",
|
|
});
|
|
}
|
|
|
|
this.addChild(new DynamicBorder());
|
|
|
|
this.#selectList = new SelectList(items, Math.min(items.length, 20), getSelectListTheme());
|
|
|
|
this.#selectList.onSelect = item => {
|
|
if (item.value === "__empty__") return;
|
|
const [name, marketplace] = splitPluginId(item.value);
|
|
if (name && marketplace) {
|
|
callbacks.onSelect(name, marketplace);
|
|
}
|
|
};
|
|
|
|
this.#selectList.onCancel = () => {
|
|
callbacks.onCancel();
|
|
};
|
|
|
|
this.addChild(this.#selectList);
|
|
this.addChild(new DynamicBorder());
|
|
}
|
|
|
|
getSelectList(): SelectList {
|
|
return this.#selectList;
|
|
}
|
|
}
|
|
|
|
function splitPluginId(id: string): [string, string] | [null, null] {
|
|
const atIdx = id.lastIndexOf("@");
|
|
if (atIdx <= 0) return [null, null];
|
|
return [id.slice(0, atIdx), id.slice(atIdx + 1)];
|
|
}
|