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