UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

368 lines 10.5 kB
/** * Extension Registry * * Provides efficient storage, indexing, and retrieval of AIWG extensions. * Supports O(1) lookup by ID, type-based indexing, and command alias resolution. * * @architecture @.aiwg/architecture/unified-extension-schema.md * @tests @test/unit/extensions/registry.test.ts * @version 1.0.0 */ /** * Extension registry for storing and querying extensions * * Maintains multiple indexes for efficient lookup: * - Primary storage: Map<id, Extension> for O(1) lookup by ID * - Type index: Map<type, Set<id>> for filtering by type * - Alias map: Map<alias, id> for command resolution * * @example * ```typescript * const registry = new ExtensionRegistry(); * * // Register an extension * registry.register(myExtension); * * // Lookup by ID (O(1)) * const ext = registry.get('mention-wire'); * * // Get all commands * const commands = registry.getByType('command'); * * // Resolve command alias * const id = registry.resolveCommand('wire'); // Returns 'mention-wire' * ``` */ export class ExtensionRegistry { /** * Primary storage: extension ID to Extension * * Provides O(1) lookup by ID. */ extensions = new Map(); /** * Type index: extension type to Set of extension IDs * * Enables efficient filtering by type. */ byType = new Map(); /** * Alias map: command alias to extension ID * * Enables O(1) command resolution by name or alias. */ aliasMap = new Map(); /** * Qualified name map: `{namespace}-{id}` to extension ID * * Enables lookup by canonical namespaced slug (e.g. `aiwg-sync` → `sync`). * Only populated for skills/commands that carry a `namespace` field. */ qualifiedNameMap = new Map(); /** * Register an extension in the registry * * If an extension with the same ID already exists, it will be replaced. * Automatically updates type index and registers command aliases. * * @param extension - Extension to register * * @example * ```typescript * registry.register({ * id: 'mention-wire', * type: 'command', * name: 'Mention Wire', * // ... other fields * }); * ``` */ register(extension) { const { id } = extension; // If extension already exists, remove it from old type index const existing = this.extensions.get(id); if (existing) { this.removeFromTypeIndex(existing); } // Store extension this.extensions.set(id, extension); // Add to type index this.addToTypeIndex(extension); // Register command aliases if present if ((extension.type === 'command' || extension.type === 'skill') && 'aliases' in extension.metadata) { const aliases = extension.metadata.aliases; if (Array.isArray(aliases)) { aliases.forEach((alias) => { this.registerAlias(alias, id); }); } } // Register primary command/skill name if (extension.type === 'command' || extension.type === 'skill') { this.aliasMap.set(id, id); } // Register qualified name for namespaced skills/commands if ((extension.type === 'skill' || extension.type === 'command') && 'namespace' in extension.metadata) { const namespace = extension.metadata.namespace; if (namespace) { const prefix = `${namespace}-`; const qualifiedName = id.startsWith(prefix) ? id : `${prefix}${id}`; this.qualifiedNameMap.set(qualifiedName, id); } } } /** * Get extension by ID * * O(1) lookup via Map. * * @param id - Extension ID * @returns Extension if found, undefined otherwise * * @example * ```typescript * const ext = registry.get('mention-wire'); * if (ext) { * console.log(ext.name); * } * ``` */ get(id) { return this.extensions.get(id); } /** * Get all extensions of a specific type * * Uses type index for efficient filtering. * * @param type - Extension type to filter by * @returns Array of extensions of the specified type * * @example * ```typescript * const commands = registry.getByType('command'); * const agents = registry.getByType('agent'); * ``` */ getByType(type) { const ids = this.byType.get(type); if (!ids || ids.size === 0) { return []; } const extensions = []; for (const id of ids) { const ext = this.extensions.get(id); if (ext) { extensions.push(ext); } } return extensions; } /** * Resolve a command name or alias to extension ID * * O(1) lookup via Map. * * @param command - Command name or alias * @returns Extension ID if found, undefined otherwise * * @example * ```typescript * const id = registry.resolveCommand('wire'); // 'mention-wire' * const ext = registry.get(id); * ``` */ resolveCommand(command) { return this.aliasMap.get(command); } /** * Register a command alias * * Associates an alias with an extension ID. The extension does not * need to be registered first. * * @param alias - Command alias * @param extensionId - Target extension ID * * @example * ```typescript * registry.registerAlias('wire', 'mention-wire'); * registry.registerAlias('w', 'mention-wire'); * ``` */ registerAlias(alias, extensionId) { this.aliasMap.set(alias, extensionId); } /** * Resolve a qualified namespaced name to an extension ID. * * Accepts either the canonical qualified slug (`aiwg-sync`) or the bare ID (`sync`). * Falls back to bare ID lookup when no qualified name match is found. * * @param qualifiedName - Qualified skill name (e.g. `aiwg-sync`) or bare ID * @returns Extension ID if found, undefined otherwise * * @example * ```typescript * const id = registry.resolveQualifiedName('aiwg-sync'); // 'sync' * const ext = registry.get(id); * ``` */ resolveQualifiedName(qualifiedName) { return this.qualifiedNameMap.get(qualifiedName) ?? this.extensions.get(qualifiedName)?.id; } /** * Get all registered extensions * * Returns a new array containing all extensions. Modifications to the * returned array will not affect the registry. * * @returns Array of all extensions * * @example * ```typescript * const all = registry.getAll(); * console.log(`Registry contains ${all.length} extensions`); * ``` */ getAll() { return Array.from(this.extensions.values()); } /** * Get count of registered extensions * * @returns Number of extensions in the registry * * @example * ```typescript * console.log(`${registry.size} extensions registered`); * ``` */ get size() { return this.extensions.size; } /** * Check if extension exists in registry * * O(1) lookup via Map. * * @param id - Extension ID to check * @returns True if extension exists, false otherwise * * @example * ```typescript * if (registry.has('mention-wire')) { * console.log('Extension is registered'); * } * ``` */ has(id) { return this.extensions.has(id); } /** * Clear the registry * * Removes all extensions, type index entries, and aliases. * * @example * ```typescript * registry.clear(); * console.log(registry.size); // 0 * ``` */ clear() { this.extensions.clear(); this.byType.clear(); this.aliasMap.clear(); this.qualifiedNameMap.clear(); } /** * Add extension to type index * * @private * @param extension - Extension to index */ addToTypeIndex(extension) { const { id, type } = extension; let typeSet = this.byType.get(type); if (!typeSet) { typeSet = new Set(); this.byType.set(type, typeSet); } typeSet.add(id); } /** * Remove extension from type index * * @private * @param extension - Extension to remove from index */ removeFromTypeIndex(extension) { const { id, type } = extension; const typeSet = this.byType.get(type); if (typeSet) { typeSet.delete(id); // Clean up empty sets if (typeSet.size === 0) { this.byType.delete(type); } } } } // ============================================ // Singleton Instance // ============================================ /** * Global registry instance * * Singleton instance shared across all calls to getRegistry(). */ let globalRegistry = null; /** * Get the global registry instance * * Returns a singleton ExtensionRegistry instance. All calls to this * function return the same instance, allowing state to be shared * across the application. * * @returns Global ExtensionRegistry instance * * @example * ```typescript * import { getRegistry } from './registry.js'; * * const registry = getRegistry(); * registry.register(myExtension); * * // Later, in another module * const sameRegistry = getRegistry(); * console.log(sameRegistry.has('my-extension')); // true * ``` */ export function getRegistry() { if (!globalRegistry) { globalRegistry = new ExtensionRegistry(); } return globalRegistry; } /** * Create a new registry instance * * Returns a new, independent ExtensionRegistry instance. Use this when * you need an isolated registry for testing or scoped operations. * * @returns New ExtensionRegistry instance * * @example * ```typescript * import { createRegistry } from './registry.js'; * * const testRegistry = createRegistry(); * testRegistry.register(testExtension); * // testRegistry is independent of global registry * ``` */ export function createRegistry() { return new ExtensionRegistry(); } //# sourceMappingURL=registry.js.map