import type { Command, CommandSchema } from './types'; import type {CommandResult, CommandRunner, CommandRunnerContext, PromptEvent} from './command-runner'; import { parseCommand } from './command-parse'; import { applyCommandSchema } from './command-validate'; import { parseCommandSchema } from './schema-parse'; import {AsyncQueue} from "../async-queue"; export type CommandRegistry = Map>; export function createCommandRegistry(): CommandRegistry { return new Map(); } export function registerCommand( registry: CommandRegistry, runner: CommandRunner ): void { registry.set(runner.schema.name, runner as CommandRunner); } export function unregisterCommand( registry: CommandRegistry, name: string ): void { registry.delete(name); } export function hasCommand( registry: CommandRegistry, name: string ): boolean { return registry.has(name); } export function getCommand( registry: CommandRegistry, name: string ): CommandRunner | undefined { return registry.get(name); } type Listener = (e: PromptEvent) => void; export type CommandRunnerContextExport = CommandRunnerContext & { registry: CommandRegistry; promptQueue: AsyncQueue; _activePrompt: PromptEvent | null; _resolvePrompt: (command: Command) => void; _rejectPrompt: (error: Error) => void; _pendingInput: string | null; }; export function createCommandRunnerContext( registry: CommandRegistry, context: TContext ): CommandRunnerContextExport { const listeners = new Set(); const on = (_event: 'prompt', listener: Listener) => { listeners.add(listener); }; const off = (_event: 'prompt', listener: Listener) => { listeners.delete(listener); }; let activePrompt: PromptEvent | null = null; const resolvePrompt = (command: Command) => { if (activePrompt) { activePrompt.resolve(command); activePrompt = null; } }; const rejectPrompt = (error: Error) => { if (activePrompt) { activePrompt.reject(error); activePrompt = null; } }; const prompt = (schema: Parameters['prompt']>[0]): Promise => { const resolvedSchema = typeof schema === 'string' ? parseCommandSchema(schema) : schema; return new Promise((resolve, reject) => { activePrompt = { schema: resolvedSchema, resolve, reject }; const event: PromptEvent = { schema: resolvedSchema, resolve, reject }; for (const listener of listeners) { listener(event); } }); }; const runnerCtx: CommandRunnerContextExport = { registry, context, run: (input: string) => runCommandWithContext(runnerCtx, input) as Promise>, runParsed: (command: Command) => runCommandParsedWithContext(runnerCtx, command), prompt, on, off, _activePrompt: null, _resolvePrompt: resolvePrompt, _rejectPrompt: rejectPrompt, _pendingInput: null, promptQueue: null! }; Object.defineProperty(runnerCtx, '_activePrompt', { get: () => activePrompt, }); let promptQueue: AsyncQueue; Object.defineProperty(runnerCtx, 'promptQueue', { get(){ if (!promptQueue) { promptQueue = new AsyncQueue(); listeners.add(async (event) => { promptQueue.push(event); }); } return promptQueue; } }); return runnerCtx; } async function executeWithRunnerContext( runnerCtx: CommandRunnerContextExport, runner: CommandRunner, command: Command ): Promise { try { const result = await runner.run.call(runnerCtx, command); return { success: true, result }; } catch (e) { const error = e as Error; return { success: false, error: error.message }; } } export async function runCommand( registry: CommandRegistry, context: TContext, input: string ): Promise { const runnerCtx = createCommandRunnerContext(registry, context); return await runCommandWithContext(runnerCtx, input); } async function runCommandWithContext( runnerCtx: CommandRunnerContextExport, input: string ): Promise<{ success: true; result: unknown } | { success: false; error: string }> { const command = parseCommand(input); return await runCommandParsedWithContext(runnerCtx, command); } export async function runCommandParsed( registry: CommandRegistry, context: TContext, command: Command ): Promise { const runnerCtx = createCommandRunnerContext(registry, context); return await runCommandParsedWithContext(runnerCtx, command); } async function runCommandParsedWithContext( runnerCtx: CommandRunnerContextExport, command: Command ): Promise { const runner = runnerCtx.registry.get(command.name); if (!runner) { return { success: false, error: `Unknown command: ${command.name}` }; } const validationResult = applyCommandSchema(command, runner.schema); if (!validationResult.valid) { return { success: false, error: validationResult.errors.join('; ') }; } return await executeWithRunnerContext(runnerCtx, runner, validationResult.command); }