262 lines
8.9 KiB
TypeScript
262 lines
8.9 KiB
TypeScript
import type { Command, CommandSchema } from './types';
|
|
import type {
|
|
CommandDef, CommandFunction,
|
|
CommandResult,
|
|
CommandRunner,
|
|
CommandRunnerContext,
|
|
CommandRunnerEvents,
|
|
PromptEvent,
|
|
PromptValidator
|
|
} from './command-runner';
|
|
import { parseCommand } from './command-parse';
|
|
import { applyCommandSchema } from './command-validate';
|
|
import { parseCommandSchema } from './schema-parse';
|
|
|
|
type CanRunParsed = {
|
|
runParsed<T=unknown>(command: Command): Promise<CommandResult<T>>,
|
|
}
|
|
|
|
export class CommandRegistry<TContext> extends Map<string, CommandRunner<TContext>>{
|
|
register<TFunc extends CommandFunction<TContext>>(...args: [schema: CommandSchema | string, run: TFunc] | [CommandDef<TContext, TFunc>]){
|
|
let schema: CommandSchema | string;
|
|
let run: TFunc;
|
|
if(args.length === 1){
|
|
schema = args[0].schema;
|
|
run = args[0].run;
|
|
}else{
|
|
schema = args[0];
|
|
run = args[1];
|
|
}
|
|
const parsedSchema = typeof schema === 'string' ? parseCommandSchema(schema) : schema;
|
|
registerCommand(this, {
|
|
schema: parsedSchema,
|
|
async run(this: CommandRunnerContext<TContext>, command: Command){
|
|
const params = command.params;
|
|
return await run.call(this.context, this.context, ...params);
|
|
},
|
|
});
|
|
|
|
type TParams = TFunc extends (ctx: TContext, ...args: infer X) => Promise<unknown> ? X : null;
|
|
type TResult = TFunc extends (ctx: TContext, ...args: any[]) => Promise<infer X> ? X : null;
|
|
return async function(ctx: TContext & CanRunParsed, ...args: TParams){
|
|
const result: CommandResult<TResult> = await ctx.runParsed({
|
|
options: {},
|
|
params: args,
|
|
flags: {},
|
|
name: parsedSchema.name,
|
|
});
|
|
if(result.success) return result.result;
|
|
throw new Error(result.error);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function createCommandRegistry<TContext>(): CommandRegistry<TContext> {
|
|
return new CommandRegistry();
|
|
}
|
|
|
|
export function registerCommand<TContext, TResult>(
|
|
registry: CommandRegistry<TContext>,
|
|
runner: CommandRunner<TContext, TResult>
|
|
): void {
|
|
registry.set(runner.schema.name, runner as CommandRunner<TContext, unknown>);
|
|
}
|
|
|
|
export function unregisterCommand<TContext>(
|
|
registry: CommandRegistry<TContext>,
|
|
name: string
|
|
): void {
|
|
registry.delete(name);
|
|
}
|
|
|
|
export function hasCommand<TContext>(
|
|
registry: CommandRegistry<TContext>,
|
|
name: string
|
|
): boolean {
|
|
return registry.has(name);
|
|
}
|
|
|
|
export function getCommand<TContext>(
|
|
registry: CommandRegistry<TContext>,
|
|
name: string
|
|
): CommandRunner<TContext, unknown> | undefined {
|
|
return registry.get(name);
|
|
}
|
|
|
|
type PromptListener = (e: PromptEvent) => void;
|
|
type PromptEndListener = () => void;
|
|
|
|
export type CommandRunnerContextExport<TContext> = CommandRunnerContext<TContext> & {
|
|
registry: CommandRegistry<TContext>;
|
|
_activePrompt: PromptEvent | null;
|
|
_tryCommit: (commandOrInput: Command | string) => string | null;
|
|
_cancel: (reason?: string) => void;
|
|
_pendingInput: string | null;
|
|
};
|
|
|
|
export function createCommandRunnerContext<TContext>(
|
|
registry: CommandRegistry<TContext>,
|
|
context: TContext
|
|
): CommandRunnerContextExport<TContext> {
|
|
const promptListeners = new Set<PromptListener>();
|
|
const promptEndListeners = new Set<PromptEndListener>();
|
|
|
|
const emitPromptEnd = () => {
|
|
for (const listener of promptEndListeners) {
|
|
listener();
|
|
}
|
|
};
|
|
|
|
const on = <T extends keyof CommandRunnerEvents>(_event: T, listener: (e: CommandRunnerEvents[T]) => void) => {
|
|
if (_event === 'prompt') {
|
|
promptListeners.add(listener as PromptListener);
|
|
} else {
|
|
promptEndListeners.add(listener as PromptEndListener);
|
|
}
|
|
};
|
|
|
|
const off = <T extends keyof CommandRunnerEvents>(_event: T, listener: (e: CommandRunnerEvents[T]) => void) => {
|
|
if (_event === 'prompt') {
|
|
promptListeners.delete(listener as PromptListener);
|
|
} else {
|
|
promptEndListeners.delete(listener as PromptEndListener);
|
|
}
|
|
};
|
|
|
|
let activePrompt: PromptEvent | null = null;
|
|
|
|
const tryCommit = (commandOrInput: Command | string) => {
|
|
if (activePrompt) {
|
|
const result = activePrompt.tryCommit(commandOrInput);
|
|
if (result === null) {
|
|
activePrompt = null;
|
|
emitPromptEnd();
|
|
}
|
|
return result;
|
|
}
|
|
return 'No active prompt';
|
|
};
|
|
|
|
const cancel = (reason?: string) => {
|
|
if (activePrompt) {
|
|
activePrompt.cancel(reason);
|
|
activePrompt = null;
|
|
emitPromptEnd();
|
|
}
|
|
};
|
|
|
|
const prompt = <TResult,TArgs extends any[]=any[]>(
|
|
schema: CommandSchema | string,
|
|
validator: PromptValidator<TResult,TArgs>,
|
|
currentPlayer?: string | null
|
|
): Promise<TResult> => {
|
|
const resolvedSchema = typeof schema === 'string' ? parseCommandSchema(schema) : schema;
|
|
return new Promise((resolve, reject) => {
|
|
const tryCommit = (commandOrInput: Command | string) => {
|
|
const command = typeof commandOrInput === 'string' ? parseCommand(commandOrInput) : commandOrInput;
|
|
const schemaResult = applyCommandSchema(command, resolvedSchema);
|
|
if (!schemaResult.valid) {
|
|
return schemaResult.errors.join('; ');
|
|
}
|
|
try{
|
|
const result = validator(...schemaResult.command.params as TArgs);
|
|
resolve(result);
|
|
return null;
|
|
}catch(e){
|
|
if(typeof e === 'string')
|
|
return e;
|
|
else
|
|
throw e;
|
|
}
|
|
};
|
|
const cancel = (reason?: string) => {
|
|
activePrompt = null;
|
|
emitPromptEnd();
|
|
reject(new Error(reason ?? 'Cancelled'));
|
|
};
|
|
activePrompt = { schema: resolvedSchema, currentPlayer: currentPlayer ?? null, tryCommit, cancel };
|
|
const event: PromptEvent = { schema: resolvedSchema, currentPlayer: currentPlayer ?? null, tryCommit, cancel };
|
|
for (const listener of promptListeners) {
|
|
listener(event);
|
|
}
|
|
});
|
|
};
|
|
|
|
const runnerCtx: CommandRunnerContextExport<TContext> = {
|
|
registry,
|
|
context,
|
|
run: <T=unknown>(input: string) => runCommandWithContext(runnerCtx, input) as Promise<CommandResult<T>>,
|
|
runParsed: <T=unknown>(command: Command) => runCommandParsedWithContext(runnerCtx, command) as Promise<CommandResult<T>>,
|
|
prompt,
|
|
on,
|
|
off,
|
|
_activePrompt: null,
|
|
_tryCommit: tryCommit,
|
|
_cancel: cancel,
|
|
_pendingInput: null,
|
|
};
|
|
|
|
Object.defineProperty(runnerCtx, '_activePrompt', {
|
|
get: () => activePrompt,
|
|
});
|
|
|
|
return runnerCtx;
|
|
}
|
|
|
|
async function executeWithRunnerContext<TContext>(
|
|
runnerCtx: CommandRunnerContextExport<TContext>,
|
|
runner: CommandRunner<TContext, unknown>,
|
|
command: Command
|
|
): Promise<CommandResult> {
|
|
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<TContext>(
|
|
registry: CommandRegistry<TContext>,
|
|
context: TContext,
|
|
input: string
|
|
): Promise<CommandResult> {
|
|
const runnerCtx = createCommandRunnerContext(registry, context);
|
|
return await runCommandWithContext(runnerCtx, input);
|
|
}
|
|
|
|
async function runCommandWithContext<TContext>(
|
|
runnerCtx: CommandRunnerContextExport<TContext>,
|
|
input: string
|
|
): Promise<{ success: true; result: unknown } | { success: false; error: string }> {
|
|
const command = parseCommand(input);
|
|
return await runCommandParsedWithContext(runnerCtx, command);
|
|
}
|
|
|
|
export async function runCommandParsed<TContext>(
|
|
registry: CommandRegistry<TContext>,
|
|
context: TContext,
|
|
command: Command
|
|
): Promise<CommandResult> {
|
|
const runnerCtx = createCommandRunnerContext(registry, context);
|
|
return await runCommandParsedWithContext(runnerCtx, command);
|
|
}
|
|
|
|
async function runCommandParsedWithContext<TContext>(
|
|
runnerCtx: CommandRunnerContextExport<TContext>,
|
|
command: Command
|
|
): Promise<CommandResult> {
|
|
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);
|
|
}
|