Files
oh-my-pi/packages/coding-agent/src/modes/components/plugin-selector.ts
T
Miroslav Drbalandcan1357 4e199f93f7 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
2026-03-30 13:44:53 +02:00

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)];
}