aiwg
Version:
Cognitive architecture for AI-augmented software development with structured memory, ensemble validation, and closed-loop correction. FAIR-aligned artifacts, 84% cost reduction via human-in-the-loop, standards adopted by 100+ organizations.
227 lines (199 loc) • 6.25 kB
text/typescript
/**
* CLI Router - Registry-Based Command Dispatch with Hook Support
*
* Main CLI entry point using the extension registry for command routing.
* Replaces the monolithic switch/case with O(1) handler lookup.
* Integrates hook system for lifecycle events.
*
* @implements @.aiwg/requirements/use-cases/UC-004-extension-system.md
* @architecture @.aiwg/architecture/decisions/ADR-001-unified-extension-system.md
* @tests @test/unit/cli/router.test.ts
* @issue #33
* @issue #58
*/
import { loadRegistry, type LoadedRegistry } from '../extensions/loader.js';
import { getFrameworkRoot } from '../channel/manager.mjs';
import type { HandlerContext } from './handlers/types.js';
import { HookRegistry, HookExecutor } from './hooks/index.js';
import type { HookContext } from './hooks/index.js';
// Cached loaded registry
let cachedRegistry: LoadedRegistry | null = null;
// Global hook registry and executor
const hookRegistry = new HookRegistry();
const hookExecutor = new HookExecutor(hookRegistry);
/**
* Initialize the CLI router
*
* Loads the extension registry with capability indexing and caches it
* for subsequent calls.
*
* @returns Loaded registry with handlers
*/
export async function initRouter(): Promise<LoadedRegistry> {
if (!cachedRegistry) {
cachedRegistry = await loadRegistry({ indexCapabilities: true });
}
return cachedRegistry;
}
/**
* Get the global hook registry
*
* Allows external code to register hooks.
*
* @returns Global hook registry
*/
export function getHookRegistry(): HookRegistry {
return hookRegistry;
}
/**
* Main CLI entry point - registry-based routing with hooks
*
* Routes commands to handlers via the extension registry. Handles alias
* resolution, unknown commands, and help display. Executes hooks at
* appropriate lifecycle points.
*
* @param args - Command line arguments
* @param options - Execution options
*
* @example
* ```typescript
* // Route 'use' command
* await run(['use', 'sdlc']);
*
* // Handle alias
* await run(['--help']);
*
* // Show help when no args
* await run([]);
* ```
*/
export async function run(args: string[], options: { cwd?: string } = {}): Promise<void> {
const registry = await initRouter();
const [rawCommand, ...commandArgs] = args;
// No command - show help
if (!rawCommand) {
const helpHandler = registry.handlerMap.get('help');
if (helpHandler) {
const ctx = await buildContext([], args, options);
await helpHandler.execute(ctx);
}
return;
}
// Resolve command (handles aliases)
const commandId = registry.registry.resolveCommand(rawCommand);
if (!commandId) {
console.error(`Unknown command: ${rawCommand}`);
console.log('Run `aiwg help` for usage information.');
process.exit(1);
}
// Get handler
const handler = registry.handlerMap.get(commandId);
if (!handler) {
console.error(`No handler found for command: ${commandId}`);
process.exit(1);
}
// Build context for handler and hooks
const ctx = await buildContext(commandArgs, args, options);
// Build hook context
const hookCtx: HookContext = {
event: 'pre-command',
command: commandId,
args: commandArgs,
cwd: ctx.cwd,
frameworkRoot: ctx.frameworkRoot,
};
try {
// Execute pre-command hooks
const preResult = await hookExecutor.execute('pre-command', hookCtx);
// Check if execution was blocked
if (preResult.blocked) {
console.error(preResult.message || `Command blocked by hook: ${preResult.blockingHook}`);
process.exit(1);
}
// Report hook errors (but don't block execution)
if (preResult.errors.length > 0) {
for (const { hook, error } of preResult.errors) {
console.error(`Warning: Hook ${hook} failed: ${error.message}`);
}
}
// Apply modifications to context if any
if (Object.keys(preResult.modifications).length > 0) {
// Hooks can modify args, but we keep the handler context simple
// Modifications are available in hookCtx.data for post-command hooks
hookCtx.data = preResult.modifications;
}
// Execute handler
const result = await handler.execute(ctx);
// Execute post-command hooks
const postHookCtx: HookContext = {
event: 'post-command',
command: commandId,
args: commandArgs,
cwd: ctx.cwd,
frameworkRoot: ctx.frameworkRoot,
data: {
...hookCtx.data,
exitCode: result.exitCode,
message: result.message,
},
};
const postResult = await hookExecutor.execute('post-command', postHookCtx);
// Report post-command hook errors
if (postResult.errors.length > 0) {
for (const { hook, error } of postResult.errors) {
console.error(`Warning: Post-command hook ${hook} failed: ${error.message}`);
}
}
// Output message if present
if (result.message) {
if (result.exitCode !== 0) {
console.error(result.message);
} else {
console.log(result.message);
}
}
if (result.exitCode !== 0) {
process.exit(result.exitCode);
}
} catch (error) {
// Execute on-error hooks
const errorHookCtx: HookContext = {
event: 'on-error',
command: commandId,
args: commandArgs,
cwd: ctx.cwd,
frameworkRoot: ctx.frameworkRoot,
error: error instanceof Error ? error : new Error(String(error)),
};
await hookExecutor.execute('on-error', errorHookCtx);
// Re-throw error
throw error;
}
}
/**
* Build handler context
*
* Constructs the context object passed to command handlers, including
* command arguments, working directory, and framework root path.
*
* @param args - Command arguments (after command name)
* @param rawArgs - Raw arguments including command name
* @param options - Execution options
* @returns Handler context
*/
async function buildContext(
args: string[],
rawArgs: string[],
options: { cwd?: string }
): Promise<HandlerContext> {
const frameworkRoot = await getFrameworkRoot();
return {
args,
rawArgs,
cwd: options.cwd || process.cwd(),
frameworkRoot,
dryRun: args.includes('--dry-run'),
};
}
// Export for testing
export { cachedRegistry };