2026-04-02 09:33:03 +08:00
|
|
|
import type { Command, CommandSchema } from './types.js';
|
2026-04-02 10:38:31 +08:00
|
|
|
import type {CommandResult, CommandRunner, CommandRunnerContext, PromptEvent} from './command-runner.js';
|
2026-04-02 08:58:11 +08:00
|
|
|
import { parseCommand } from './command-parse.js';
|
2026-04-02 09:33:03 +08:00
|
|
|
import { applyCommandSchema } from './command-validate.js';
|
2026-04-02 09:05:47 +08:00
|
|
|
import { parseCommandSchema } from './schema-parse.js';
|
2026-04-02 14:39:30 +08:00
|
|
|
import {AsyncQueue} from "../async-queue";
|
2026-04-02 08:58:11 +08:00
|
|
|
|
|
|
|
|
export type CommandRegistry<TContext> = Map<string, CommandRunner<TContext, unknown>>;
|
|
|
|
|
|
|
|
|
|
export function createCommandRegistry<TContext>(): CommandRegistry<TContext> {
|
|
|
|
|
return new Map();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 09:05:47 +08:00
|
|
|
type Listener = (e: PromptEvent) => void;
|
|
|
|
|
|
|
|
|
|
export type CommandRunnerContextExport<TContext> = CommandRunnerContext<TContext> & {
|
|
|
|
|
registry: CommandRegistry<TContext>;
|
2026-04-02 14:39:30 +08:00
|
|
|
promptQueue: AsyncQueue<PromptEvent>;
|
2026-04-02 09:33:03 +08:00
|
|
|
_activePrompt: PromptEvent | null;
|
|
|
|
|
_resolvePrompt: (command: Command) => void;
|
|
|
|
|
_rejectPrompt: (error: Error) => void;
|
|
|
|
|
_pendingInput: string | null;
|
2026-04-02 09:05:47 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function createCommandRunnerContext<TContext>(
|
2026-04-02 08:58:11 +08:00
|
|
|
registry: CommandRegistry<TContext>,
|
2026-04-02 09:05:47 +08:00
|
|
|
context: TContext
|
|
|
|
|
): CommandRunnerContextExport<TContext> {
|
|
|
|
|
const listeners = new Set<Listener>();
|
|
|
|
|
|
|
|
|
|
const on = (_event: 'prompt', listener: Listener) => {
|
|
|
|
|
listeners.add(listener);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const off = (_event: 'prompt', listener: Listener) => {
|
|
|
|
|
listeners.delete(listener);
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-02 09:33:03 +08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-02 09:05:47 +08:00
|
|
|
const prompt = (schema: Parameters<CommandRunnerContext<TContext>['prompt']>[0]): Promise<Command> => {
|
|
|
|
|
const resolvedSchema = typeof schema === 'string' ? parseCommandSchema(schema) : schema;
|
|
|
|
|
return new Promise((resolve, reject) => {
|
2026-04-02 09:33:03 +08:00
|
|
|
activePrompt = { schema: resolvedSchema, resolve, reject };
|
2026-04-02 09:05:47 +08:00
|
|
|
const event: PromptEvent = { schema: resolvedSchema, resolve, reject };
|
|
|
|
|
for (const listener of listeners) {
|
|
|
|
|
listener(event);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const runnerCtx: CommandRunnerContextExport<TContext> = {
|
|
|
|
|
registry,
|
|
|
|
|
context,
|
2026-04-02 10:38:31 +08:00
|
|
|
run: <T=unknown>(input: string) => runCommandWithContext(runnerCtx, input) as Promise<CommandResult<T>>,
|
2026-04-02 09:33:03 +08:00
|
|
|
runParsed: (command: Command) => runCommandParsedWithContext(runnerCtx, command),
|
2026-04-02 09:05:47 +08:00
|
|
|
prompt,
|
|
|
|
|
on,
|
|
|
|
|
off,
|
2026-04-02 09:33:03 +08:00
|
|
|
_activePrompt: null,
|
|
|
|
|
_resolvePrompt: resolvePrompt,
|
|
|
|
|
_rejectPrompt: rejectPrompt,
|
|
|
|
|
_pendingInput: null,
|
2026-04-02 14:39:30 +08:00
|
|
|
promptQueue: null!
|
2026-04-02 09:05:47 +08:00
|
|
|
};
|
|
|
|
|
|
2026-04-02 09:33:03 +08:00
|
|
|
Object.defineProperty(runnerCtx, '_activePrompt', {
|
|
|
|
|
get: () => activePrompt,
|
|
|
|
|
});
|
2026-04-02 14:39:30 +08:00
|
|
|
|
|
|
|
|
let promptQueue: AsyncQueue<PromptEvent>;
|
|
|
|
|
Object.defineProperty(runnerCtx, 'promptQueue', {
|
|
|
|
|
get(){
|
|
|
|
|
if (!promptQueue) {
|
|
|
|
|
promptQueue = new AsyncQueue();
|
|
|
|
|
listeners.add(async (event) => {
|
|
|
|
|
promptQueue.push(event);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return promptQueue;
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-04-02 09:33:03 +08:00
|
|
|
|
2026-04-02 09:05:47 +08:00
|
|
|
return runnerCtx;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function executeWithRunnerContext<TContext>(
|
|
|
|
|
runnerCtx: CommandRunnerContextExport<TContext>,
|
2026-04-02 08:58:11 +08:00
|
|
|
runner: CommandRunner<TContext, unknown>,
|
|
|
|
|
command: Command
|
2026-04-02 10:38:31 +08:00
|
|
|
): Promise<CommandResult> {
|
2026-04-02 08:58:11 +08:00
|
|
|
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
|
2026-04-02 10:38:31 +08:00
|
|
|
): Promise<CommandResult> {
|
2026-04-02 09:05:47 +08:00
|
|
|
const runnerCtx = createCommandRunnerContext(registry, context);
|
2026-04-02 09:33:03 +08:00
|
|
|
return await runCommandWithContext(runnerCtx, input);
|
2026-04-02 09:05:47 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function runCommandWithContext<TContext>(
|
|
|
|
|
runnerCtx: CommandRunnerContextExport<TContext>,
|
|
|
|
|
input: string
|
2026-04-02 08:58:11 +08:00
|
|
|
): Promise<{ success: true; result: unknown } | { success: false; error: string }> {
|
|
|
|
|
const command = parseCommand(input);
|
2026-04-02 09:33:03 +08:00
|
|
|
return await runCommandParsedWithContext(runnerCtx, command);
|
2026-04-02 08:58:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function runCommandParsed<TContext>(
|
|
|
|
|
registry: CommandRegistry<TContext>,
|
|
|
|
|
context: TContext,
|
|
|
|
|
command: Command
|
2026-04-02 10:38:31 +08:00
|
|
|
): Promise<CommandResult> {
|
2026-04-02 09:05:47 +08:00
|
|
|
const runnerCtx = createCommandRunnerContext(registry, context);
|
2026-04-02 09:33:03 +08:00
|
|
|
return await runCommandParsedWithContext(runnerCtx, command);
|
2026-04-02 09:05:47 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function runCommandParsedWithContext<TContext>(
|
|
|
|
|
runnerCtx: CommandRunnerContextExport<TContext>,
|
|
|
|
|
command: Command
|
2026-04-02 10:38:31 +08:00
|
|
|
): Promise<CommandResult> {
|
2026-04-02 09:33:03 +08:00
|
|
|
const runner = runnerCtx.registry.get(command.name);
|
2026-04-02 08:58:11 +08:00
|
|
|
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('; ') };
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 09:05:47 +08:00
|
|
|
return await executeWithRunnerContext(runnerCtx, runner, validationResult.command);
|
2026-04-02 08:58:11 +08:00
|
|
|
}
|