c7f4f597bd
When a plugin is installed in both user and project scope, the selector now shows separate scoped entries (e.g. 'plugin-name@1.0 [project]' and 'plugin-name@1.0 [user]') and forwards the selected scope to uninstallPlugin(), preventing the disambiguation error introduced when dual-scope support was added.
96 lines
3.0 KiB
TypeScript
96 lines
3.0 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, scope?: "user" | "project") => void;
|
|
onCancel: () => void;
|
|
}
|
|
|
|
export interface PluginItem {
|
|
plugin: { name: string; version?: string; description?: string };
|
|
marketplace: string;
|
|
/** Scope of this entry. When set, appended to the label and forwarded to onSelect. */
|
|
scope?: "user" | "project";
|
|
}
|
|
|
|
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, scope }) => {
|
|
// Encode scope into the value so onSelect can recover it without a parallel Map.
|
|
// Format: "name@marketplace" or "name@marketplace#scope"
|
|
const id = scope ? `${plugin.name}@${marketplace}#${scope}` : `${plugin.name}@${marketplace}`;
|
|
const installed = installedIds.has(`${plugin.name}@${marketplace}`);
|
|
const version = plugin.version ? `@${plugin.version}` : "";
|
|
const status = installed ? " [installed]" : "";
|
|
const scopeTag = scope ? ` [${scope}]` : "";
|
|
|
|
return {
|
|
value: id,
|
|
label: `${plugin.name}${version}${status}${scopeTag}`,
|
|
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, scope] = splitPluginId(item.value);
|
|
if (name && marketplace) {
|
|
callbacks.onSelect(name, marketplace, scope);
|
|
}
|
|
};
|
|
|
|
this.#selectList.onCancel = () => {
|
|
callbacks.onCancel();
|
|
};
|
|
|
|
this.addChild(this.#selectList);
|
|
this.addChild(new DynamicBorder());
|
|
}
|
|
|
|
getSelectList(): SelectList {
|
|
return this.#selectList;
|
|
}
|
|
}
|
|
|
|
function splitPluginId(id: string): [string, string, "user" | "project" | undefined] | [null, null, null] {
|
|
// value format: "name@marketplace" or "name@marketplace#scope"
|
|
const hashIdx = id.indexOf("#");
|
|
const base = hashIdx >= 0 ? id.slice(0, hashIdx) : id;
|
|
const scope = hashIdx >= 0 ? (id.slice(hashIdx + 1) as "user" | "project") : undefined;
|
|
const atIdx = base.lastIndexOf("@");
|
|
if (atIdx <= 0) return [null, null, null];
|
|
return [base.slice(0, atIdx), base.slice(atIdx + 1), scope];
|
|
}
|