/** * Capability Registry * * Central registry for capabilities and providers. Provides the main API for: * - Defining capabilities (what we're looking for) * - Registering providers (where to find it) * - Loading items for a capability across all providers */ import * as os from "node:os"; import * as path from "node:path"; import { getProjectDir, logger } from "@oh-my-pi/pi-utils"; import type { Settings } from "../config/settings"; import { clearCache as clearFsCache, findRepoRoot, cacheStats as fsCacheStats, invalidate as invalidateFs } from "./fs"; import type { Capability, CapabilityInfo, CapabilityResult, LoadContext, LoadOptions, Provider, ProviderInfo, SourceMeta, } from "./types"; // ============================================================================= // Registry State // ============================================================================= /** Registry of all capabilities */ const capabilities = new Map>(); /** Reverse index: provider ID -> capability IDs it's registered for */ const providerCapabilities = new Map>(); /** Provider display metadata (shared across capabilities) */ const providerMeta = new Map(); /** Disabled providers (by ID) */ const disabledProviders = new Set(); /** Settings manager for persistence (if set) */ let settings: Settings | null = null; // ============================================================================= // Registration API // ============================================================================= /** * Define a new capability. */ export function defineCapability(def: Omit, "providers">): Capability { if (capabilities.has(def.id)) { throw new Error(`Capability "${def.id}" is already defined`); } const capability: Capability = { ...def, providers: [] }; capabilities.set(def.id, capability as Capability); return capability; } /** * Register a provider for a capability. */ export function registerProvider(capabilityId: string, provider: Provider): void { const capability = capabilities.get(capabilityId); if (!capability) { throw new Error(`Unknown capability: "${capabilityId}". Define it first with defineCapability().`); } // Store provider metadata (for cross-capability display) if (!providerMeta.has(provider.id)) { providerMeta.set(provider.id, { displayName: provider.displayName, description: provider.description, }); } // Track which capabilities this provider is registered for if (!providerCapabilities.has(provider.id)) { providerCapabilities.set(provider.id, new Set()); } providerCapabilities.get(provider.id)!.add(capabilityId); // Insert in priority order (highest first) const providers = capability.providers as Provider[]; const idx = providers.findIndex(p => p.priority < provider.priority); if (idx === -1) { providers.push(provider); } else { providers.splice(idx, 0, provider); } } // ============================================================================= // Loading API // ============================================================================= /** * Async loading logic shared by loadCapability(). */ async function loadImpl( capability: Capability, providers: Provider[], ctx: LoadContext, options: LoadOptions, ): Promise> { const allItems: Array = []; const allWarnings: string[] = []; const contributingProviders: string[] = []; const disabledExtensionIds = options.includeDisabled ? new Set() : new Set(options.disabledExtensions ?? settings?.get("disabledExtensions") ?? []); const results = await Promise.all( providers.map(async provider => { try { const result = await logger.time( `capability:${capability.id}:${provider.id}`, provider.load.bind(provider), ctx, ); return { provider, result }; } catch (error) { logger.debug(`capability:${capability.id}:${provider.id}:error`); return { provider, error }; } }), ); for (const entry of results) { const { provider } = entry; if ("error" in entry) { allWarnings.push(`[${provider.displayName}] Failed to load: ${entry.error}`); continue; } const result = entry.result; if (!result) continue; if (result.warnings) { allWarnings.push(...result.warnings.map(w => `[${provider.displayName}] ${w}`)); } let contributedItemCount = 0; for (const item of result.items) { const itemWithSource = item as T & { _source: SourceMeta }; if (!itemWithSource._source) { allWarnings.push(`[${provider.displayName}] Item missing _source metadata, skipping`); continue; } const extensionId = capability.toExtensionId?.(itemWithSource); if (extensionId && disabledExtensionIds.has(extensionId)) { continue; } itemWithSource._source.providerName = provider.displayName; allItems.push(itemWithSource as T & { _source: SourceMeta; _shadowed?: boolean }); contributedItemCount += 1; } if (contributedItemCount > 0) { contributingProviders.push(provider.id); } } // Deduplicate by key (first wins = highest priority) const seen = new Map(); const deduped: Array = []; for (let i = 0; i < allItems.length; i++) { const item = allItems[i]; const key = capability.key(item); if (key === undefined) { deduped.push(item); } else if (!seen.has(key)) { seen.set(key, i); deduped.push(item); } else { item._shadowed = true; } } // Validate items (only non-shadowed items) if (capability.validate && !options.includeInvalid) { for (let i = deduped.length - 1; i >= 0; i--) { const error = capability.validate(deduped[i]); if (error) { const source = deduped[i]._source; allWarnings.push( `[${source?.providerName ?? "unknown"}] Invalid item at ${source?.path ?? "unknown"}: ${error}`, ); deduped.splice(i, 1); } } } return { items: deduped, all: allItems, warnings: allWarnings, providers: contributingProviders, }; } /** * Filter providers based on options and disabled state. */ function filterProviders(capability: Capability, options: LoadOptions): Provider[] { let providers = (capability.providers as Provider[]).filter(p => !disabledProviders.has(p.id)); if (options.providers) { const allowed = new Set(options.providers); providers = providers.filter(p => allowed.has(p.id)); } if (options.excludeProviders) { const excluded = new Set(options.excludeProviders); providers = providers.filter(p => !excluded.has(p.id)); } return providers; } /** * Load a capability by ID. */ export async function loadCapability(capabilityId: string, options: LoadOptions = {}): Promise> { const capability = capabilities.get(capabilityId) as Capability | undefined; if (!capability) { throw new Error(`Unknown capability: "${capabilityId}"`); } const cwd = options.cwd ?? getProjectDir(); const home = os.homedir(); const repoRoot = await findRepoRoot(cwd); const ctx: LoadContext = { cwd, home, repoRoot }; const providers = filterProviders(capability, options); return await loadImpl(capability, providers, ctx, options); } // ============================================================================= // Provider Enable/Disable API // ============================================================================= /** * Initialize capability system with settings manager for persistence. * Call this once on startup to enable persistent provider state. */ export function initializeWithSettings(activeSettings: Settings): void { settings = activeSettings; // Load disabled providers from settings const disabled = settings.get("disabledProviders"); disabledProviders.clear(); for (const id of disabled) { disabledProviders.add(id); } } /** * Persist current disabled providers to settings. */ function persistDisabledProviders(): void { if (settings) { settings.set("disabledProviders", Array.from(disabledProviders)); } } /** * Disable a provider globally (across all capabilities). */ export function disableProvider(providerId: string): void { disabledProviders.add(providerId); persistDisabledProviders(); } /** * Enable a previously disabled provider. */ export function enableProvider(providerId: string): void { disabledProviders.delete(providerId); persistDisabledProviders(); } /** * Check if a provider is enabled. */ export function isProviderEnabled(providerId: string): boolean { return !disabledProviders.has(providerId); } /** * Get list of all disabled provider IDs. */ export function getDisabledProviders(): string[] { return Array.from(disabledProviders); } /** * Set disabled providers from a list (replaces current set). */ export function setDisabledProviders(providerIds: string[]): void { disabledProviders.clear(); for (const id of providerIds) { disabledProviders.add(id); } persistDisabledProviders(); } // ============================================================================= // Introspection API // ============================================================================= /** * Get a capability definition (for introspection). */ export function getCapability(id: string): Capability | undefined { return capabilities.get(id) as Capability | undefined; } /** * List all registered capability IDs. */ export function listCapabilities(): string[] { return Array.from(capabilities.keys()); } /** * Get capability info for UI display. */ export function getCapabilityInfo(capabilityId: string): CapabilityInfo | undefined { const capability = capabilities.get(capabilityId); if (!capability) return undefined; return { id: capability.id, displayName: capability.displayName, description: capability.description, providers: capability.providers.map(p => ({ id: p.id, displayName: p.displayName, description: p.description, priority: p.priority, enabled: !disabledProviders.has(p.id), })), }; } /** * Get all capabilities info for UI display. */ export function getAllCapabilitiesInfo(): CapabilityInfo[] { return listCapabilities().map(id => getCapabilityInfo(id)!); } /** * Get provider info for UI display. */ export function getProviderInfo(providerId: string): ProviderInfo | undefined { const meta = providerMeta.get(providerId); const caps = providerCapabilities.get(providerId); if (!meta || !caps) return undefined; // Find priority from first capability's provider list let priority = 0; for (const capId of caps) { const cap = capabilities.get(capId); const provider = cap?.providers.find(p => p.id === providerId); if (provider) { priority = provider.priority; break; } } return { id: providerId, displayName: meta.displayName, description: meta.description, priority, capabilities: Array.from(caps), enabled: !disabledProviders.has(providerId), }; } /** * Get all providers info for UI display (deduplicated across capabilities). */ export function getAllProvidersInfo(): ProviderInfo[] { const providers: ProviderInfo[] = []; for (const providerId of providerMeta.keys()) { const info = getProviderInfo(providerId); if (info) { providers.push(info); } } // Sort by priority (highest first) providers.sort((a, b) => b.priority - a.priority); return providers; } // ============================================================================= // Cache Management // ============================================================================= /** * Reset all caches. Call after chdir or filesystem changes. */ export function reset(): void { clearFsCache(); } /** * Invalidate cache for a specific path. * @param filePath - Absolute or relative path to invalidate */ export function invalidate(filePath: string, cwd?: string): void { const resolved = cwd ? path.resolve(cwd, filePath) : filePath; invalidateFs(resolved); } /** * Get cache stats for diagnostics. */ export function cacheStats(): { content: number; dir: number } { return fsCacheStats(); } // ============================================================================= // Re-exports // ============================================================================= export type * from "./types";