| import type { |
| MarketplacePlugin, |
| LocalPlugin, |
| PluginBundledSkill, |
| } from "#/api/plugins-service"; |
| import type { InstalledPluginInfo } from "#/api/plugins-management-service"; |
|
|
| export type PluginStatusFilter = "all" | "installed" | "available" | "local"; |
|
|
| |
| |
| |
| |
| export interface PluginViewModel { |
| name: string; |
| description: string | null; |
| source: string | null; |
| ref: string | null; |
| repoPath: string | null; |
| |
| installed: boolean; |
| |
| enabled: boolean; |
| |
| version: string | null; |
| |
| inCatalog: boolean; |
| |
| |
| |
| |
| isLocal: boolean; |
| |
| |
| |
| |
| |
| |
| path: string | null; |
| skills: PluginBundledSkill[] | null; |
| files: string[] | null; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function buildPluginsViewModel( |
| marketplace: MarketplacePlugin[] | undefined, |
| installed: InstalledPluginInfo[] | undefined, |
| local: LocalPlugin[] | undefined = undefined, |
| ): PluginViewModel[] { |
| const byName = new Map<string, PluginViewModel>(); |
|
|
| for (const entry of marketplace ?? []) { |
| byName.set(entry.name, { |
| name: entry.name, |
| description: entry.description, |
| source: entry.source, |
| ref: entry.ref ?? null, |
| repoPath: entry.repo_path ?? null, |
| installed: entry.installed, |
| enabled: false, |
| version: null, |
| inCatalog: true, |
| isLocal: false, |
| path: entry.path ?? null, |
| skills: entry.skills ?? null, |
| files: entry.files ?? null, |
| }); |
| } |
|
|
| for (const entry of installed ?? []) { |
| const existing = byName.get(entry.name); |
| |
| |
| const hasContents = entry.skills != null || entry.files != null; |
| byName.set(entry.name, { |
| name: entry.name, |
| description: existing?.description ?? entry.description ?? null, |
| source: entry.source ?? existing?.source ?? null, |
| ref: entry.resolved_ref ?? existing?.ref ?? null, |
| repoPath: entry.repo_path ?? existing?.repoPath ?? null, |
| installed: true, |
| enabled: entry.enabled, |
| version: entry.version ?? null, |
| inCatalog: existing?.inCatalog ?? false, |
| isLocal: false, |
| path: hasContents ? entry.install_path : (existing?.path ?? null), |
| skills: hasContents ? (entry.skills ?? null) : (existing?.skills ?? null), |
| files: hasContents ? (entry.files ?? null) : (existing?.files ?? null), |
| }); |
| } |
|
|
| |
| |
| |
| |
| for (const entry of local ?? []) { |
| if (byName.has(entry.name)) continue; |
| byName.set(entry.name, { |
| name: entry.name, |
| description: entry.description || null, |
| source: null, |
| ref: null, |
| repoPath: null, |
| installed: false, |
| enabled: false, |
| version: entry.version || null, |
| inCatalog: false, |
| isLocal: true, |
| path: entry.path ?? null, |
| skills: entry.skills ?? null, |
| files: entry.files ?? null, |
| }); |
| } |
|
|
| |
| const rank = (plugin: PluginViewModel): number => |
| plugin.installed ? 0 : plugin.isLocal ? 1 : 2; |
|
|
| return Array.from(byName.values()).sort((a, b) => { |
| const rankDelta = rank(a) - rank(b); |
| if (rankDelta !== 0) return rankDelta; |
| return a.name.localeCompare(b.name); |
| }); |
| } |
|
|
| |
| export function matchesPluginSearch( |
| plugin: PluginViewModel, |
| query: string, |
| ): boolean { |
| const trimmed = query.trim().toLowerCase(); |
| if (!trimmed) return true; |
| const haystacks = [ |
| plugin.name, |
| plugin.description ?? "", |
| plugin.source ?? "", |
| plugin.repoPath ?? "", |
| plugin.ref ?? "", |
| ]; |
| return haystacks.some((value) => value.toLowerCase().includes(trimmed)); |
| } |
|
|
| |
| export function matchesPluginStatus( |
| plugin: PluginViewModel, |
| filter: PluginStatusFilter, |
| ): boolean { |
| if (filter === "installed") return plugin.installed; |
| |
| |
| |
| if (filter === "available") return !plugin.installed && !plugin.isLocal; |
| if (filter === "local") return plugin.isLocal; |
| return true; |
| } |
|
|