CommandRegistry.ts

Central registry for discovering and instantiating commands with dynamic loading, dependency injection, and metadata-driven orchestration capabilities

CommandRegistry.tsv1.0.022.3 KB
CommandRegistry.ts(typescript)
1import { BaseCommand, CommandMetadata } from '../interfaces/BaseCommand';
2import { ILogger, ICommandLogger } from '../logging/ILogger';
3import { ConsoleLogger } from '../logging/ConsoleLogger';
4import { BaseService, ServiceMetadata } from '../interfaces/BaseService';
5
6/**
7 * Registration information for a command.
8 * Links the command class with its metadata.
9 */
10export interface CommandRegistration {
11  /** Constructor function for creating command instances */
12  constructor: CommandConstructor;
13  
14  /** Static metadata from the command class */
15  metadata: CommandMetadata;
16  
17  /** When this command was registered */
18  registeredAt: Date;
19}
20
21/**
22 * Constructor type for command classes.
23 * All command classes must have a static metadata property.
24 */
25export interface CommandConstructor {
26  new(input?: any, logger?: ICommandLogger, services?: Record<string, any>): BaseCommand;
27  new(...args: any[]): BaseCommand; // For backward compatibility
28  readonly metadata: CommandMetadata;
29}
30
31/**
32 * Represents a possible workflow chain from start to end contract.
33 */
34export interface WorkflowChain {
35  /** Commands in execution order */
36  commands: CommandMetadata[];
37  
38  /** Total estimated duration */
39  estimatedDuration: number;
40  
41  /** Chain complexity score (number of steps) */
42  complexity: number;
43  
44  /** Whether this chain has any gaps or compatibility issues */
45  isComplete: boolean;
46}
47
48/**
49 * Central registry for discovering and instantiating commands.
50 * Enables dynamic command discovery and metadata-driven orchestration.
51 */
52export class CommandRegistry extends BaseService {
53  getMetadata(): ServiceMetadata {
54    return {
55      name: 'CommandRegistry',
56      displayName: 'Command Registry',
57      description: 'Central registry for discovering and instantiating commands. Enables dynamic command discovery and metadata-driven orchestration.',
58      contract: 'CommandRegistry',
59      version: '1.0.0',
60      contractVersion: '1.0',
61      implementation: 'CommandRegistry'
62    } as ServiceMetadata;
63  }
64  
65  initialize(_config?: Record<string, any>): Promise<void> | void {
66    CommandRegistry.getInstance();
67  }
68  
69  isHealthy(): Promise<boolean> | boolean {
70    return true; // Always healthy for now, can be extended later
71  }
72  
73  destroy(): Promise<void> | void {
74    this.commands.clear();
75    this.logger.info('CommandRegistry destroyed');  
76  }
77  
78  private commands = new Map<string, CommandRegistration>();
79  private static instance: CommandRegistry;
80  private logger: ILogger;
81
82  constructor() {
83    super();
84    this.logger = new ConsoleLogger('CommandRegistry');
85  }
86  
87  /**
88   * This method should only be called by the ServiceRegistry.
89   * To access the singleton instance, use ServiceRegistry.getInstance().getCommandRegistry().
90   * @returns 
91   */
92  public static getInstance(): CommandRegistry {
93    if (!CommandRegistry.instance) {
94      CommandRegistry.instance = new CommandRegistry();
95    }
96    return CommandRegistry.instance;
97  }
98
99  /**
100   * Create a new isolated CommandRegistry instance
101   * Use this for per-container registries to avoid singleton issues
102   */
103  public static createInstance(): CommandRegistry {
104    return new CommandRegistry();
105  }
106  
107  /**
108   * This method will attempt to dynamically load a command module based on the provided metadata.
109   * It will then return a CommandRegistration object containing the command constructor and metadata.
110   * This command does not add the command to the registry, it only resolves it.
111   * @param metadata 
112   * @returns 
113   */
114  private async resolveCommand(metadata: CommandMetadata): Promise<CommandRegistration> {
115    let fullCommandName = `@/commands/${metadata.category}/${metadata.name}`;
116    let command = this.commands.get(fullCommandName);
117    if (!command) {
118        // Lazy load the command module
119        const module = await import(`../../commands/${metadata.category}/${metadata.name}`);
120        const commandConstructor = Object.values(module)[0] as CommandConstructor;
121        command = {
122          constructor: commandConstructor,
123          metadata,
124          registeredAt: new Date()
125        } as CommandRegistration;
126    }
127    return command as CommandRegistration;
128  }
129  
130  /**
131   * Create a command instance by full name (category/CommandName)
132   * This method allows commands to create other commands through the registry
133   */
134  public async createCommandByName<T extends BaseCommand>(
135    fullCommandName: string,
136    input?: any,
137    logger?: ICommandLogger
138  ): Promise<T> {
139    try {
140      // fullCommandName format: "category/CommandName"
141      const [category, commandName] = fullCommandName.split('/');
142      
143      if (!category || !commandName) {
144        throw new Error(`Invalid command name format. Expected 'category/CommandName', got '${fullCommandName}'`);
145      }
146      // Check if the command is already registered
147      if (this.commands.has(fullCommandName)) {
148        const registration = this.commands.get(fullCommandName);
149        if (registration) {
150          return await this.createCommand<T>(registration.constructor, input, logger);
151        }
152      }
153      // Lazy load the command module
154      const module = await import(`../../commands/${category}/${commandName}`);
155      
156      // Find the command constructor (it should match the commandName)
157      const commandConstructor = module[commandName] as CommandConstructor;
158      
159      if (!commandConstructor || !commandConstructor.metadata) {
160        throw new Error(`Command ${commandName} not found in module`);
161      }
162      
163      // Create the command using the registry
164      return await this.createCommand<T>(commandConstructor, input, logger);
165      
166    } catch (error) {
167      this.logger.error(`Failed to create command ${fullCommandName}`, error as Error);
168      throw new Error(`Failed to create command ${fullCommandName}: ${error}`);
169    }
170  }
171  
172  public async get<T extends BaseCommand>(commandClass: CommandConstructor, input?: any, logger?: ICommandLogger): Promise<T> {
173    const metadata = commandClass.metadata;
174    
175    if (!metadata || !metadata.name) {
176      const error = `Command class must have static metadata with name property`;
177      this.logger.error(error);
178      throw new Error(error);
179    }
180    let fullCommandName = `@/commands/${metadata.category}/${metadata.name}`;
181    if (!this.commands.has(fullCommandName)) {
182      const registration = await this.resolveCommand(metadata);
183      if (!registration) {
184        const error = `Command '${fullCommandName}' is not registered`;
185        this.logger.error(error);
186        throw new Error(error);
187      }
188      this.commands.set(fullCommandName, registration);
189    }
190    
191    return await this.createCommand<T>(commandClass, input, logger);
192  }
193
194  /**
195   * Register a command class with the registry.
196   * Command must have static metadata property.
197   */
198  registerCommand(commandClass: CommandConstructor): void {
199    const metadata = commandClass.metadata;
200    
201    if (!metadata || !metadata.name) {
202      const error = `Command class must have static metadata with name property`;
203      this.logger.error(error);
204      throw new Error(error);
205    }
206    
207    if (this.commands.has(metadata.name)) {
208      const error = `Command '${metadata.name}' is already registered`;
209      this.logger.error(error);
210      throw new Error(error);
211    }
212    
213    this.commands.set(metadata.name, {
214      constructor: commandClass,
215      metadata,
216      registeredAt: new Date()
217    });
218    
219    this.logger.info(`Registered command: ${metadata.name}`, {
220      category: metadata.category,
221      inputType: metadata.inputType,
222      outputType: metadata.outputType,
223      permissions: metadata.permissions
224    });
225  }
226  
227  /**
228   * Create a command instance with automatic service and command dependency injection.
229   * This is the preferred method for creating commands.
230   */
231  private async createCommand<T extends BaseCommand>(
232    commandClass: CommandConstructor, 
233    input?: any, 
234    logger?: ICommandLogger,
235  ): Promise<T> {
236    const metadata = commandClass.metadata;
237    
238    // Handle both old and new dependency formats
239    const deps = metadata.dependencies;
240    let serviceDeps: string[] = [];
241    let commandDeps: string[] = [];
242    
243    if (Array.isArray(deps)) {
244      // Legacy format: dependencies: ['Service1', 'Service2']
245      serviceDeps = deps;
246    } else if (deps && typeof deps === 'object') {
247      // New format: dependencies: { services: [...], commands: [...] }
248      serviceDeps = deps.services || [];
249      commandDeps = deps.commands || [];
250    }
251    
252    // Resolve service dependencies
253    const services = this.resolveServiceDependencies(serviceDeps);
254    
255    // Resolve command dependencies
256    const commands = await this.resolveCommandDependencies(commandDeps);
257    
258    this.logger.debug(`Creating command '${metadata.name}' with dependencies:`, {
259      commandName: metadata.name,
260      serviceCount: Object.keys(services).length,
261      commandCount: Object.keys(commands).length,
262      services: Object.keys(services),
263      commands: Object.keys(commands)
264    });
265    
266    return new commandClass(input, logger, services, commands) as T;
267  }
268
269  /**
270   * Service resolver function - set by the DI container
271   */
272  private serviceResolver?: (serviceName: string) => any;
273
274  /**
275   * Set the service resolver function
276   */
277  setServiceResolver(resolver: (serviceName: string) => any): void {
278    this.serviceResolver = resolver;
279  }
280
281  /**
282   * Resolve service dependencies using the configured service resolver.
283   * Maps dependency names to actual service instances.
284   */
285  private resolveServiceDependencies(dependencies: string[]): Record<string, any> {
286    const services: Record<string, any> = {};
287    
288    if (!this.serviceResolver) {
289      this.logger.warn('No service resolver configured - commands may not have access to services');
290      return services;
291    }
292    
293    for (const dependencyName of dependencies) {
294      try {
295        const service = this.serviceResolver(dependencyName);
296        services[dependencyName] = service;
297        this.logger.debug(`Resolved service dependency: ${dependencyName}`);
298        
299      } catch (error) {
300        const errorMessage = `Failed to resolve service dependency '${dependencyName}': ${error instanceof Error ? error.message : String(error)}`;
301        this.logger.error(errorMessage, undefined, { dependencyName });
302        throw new Error(errorMessage);
303      }
304    }
305    
306    return services;
307  }
308  
309  /**
310   * Resolve command dependencies by creating instances with limited depth to prevent circular dependencies
311   */
312  private async resolveCommandDependencies(dependencies: string[], depth: number = 0): Promise<Record<string, BaseCommand>> {
313    const commands: Record<string, BaseCommand> = {};
314    const maxDepth = 10; // Allow deep dependency trees but catch circular dependencies
315    
316    if (depth >= maxDepth) {
317      throw new Error(`Command dependency depth exceeded ${maxDepth} levels. This likely indicates a circular dependency. Depth: ${depth}`);
318    }
319    
320    for (const dependencyName of dependencies) {
321      try {
322        // fullCommandName format: "category/CommandName"
323       
324        const [category, commandName] = dependencyName.split('/');
325        
326        if (!category || !commandName) {
327          throw new Error(`Invalid command name format. Expected 'category/CommandName', got '${dependencyName}'`);
328        }
329        
330        // Lazy load the command module
331        const module = await import(`../../commands/${category}/${commandName}`);
332        
333        // Find the command constructor (it should match the commandName)
334        const commandConstructor = module[commandName] as CommandConstructor;
335        
336        if (!commandConstructor || !commandConstructor.metadata) {
337          throw new Error(`Command ${commandName} not found in module`);
338        }
339        
340        // Get dependencies for this command
341        const metadata = commandConstructor.metadata;
342        const deps = metadata.dependencies;
343        let serviceDeps: string[] = [];
344        let commandDeps: string[] = [];
345        
346        if (Array.isArray(deps)) {
347          serviceDeps = deps;
348        } else if (deps && typeof deps === 'object') {
349          serviceDeps = deps.services || [];
350          commandDeps = deps.commands || [];
351        }
352        
353        const services = this.resolveServiceDependencies(serviceDeps);
354        
355        // Recursively resolve command dependencies up to maxDepth
356        let nestedCommands: Record<string, BaseCommand> = {};
357        if (depth < maxDepth && commandDeps.length > 0) {
358          nestedCommands = await this.resolveCommandDependencies(commandDeps, depth + 1);
359        }
360        
361        // Create instance with resolved dependencies
362        const commandInstance = new commandConstructor(undefined, undefined, services, nestedCommands);
363        commands[dependencyName] = commandInstance;
364        
365        this.logger.debug(`Resolved command dependency: ${dependencyName} (depth: ${depth})`);
366        
367      } catch (error) {
368        const errorMessage = `Failed to resolve command dependency '${dependencyName}': ${error instanceof Error ? error.message : String(error)}`;
369        this.logger.error(errorMessage, undefined, { dependencyName });
370        throw new Error(errorMessage);
371      }
372    }
373    
374    return commands;
375  }
376  
377
378  /**
379   * Get command metadata without instantiating.
380   * Useful for analysis and orchestration planning.
381   */
382  getCommandMetadata(name: string): CommandMetadata {
383    const registration = this.commands.get(name);
384    
385    if (!registration) {
386      throw new Error(`Command '${name}' not found in registry`);
387    }
388    
389    return registration.metadata;
390  }
391  
392  /**
393   * Find commands by category.
394   * Useful for grouping related functionality.
395   */
396  findByCategory(category: string): CommandMetadata[] {
397    return Array.from(this.commands.values())
398      .filter(reg => reg.metadata.category === category)
399      .map(reg => reg.metadata);
400  }
401
402  /**
403   * Find commands that depend on a specific service.
404   * Useful for impact analysis when services change.
405   */
406  findByDependency(serviceName: string): CommandMetadata[] {
407    return Array.from(this.commands.values())
408      .filter(reg => {
409        const deps = reg.metadata.dependencies;
410        if (!deps) return false;
411        
412        // Handle both old and new dependency formats
413        if (Array.isArray(deps)) {
414          return deps.includes(serviceName);
415        } else if (typeof deps === 'object' && deps.services) {
416          return deps.services.includes(serviceName);
417        }
418        return false;
419      })
420      .map(reg => reg.metadata);
421  }
422  
423  /**
424   * Find commands that can process specific input/output types.
425   * Enables automatic pipeline construction.
426   */
427  findByDataFlow(inputType?: string, outputType?: string): CommandMetadata[] {
428    return Array.from(this.commands.values())
429      .filter(reg => {
430        const meta = reg.metadata;
431        const inputMatch = !inputType || meta.inputType === inputType;
432        const outputMatch = !outputType || meta.outputType === outputType;
433        return inputMatch && outputMatch;
434      })
435      .map(reg => reg.metadata);
436  }
437  
438  // ===============================
439  // CONTRACT-BASED DISCOVERY METHODS
440  // ===============================
441  
442  /**
443   * Find commands that can consume the output of the specified command.
444   * Enables automatic workflow chain construction.
445   */
446  findNextCommands(producingCommandName: string): CommandMetadata[] {
447    const producingCommand = this.getCommandMetadata(producingCommandName);
448    const outputType = producingCommand.outputType;
449    
450    if (!outputType) {
451      return []; // Command doesn't produce output
452    }
453    
454    return Array.from(this.commands.values())
455      .filter(reg => reg.metadata.inputType === outputType)
456      .map(reg => reg.metadata);
457  }
458  
459  /**
460   * Find commands that produce output that the specified command can consume.
461   * Useful for finding data sources for a command.
462   */
463  findPreviousCommands(consumingCommandName: string): CommandMetadata[] {
464    const consumingCommand = this.getCommandMetadata(consumingCommandName);
465    const inputType = consumingCommand.inputType;
466    
467    if (!inputType) {
468      return []; // Command doesn't require input
469    }
470    
471    return Array.from(this.commands.values())
472      .filter(reg => reg.metadata.outputType === inputType)
473      .map(reg => reg.metadata);
474  }
475  
476  /**
477   * Find commands that produce the specified output type.
478   * Useful for finding all possible sources of a data type.
479   */
480  findCommandsProducing(outputType: string): CommandMetadata[] {
481    return Array.from(this.commands.values())
482      .filter(reg => reg.metadata.outputType === outputType)
483      .map(reg => reg.metadata);
484  }
485  
486  /**
487   * Find commands that consume the specified input type.
488   * Useful for finding all possible consumers of a data type.
489   */
490  findCommandsConsuming(inputType: string): CommandMetadata[] {
491    return Array.from(this.commands.values())
492      .filter(reg => reg.metadata.inputType === inputType)
493      .map(reg => reg.metadata);
494  }
495  
496  /**
497   * Find alternative commands with the same input/output contracts.
498   * Useful for A/B testing, fallbacks, and command substitution.
499   */
500  findAlternativeCommands(commandName: string): CommandMetadata[] {
501    const originalCommand = this.getCommandMetadata(commandName);
502    
503    return Array.from(this.commands.values())
504      .filter(reg => {
505        const meta = reg.metadata;
506        return meta.name !== commandName && // Not the same command
507               meta.inputType === originalCommand.inputType &&
508               meta.outputType === originalCommand.outputType &&
509               meta.errorType === originalCommand.errorType;
510      })
511      .map(reg => reg.metadata);
512  }
513  
514  /**
515   * Find complete workflow chains from start contract to end contract.
516   * Uses breadth-first search to find shortest paths.
517   */
518  findWorkflowChains(startContract: string, endContract: string, maxDepth: number = 5): WorkflowChain[] {
519    const chains: WorkflowChain[] = [];
520    const visited = new Set<string>();
521    
522    // Find all commands that can start the workflow (produce startContract)
523    const startCommands = this.findCommandsProducing(startContract);
524    
525    for (const startCommand of startCommands) {
526      const chain = this.buildChainRecursive(
527        [startCommand], 
528        endContract, 
529        visited, 
530        maxDepth
531      );
532      
533      if (chain) {
534        chains.push(chain);
535      }
536    }
537    
538    // Sort by complexity (shorter chains first)
539    return chains.sort((a, b) => a.complexity - b.complexity);
540  }
541  
542  /**
543   * Get comprehensive contract analysis for the registry.
544   * Shows all contract relationships and potential workflow paths.
545   */
546  getContractAnalysis(): any {
547    const commands = this.getAllCommands();
548    const contracts = new Set<string>();
549    const contractGraph: { [key: string]: string[] } = {};
550    
551    // Build contract graph
552    commands.forEach(cmd => {
553      if (cmd.inputType) contracts.add(cmd.inputType);
554      if (cmd.outputType) contracts.add(cmd.outputType);
555      
556      if (cmd.outputType) {
557        if (!contractGraph[cmd.outputType]) {
558          contractGraph[cmd.outputType] = [];
559        }
560        contractGraph[cmd.outputType].push(cmd.name);
561      }
562    });
563    
564    return {
565      totalCommands: commands.length,
566      totalContracts: contracts.size,
567      contractTypes: Array.from(contracts).sort(),
568      contractProducers: contractGraph,
569      orphanedContracts: Array.from(contracts).filter(contract => 
570        !this.findCommandsProducing(contract).length || 
571        !this.findCommandsConsuming(contract).length
572      ),
573      fullyConnectedContracts: Array.from(contracts).filter(contract => 
574        this.findCommandsProducing(contract).length > 0 && 
575        this.findCommandsConsuming(contract).length > 0
576      )
577    };
578  }
579  
580  /**
581   * Recursive helper for building workflow chains.
582   */
583  private buildChainRecursive(
584    currentChain: CommandMetadata[], 
585    targetContract: string, 
586    visited: Set<string>, 
587    remainingDepth: number
588  ): WorkflowChain | null {
589    
590    if (remainingDepth <= 0) return null;
591    
592    const lastCommand = currentChain[currentChain.length - 1];
593    
594    // Check if we've reached the target
595    if (lastCommand.outputType === targetContract) {
596      return {
597        commands: [...currentChain],
598        estimatedDuration: this.calculateChainDuration(currentChain),
599        complexity: currentChain.length,
600        isComplete: true
601      };
602    }
603    
604    // Find next possible commands
605    const nextCommands = this.findNextCommands(lastCommand.name);
606    
607    for (const nextCommand of nextCommands) {
608      if (!visited.has(nextCommand.name)) {
609        visited.add(nextCommand.name);
610        
611        const result = this.buildChainRecursive(
612          [...currentChain, nextCommand],
613          targetContract,
614          visited,
615          remainingDepth - 1
616        );
617        
618        if (result) {
619          return result;
620        }
621        
622        visited.delete(nextCommand.name);
623      }
624    }
625    
626    return null;
627  }
628  
629  /**
630   * Calculate estimated duration for a command chain.
631   */
632  private calculateChainDuration(chain: CommandMetadata[]): number {
633    return chain.reduce((total, cmd) => {
634      const duration = cmd.performance?.expectedDuration || "1000ms";
635      const ms = this.parseDurationToMs(duration);
636      return total + ms;
637    }, 0);
638  }
639  
640  /**
641   * Parse duration strings to milliseconds.
642   */
643  private parseDurationToMs(duration: string): number {
644    const match = duration.match(/(\d+(?:\.\d+)?)\s*(ms|s|seconds?|minutes?)/i);
645    if (!match) return 1000; // Default 1 second
646    
647    const value = parseFloat(match[1]);
648    const unit = match[2].toLowerCase();
649    
650    switch (unit) {
651      case 'ms': return value;
652      case 's': 
653      case 'second':
654      case 'seconds': return value * 1000;
655      case 'minute':
656      case 'minutes': return value * 60 * 1000;
657      default: return value;
658    }
659  }
660  
661  // ===============================
662  // EXISTING METHODS
663  // ===============================
664  
665  /**
666   * Get all registered commands.
667   * Useful for system analysis and debugging.
668   */
669  getAllCommands(): CommandMetadata[] {
670    return Array.from(this.commands.values()).map(reg => reg.metadata);
671  }
672  
673  /**
674   * Check if a command is registered.
675   */
676  hasCommand(name: string): boolean {
677    return this.commands.has(name);
678  }
679  
680  /**
681   * Clear all registered commands.
682   * Mainly useful for testing.
683   */
684  clear(): void {
685    this.commands.clear();
686  }
687}

Metadata

Path
utaba/main/services/micro-block/CommandRegistry.ts
Namespace
utaba/main/services/micro-block
Author
utaba
Category
services
Technology
typescript
Contract Version
1.0.0
MIME Type
application/typescript
Published
18-Jul-2025
Last Updated
18-Jul-2025