Compare commits
4 Commits
e06dc8ecba
...
bcb31da773
| Author | SHA1 | Date |
|---|---|---|
|
|
bcb31da773 | |
|
|
3bc35df63c | |
|
|
281cbf845d | |
|
|
f1b1741db8 |
|
|
@ -71,7 +71,7 @@ function commandMatchesSchema(command: Command, schema: CommandSchema): boolean
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const requiredOptions = schema.options.filter(o => o.required);
|
const requiredOptions = Object.values(schema.options).filter(o => o.required);
|
||||||
for (const opt of requiredOptions) {
|
for (const opt of requiredOptions) {
|
||||||
const hasOption = opt.name in command.options || (opt.short && opt.short in command.options);
|
const hasOption = opt.name in command.options || (opt.short && opt.short in command.options);
|
||||||
if (!hasOption) {
|
if (!hasOption) {
|
||||||
|
|
@ -131,7 +131,7 @@ function handleGeneratorResult<T>(
|
||||||
ctx.state = 'invoking';
|
ctx.state = 'invoking';
|
||||||
return invokeChildRule(host, yielded.rule, yielded.command, ctx as RuleContext<unknown>);
|
return invokeChildRule(host, yielded.rule, yielded.command, ctx as RuleContext<unknown>);
|
||||||
} else {
|
} else {
|
||||||
ctx.schema = parseYieldedSchema({ name: '', params: [], options: [], flags: [] });
|
ctx.schema = parseYieldedSchema({ name: '', params: [], options: {}, flags: {} });
|
||||||
ctx.state = 'yielded';
|
ctx.state = 'yielded';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,9 @@ export { createRule, dispatchCommand } from './core/rule';
|
||||||
export type { Command, CommandSchema, CommandParamSchema, CommandOptionSchema, CommandFlagSchema } from './utils/command';
|
export type { Command, CommandSchema, CommandParamSchema, CommandOptionSchema, CommandFlagSchema } from './utils/command';
|
||||||
export { parseCommand, parseCommandSchema, validateCommand, parseCommandWithSchema, applyCommandSchema } from './utils/command';
|
export { parseCommand, parseCommandSchema, validateCommand, parseCommandWithSchema, applyCommandSchema } from './utils/command';
|
||||||
|
|
||||||
|
export type { CommandRunner, CommandRunnerHandler, CommandRegistry, CommandRunnerContext } from './utils/command';
|
||||||
|
export { createCommandRegistry, registerCommand, unregisterCommand, hasCommand, getCommand, runCommand, createCommandRunnerContext } from './utils/command';
|
||||||
|
|
||||||
export type { Entity, EntityAccessor } from './utils/entity';
|
export type { Entity, EntityAccessor } from './utils/entity';
|
||||||
export { createEntityCollection } from './utils/entity';
|
export { createEntityCollection } from './utils/entity';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
export { parseCommand } from './command/command-parse.js';
|
|
||||||
export { parseCommandSchema } from './command/schema-parse.js';
|
|
||||||
export { validateCommand, parseCommandWithSchema, applyCommandSchema } from './command/command-validate.js';
|
|
||||||
export type {
|
|
||||||
Command,
|
|
||||||
CommandParamSchema,
|
|
||||||
CommandOptionSchema,
|
|
||||||
CommandFlagSchema,
|
|
||||||
CommandSchema,
|
|
||||||
} from './command/types.js';
|
|
||||||
|
|
@ -19,7 +19,7 @@ function validateCommandCore(command: Command, schema: CommandSchema): string[]
|
||||||
errors.push(`参数过多:最多 ${schema.params.length} 个参数,实际 ${command.params.length} 个`);
|
errors.push(`参数过多:最多 ${schema.params.length} 个参数,实际 ${command.params.length} 个`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const requiredOptions = schema.options.filter(o => o.required);
|
const requiredOptions = Object.values(schema.options).filter(o => o.required);
|
||||||
for (const opt of requiredOptions) {
|
for (const opt of requiredOptions) {
|
||||||
const hasOption = opt.name in command.options || (opt.short && opt.short in command.options);
|
const hasOption = opt.name in command.options || (opt.short && opt.short in command.options);
|
||||||
if (!hasOption) {
|
if (!hasOption) {
|
||||||
|
|
@ -58,7 +58,7 @@ export function applyCommandSchema(
|
||||||
|
|
||||||
const parsedOptions: Record<string, unknown> = { ...command.options };
|
const parsedOptions: Record<string, unknown> = { ...command.options };
|
||||||
for (const [key, value] of Object.entries(command.options)) {
|
for (const [key, value] of Object.entries(command.options)) {
|
||||||
const optSchema = schema.options.find(o => o.name === key || o.short === key);
|
const optSchema = schema.options[key] ?? (key.length === 1 ? Object.values(schema.options).find(o => o.short === key) : undefined);
|
||||||
if (optSchema?.schema && typeof value === 'string') {
|
if (optSchema?.schema && typeof value === 'string') {
|
||||||
try {
|
try {
|
||||||
parsedOptions[key] = optSchema.schema.parse(value);
|
parsedOptions[key] = optSchema.schema.parse(value);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
import type { Command } from './types.js';
|
||||||
|
import type { CommandRunner, CommandRunnerContext, PromptEvent } from './command-runner.js';
|
||||||
|
import { parseCommand } from './command-parse.js';
|
||||||
|
import { applyCommandSchema } from './command-apply.js';
|
||||||
|
import { parseCommandSchema } from './schema-parse.js';
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
type Listener = (e: PromptEvent) => void;
|
||||||
|
|
||||||
|
export type CommandRunnerContextExport<TContext> = CommandRunnerContext<TContext> & {
|
||||||
|
registry: CommandRegistry<TContext>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createCommandRunnerContext<TContext>(
|
||||||
|
registry: CommandRegistry<TContext>,
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
const prompt = (schema: Parameters<CommandRunnerContext<TContext>['prompt']>[0]): Promise<Command> => {
|
||||||
|
const resolvedSchema = typeof schema === 'string' ? parseCommandSchema(schema) : schema;
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const event: PromptEvent = { schema: resolvedSchema, resolve, reject };
|
||||||
|
for (const listener of listeners) {
|
||||||
|
listener(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const runnerCtx: CommandRunnerContextExport<TContext> = {
|
||||||
|
registry,
|
||||||
|
context,
|
||||||
|
run: (input: string) => runCommandWithContext(registry, runnerCtx, input),
|
||||||
|
runParsed: (command: Command) => runCommandParsedWithContext(registry, runnerCtx, command),
|
||||||
|
prompt,
|
||||||
|
on,
|
||||||
|
off,
|
||||||
|
};
|
||||||
|
|
||||||
|
return runnerCtx;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeWithRunnerContext<TContext>(
|
||||||
|
runnerCtx: CommandRunnerContextExport<TContext>,
|
||||||
|
runner: CommandRunner<TContext, unknown>,
|
||||||
|
command: Command
|
||||||
|
): Promise<{ success: true; result: unknown } | { success: false; error: string }> {
|
||||||
|
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<{ success: true; result: unknown } | { success: false; error: string }> {
|
||||||
|
const runnerCtx = createCommandRunnerContext(registry, context);
|
||||||
|
return await runCommandWithContext(registry, runnerCtx, input);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runCommandWithContext<TContext>(
|
||||||
|
registry: CommandRegistry<TContext>,
|
||||||
|
runnerCtx: CommandRunnerContextExport<TContext>,
|
||||||
|
input: string
|
||||||
|
): Promise<{ success: true; result: unknown } | { success: false; error: string }> {
|
||||||
|
const command = parseCommand(input);
|
||||||
|
return await runCommandParsedWithContext(registry, runnerCtx, command);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runCommandParsed<TContext>(
|
||||||
|
registry: CommandRegistry<TContext>,
|
||||||
|
context: TContext,
|
||||||
|
command: Command
|
||||||
|
): Promise<{ success: true; result: unknown } | { success: false; error: string }> {
|
||||||
|
const runnerCtx = createCommandRunnerContext(registry, context);
|
||||||
|
return await runCommandParsedWithContext(registry, runnerCtx, command);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runCommandParsedWithContext<TContext>(
|
||||||
|
registry: CommandRegistry<TContext>,
|
||||||
|
runnerCtx: CommandRunnerContextExport<TContext>,
|
||||||
|
command: Command
|
||||||
|
): Promise<{ success: true; result: unknown } | { success: false; error: string }> {
|
||||||
|
const runner = 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);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
import type { Command, CommandSchema } from './types.js';
|
||||||
|
|
||||||
|
export type PromptEvent = {
|
||||||
|
schema: CommandSchema;
|
||||||
|
resolve: (command: Command) => void;
|
||||||
|
reject: (error: Error) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CommandRunnerEvents = {
|
||||||
|
prompt: PromptEvent;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CommandRunnerContext<TContext> = {
|
||||||
|
context: TContext;
|
||||||
|
run: (input: string) => Promise<{ success: true; result: unknown } | { success: false; error: string }>;
|
||||||
|
runParsed: (command: Command) => Promise<{ success: true; result: unknown } | { success: false; error: string }>;
|
||||||
|
prompt: (schema: CommandSchema | string) => Promise<Command>;
|
||||||
|
on: <T extends keyof CommandRunnerEvents>(event: T, listener: (e: CommandRunnerEvents[T]) => void) => void;
|
||||||
|
off: <T extends keyof CommandRunnerEvents>(event: T, listener: (e: CommandRunnerEvents[T]) => void) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CommandRunnerHandler<TContext, TResult> = (
|
||||||
|
this: CommandRunnerContext<TContext>,
|
||||||
|
command: Command
|
||||||
|
) => Promise<TResult>;
|
||||||
|
|
||||||
|
export type CommandRunner<TContext, TResult = unknown> = {
|
||||||
|
schema: CommandSchema;
|
||||||
|
run: CommandRunnerHandler<TContext, TResult>;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
export { parseCommand } from './command-parse';
|
||||||
|
export { parseCommandSchema } from './schema-parse';
|
||||||
|
export { validateCommand, parseCommandWithSchema, applyCommandSchema } from './command-validate';
|
||||||
|
export {
|
||||||
|
createCommandRegistry,
|
||||||
|
registerCommand,
|
||||||
|
unregisterCommand,
|
||||||
|
hasCommand,
|
||||||
|
getCommand,
|
||||||
|
runCommand,
|
||||||
|
runCommandParsed,
|
||||||
|
createCommandRunnerContext,
|
||||||
|
} from './command-registry';
|
||||||
|
export type {
|
||||||
|
Command,
|
||||||
|
CommandParamSchema,
|
||||||
|
CommandOptionSchema,
|
||||||
|
CommandFlagSchema,
|
||||||
|
CommandSchema,
|
||||||
|
} from './types';
|
||||||
|
export type { CommandRunner, CommandRunnerHandler, CommandRunnerContext, PromptEvent, CommandRunnerEvents } from './command-runner';
|
||||||
|
export type { CommandRegistry, CommandRunnerContextExport } from './command-registry';
|
||||||
|
|
@ -5,8 +5,8 @@ export function parseCommandSchema(schemaStr: string, name?: string): CommandSch
|
||||||
const schema: CommandSchema = {
|
const schema: CommandSchema = {
|
||||||
name: name ?? '',
|
name: name ?? '',
|
||||||
params: [],
|
params: [],
|
||||||
options: [],
|
options: {},
|
||||||
flags: [],
|
flags: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const tokens = tokenizeSchema(schemaStr);
|
const tokens = tokenizeSchema(schemaStr);
|
||||||
|
|
@ -27,28 +27,28 @@ export function parseCommandSchema(schemaStr: string, name?: string): CommandSch
|
||||||
if (inner.startsWith('--')) {
|
if (inner.startsWith('--')) {
|
||||||
const result = parseOptionToken(inner.slice(2), false);
|
const result = parseOptionToken(inner.slice(2), false);
|
||||||
if (result.isFlag) {
|
if (result.isFlag) {
|
||||||
schema.flags.push({ name: result.name, short: result.short });
|
schema.flags[result.name] = { name: result.name, short: result.short };
|
||||||
} else {
|
} else {
|
||||||
schema.options.push({
|
schema.options[result.name] = {
|
||||||
name: result.name,
|
name: result.name,
|
||||||
short: result.short,
|
short: result.short,
|
||||||
required: false,
|
required: false,
|
||||||
defaultValue: result.defaultValue,
|
defaultValue: result.defaultValue,
|
||||||
schema: result.schema,
|
schema: result.schema,
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
} else if (inner.startsWith('-') && inner.length > 1 && !inner.includes('--')) {
|
} else if (inner.startsWith('-') && inner.length > 1 && !inner.includes('--')) {
|
||||||
const result = parseOptionToken(inner.slice(1), false);
|
const result = parseOptionToken(inner.slice(1), false);
|
||||||
if (result.isFlag) {
|
if (result.isFlag) {
|
||||||
schema.flags.push({ name: result.name, short: result.short || result.name });
|
schema.flags[result.name] = { name: result.name, short: result.short || result.name };
|
||||||
} else {
|
} else {
|
||||||
schema.options.push({
|
schema.options[result.name] = {
|
||||||
name: result.name,
|
name: result.name,
|
||||||
short: result.short || result.name,
|
short: result.short || result.name,
|
||||||
required: false,
|
required: false,
|
||||||
defaultValue: result.defaultValue,
|
defaultValue: result.defaultValue,
|
||||||
schema: result.schema,
|
schema: result.schema,
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const isVariadic = inner.endsWith('...');
|
const isVariadic = inner.endsWith('...');
|
||||||
|
|
@ -78,29 +78,29 @@ export function parseCommandSchema(schemaStr: string, name?: string): CommandSch
|
||||||
} else if (token.startsWith('--')) {
|
} else if (token.startsWith('--')) {
|
||||||
const result = parseOptionToken(token.slice(2), true);
|
const result = parseOptionToken(token.slice(2), true);
|
||||||
if (result.isFlag) {
|
if (result.isFlag) {
|
||||||
schema.flags.push({ name: result.name, short: result.short });
|
schema.flags[result.name] = { name: result.name, short: result.short };
|
||||||
} else {
|
} else {
|
||||||
schema.options.push({
|
schema.options[result.name] = {
|
||||||
name: result.name,
|
name: result.name,
|
||||||
short: result.short,
|
short: result.short,
|
||||||
required: true,
|
required: true,
|
||||||
defaultValue: result.defaultValue,
|
defaultValue: result.defaultValue,
|
||||||
schema: result.schema,
|
schema: result.schema,
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
i++;
|
i++;
|
||||||
} else if (token.startsWith('-') && token.length > 1 && !/^-?\d+$/.test(token)) {
|
} else if (token.startsWith('-') && token.length > 1 && !/^-?\d+$/.test(token)) {
|
||||||
const result = parseOptionToken(token.slice(1), true);
|
const result = parseOptionToken(token.slice(1), true);
|
||||||
if (result.isFlag) {
|
if (result.isFlag) {
|
||||||
schema.flags.push({ name: result.name, short: result.short || result.name });
|
schema.flags[result.name] = { name: result.name, short: result.short || result.name };
|
||||||
} else {
|
} else {
|
||||||
schema.options.push({
|
schema.options[result.name] = {
|
||||||
name: result.name,
|
name: result.name,
|
||||||
short: result.short || result.name,
|
short: result.short || result.name,
|
||||||
required: true,
|
required: true,
|
||||||
defaultValue: result.defaultValue,
|
defaultValue: result.defaultValue,
|
||||||
schema: result.schema,
|
schema: result.schema,
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
i++;
|
i++;
|
||||||
} else if (token.startsWith('<') && token.endsWith('>')) {
|
} else if (token.startsWith('<') && token.endsWith('>')) {
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,8 @@ export type CommandFlagSchema = {
|
||||||
export type CommandSchema = {
|
export type CommandSchema = {
|
||||||
name: string;
|
name: string;
|
||||||
params: CommandParamSchema[];
|
params: CommandParamSchema[];
|
||||||
options: CommandOptionSchema[];
|
options: Record<string, CommandOptionSchema>;
|
||||||
flags: CommandFlagSchema[];
|
flags: Record<string, CommandFlagSchema>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ParsedOptionResult {
|
export interface ParsedOptionResult {
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,8 @@ describe('Rule System', () => {
|
||||||
expect(rule.schema.params[0].required).toBe(true);
|
expect(rule.schema.params[0].required).toBe(true);
|
||||||
expect(rule.schema.params[1].name).toBe('to');
|
expect(rule.schema.params[1].name).toBe('to');
|
||||||
expect(rule.schema.params[1].required).toBe(true);
|
expect(rule.schema.params[1].required).toBe(true);
|
||||||
expect(rule.schema.flags).toHaveLength(1);
|
expect(Object.keys(rule.schema.flags)).toHaveLength(1);
|
||||||
expect(rule.schema.flags[0].name).toBe('force');
|
expect(rule.schema.flags.force.name).toBe('force');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create a generator when called', () => {
|
it('should create a generator when called', () => {
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,11 @@ describe('parseCommandSchema with inline-schema', () => {
|
||||||
it('should parse schema with typed options', () => {
|
it('should parse schema with typed options', () => {
|
||||||
const schema = parseCommandSchema('move <from> <to> [--all: boolean] [--count: number]');
|
const schema = parseCommandSchema('move <from> <to> [--all: boolean] [--count: number]');
|
||||||
expect(schema.name).toBe('move');
|
expect(schema.name).toBe('move');
|
||||||
expect(schema.flags).toHaveLength(1);
|
expect(Object.keys(schema.flags)).toHaveLength(1);
|
||||||
expect(schema.options).toHaveLength(1);
|
expect(Object.keys(schema.options)).toHaveLength(1);
|
||||||
expect(schema.flags[0].name).toBe('all');
|
expect(schema.flags.all.name).toBe('all');
|
||||||
expect(schema.options[0].name).toBe('count');
|
expect(schema.options.count.name).toBe('count');
|
||||||
expect(schema.options[0].schema).toBeDefined();
|
expect(schema.options.count.schema).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse schema with tuple type', () => {
|
it('should parse schema with tuple type', () => {
|
||||||
|
|
@ -58,7 +58,7 @@ describe('parseCommandSchema with inline-schema', () => {
|
||||||
);
|
);
|
||||||
expect(schema.name).toBe('move');
|
expect(schema.name).toBe('move');
|
||||||
expect(schema.params).toHaveLength(2);
|
expect(schema.params).toHaveLength(2);
|
||||||
expect(schema.options).toHaveLength(1);
|
expect(Object.keys(schema.options)).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse schema with optional typed param', () => {
|
it('should parse schema with optional typed param', () => {
|
||||||
|
|
@ -73,9 +73,9 @@ describe('parseCommandSchema with inline-schema', () => {
|
||||||
it('should parse schema with optional typed option', () => {
|
it('should parse schema with optional typed option', () => {
|
||||||
const schema = parseCommandSchema('move <from> [--speed: number]');
|
const schema = parseCommandSchema('move <from> [--speed: number]');
|
||||||
expect(schema.name).toBe('move');
|
expect(schema.name).toBe('move');
|
||||||
expect(schema.options).toHaveLength(1);
|
expect(Object.keys(schema.options)).toHaveLength(1);
|
||||||
expect(schema.options[0].required).toBe(false);
|
expect(schema.options.speed.required).toBe(false);
|
||||||
expect(schema.options[0].schema).toBeDefined();
|
expect(schema.options.speed.schema).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,427 @@
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { parseCommandSchema } from '../../src/utils/command/schema-parse';
|
||||||
|
import {
|
||||||
|
createCommandRegistry,
|
||||||
|
registerCommand,
|
||||||
|
unregisterCommand,
|
||||||
|
hasCommand,
|
||||||
|
getCommand,
|
||||||
|
runCommand,
|
||||||
|
createCommandRunnerContext,
|
||||||
|
type CommandRegistry,
|
||||||
|
type CommandRunnerContextExport,
|
||||||
|
} from '../../src/utils/command/command-registry';
|
||||||
|
import type { CommandRunner, PromptEvent } from '../../src/utils/command/command-runner';
|
||||||
|
|
||||||
|
type TestContext = {
|
||||||
|
counter: number;
|
||||||
|
log: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('CommandRegistry', () => {
|
||||||
|
it('should create an empty registry', () => {
|
||||||
|
const registry = createCommandRegistry<TestContext>();
|
||||||
|
expect(registry.size).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should register a command', () => {
|
||||||
|
const registry = createCommandRegistry<TestContext>();
|
||||||
|
const runner: CommandRunner<TestContext, number> = {
|
||||||
|
schema: parseCommandSchema('add <a> <b>'),
|
||||||
|
run: async function (cmd) {
|
||||||
|
return Number(cmd.params[0]) + Number(cmd.params[1]);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
registerCommand(registry, runner);
|
||||||
|
expect(registry.size).toBe(1);
|
||||||
|
expect(hasCommand(registry, 'add')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should unregister a command', () => {
|
||||||
|
const registry = createCommandRegistry<TestContext>();
|
||||||
|
const runner: CommandRunner<TestContext> = {
|
||||||
|
schema: parseCommandSchema('remove'),
|
||||||
|
run: async () => {},
|
||||||
|
};
|
||||||
|
registerCommand(registry, runner);
|
||||||
|
expect(hasCommand(registry, 'remove')).toBe(true);
|
||||||
|
unregisterCommand(registry, 'remove');
|
||||||
|
expect(hasCommand(registry, 'remove')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get a command runner', () => {
|
||||||
|
const registry = createCommandRegistry<TestContext>();
|
||||||
|
const runner: CommandRunner<TestContext> = {
|
||||||
|
schema: parseCommandSchema('get'),
|
||||||
|
run: async () => {},
|
||||||
|
};
|
||||||
|
registerCommand(registry, runner);
|
||||||
|
const retrieved = getCommand(registry, 'get');
|
||||||
|
expect(retrieved).toBe(runner);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined for unknown command', () => {
|
||||||
|
const registry = createCommandRegistry<TestContext>();
|
||||||
|
const retrieved = getCommand(registry, 'unknown');
|
||||||
|
expect(retrieved).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('runCommand', () => {
|
||||||
|
it('should run a command successfully', async () => {
|
||||||
|
const registry = createCommandRegistry<TestContext>();
|
||||||
|
const runner: CommandRunner<TestContext, number> = {
|
||||||
|
schema: parseCommandSchema('add <a> <b>'),
|
||||||
|
run: async function (cmd) {
|
||||||
|
return Number(cmd.params[0]) + Number(cmd.params[1]);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
registerCommand(registry, runner);
|
||||||
|
|
||||||
|
const result = await runCommand(registry, { counter: 0, log: [] }, 'add 1 2');
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.result).toBe(3);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail for unknown command', async () => {
|
||||||
|
const registry = createCommandRegistry<TestContext>();
|
||||||
|
const result = await runCommand(registry, { counter: 0, log: [] }, 'unknown');
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
if (!result.success) {
|
||||||
|
expect(result.error).toContain('Unknown command');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail for invalid command params', async () => {
|
||||||
|
const registry = createCommandRegistry<TestContext>();
|
||||||
|
const runner: CommandRunner<TestContext> = {
|
||||||
|
schema: parseCommandSchema('add <a> <b>'),
|
||||||
|
run: async () => {},
|
||||||
|
};
|
||||||
|
registerCommand(registry, runner);
|
||||||
|
|
||||||
|
const result = await runCommand(registry, { counter: 0, log: [] }, 'add 1');
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
if (!result.success) {
|
||||||
|
expect(result.error).toContain('参数不足');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should access context via this.context', async () => {
|
||||||
|
const registry = createCommandRegistry<TestContext>();
|
||||||
|
const runner: CommandRunner<TestContext, number> = {
|
||||||
|
schema: parseCommandSchema('increment'),
|
||||||
|
run: async function () {
|
||||||
|
this.context.counter++;
|
||||||
|
return this.context.counter;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
registerCommand(registry, runner);
|
||||||
|
|
||||||
|
const ctx = { counter: 0, log: [] };
|
||||||
|
await runCommand(registry, ctx, 'increment');
|
||||||
|
expect(ctx.counter).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle async errors', async () => {
|
||||||
|
const registry = createCommandRegistry<TestContext>();
|
||||||
|
const runner: CommandRunner<TestContext> = {
|
||||||
|
schema: parseCommandSchema('fail'),
|
||||||
|
run: async () => {
|
||||||
|
throw new Error('Something went wrong');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
registerCommand(registry, runner);
|
||||||
|
|
||||||
|
const result = await runCommand(registry, { counter: 0, log: [] }, 'fail');
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
if (!result.success) {
|
||||||
|
expect(result.error).toBe('Something went wrong');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('CommandRunnerContext', () => {
|
||||||
|
it('should create a runner context', () => {
|
||||||
|
const registry = createCommandRegistry<TestContext>();
|
||||||
|
const ctx = { counter: 0, log: [] };
|
||||||
|
const runnerCtx = createCommandRunnerContext(registry, ctx);
|
||||||
|
expect(runnerCtx.registry).toBe(registry);
|
||||||
|
expect(runnerCtx.context).toBe(ctx);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should run commands via runner context', async () => {
|
||||||
|
const registry = createCommandRegistry<TestContext>();
|
||||||
|
const runner: CommandRunner<TestContext, string> = {
|
||||||
|
schema: parseCommandSchema('greet <name>'),
|
||||||
|
run: async function (cmd) {
|
||||||
|
this.context.log.push(`Hello, ${cmd.params[0]}!`);
|
||||||
|
return `Hello, ${cmd.params[0]}!`;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
registerCommand(registry, runner);
|
||||||
|
|
||||||
|
const ctx = { counter: 0, log: [] };
|
||||||
|
const runnerCtx = createCommandRunnerContext(registry, ctx);
|
||||||
|
const result = await runnerCtx.run('greet World');
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.result).toBe('Hello, World!');
|
||||||
|
}
|
||||||
|
expect(ctx.log).toEqual(['Hello, World!']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow commands to call other commands via this.run', async () => {
|
||||||
|
const registry = createCommandRegistry<TestContext>();
|
||||||
|
|
||||||
|
const addRunner: CommandRunner<TestContext, number> = {
|
||||||
|
schema: parseCommandSchema('add <a> <b>'),
|
||||||
|
run: async function (cmd) {
|
||||||
|
return Number(cmd.params[0]) + Number(cmd.params[1]);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
registerCommand(registry, addRunner);
|
||||||
|
|
||||||
|
const multiplyRunner: CommandRunner<TestContext, number> = {
|
||||||
|
schema: parseCommandSchema('multiply <a> <b>'),
|
||||||
|
run: async function (cmd) {
|
||||||
|
const a = Number(cmd.params[0]);
|
||||||
|
const b = Number(cmd.params[1]);
|
||||||
|
const addResult = await this.run(`add ${a} ${a}`);
|
||||||
|
if (!addResult.success) throw new Error('add failed');
|
||||||
|
return (addResult.result as number) * b;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
registerCommand(registry, multiplyRunner);
|
||||||
|
|
||||||
|
const ctx = { counter: 0, log: [] };
|
||||||
|
const result = await runCommand(registry, ctx, 'multiply 3 4');
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.result).toBe(24);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow commands to call other commands via this.runParsed', async () => {
|
||||||
|
const registry = createCommandRegistry<TestContext>();
|
||||||
|
|
||||||
|
const doubleRunner: CommandRunner<TestContext, number> = {
|
||||||
|
schema: parseCommandSchema('double <n>'),
|
||||||
|
run: async function (cmd) {
|
||||||
|
return Number(cmd.params[0]) * 2;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
registerCommand(registry, doubleRunner);
|
||||||
|
|
||||||
|
const quadrupleRunner: CommandRunner<TestContext, number> = {
|
||||||
|
schema: parseCommandSchema('quadruple <n>'),
|
||||||
|
run: async function (cmd) {
|
||||||
|
const n = Number(cmd.params[0]);
|
||||||
|
const doubleResult = await this.runParsed({ name: 'double', params: [String(n)], options: {}, flags: {} });
|
||||||
|
if (!doubleResult.success) throw new Error('double failed');
|
||||||
|
return (doubleResult.result as number) * 2;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
registerCommand(registry, quadrupleRunner);
|
||||||
|
|
||||||
|
const ctx = { counter: 0, log: [] };
|
||||||
|
const result = await runCommand(registry, ctx, 'quadruple 5');
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.result).toBe(20);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('prompt', () => {
|
||||||
|
it('should dispatch prompt event with string schema', async () => {
|
||||||
|
const registry = createCommandRegistry<TestContext>();
|
||||||
|
|
||||||
|
const chooseRunner: CommandRunner<TestContext, string> = {
|
||||||
|
schema: parseCommandSchema('choose'),
|
||||||
|
run: async function () {
|
||||||
|
const result = await this.prompt('select <card>');
|
||||||
|
return result.params[0] as string;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
registerCommand(registry, chooseRunner);
|
||||||
|
|
||||||
|
const ctx = { counter: 0, log: [] };
|
||||||
|
let promptEvent: PromptEvent | null = null;
|
||||||
|
|
||||||
|
const runnerCtx = createCommandRunnerContext(registry, ctx);
|
||||||
|
runnerCtx.on('prompt', (e) => {
|
||||||
|
promptEvent = e;
|
||||||
|
});
|
||||||
|
|
||||||
|
const runPromise = runnerCtx.run('choose');
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
expect(promptEvent).not.toBeNull();
|
||||||
|
expect(promptEvent!.schema.name).toBe('select');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve prompt with valid input', async () => {
|
||||||
|
const registry = createCommandRegistry<TestContext>();
|
||||||
|
|
||||||
|
const chooseRunner: CommandRunner<TestContext, string> = {
|
||||||
|
schema: parseCommandSchema('choose'),
|
||||||
|
run: async function () {
|
||||||
|
const result = await this.prompt('select <card>');
|
||||||
|
this.context.log.push(`selected ${result.params[0]}`);
|
||||||
|
return result.params[0] as string;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
registerCommand(registry, chooseRunner);
|
||||||
|
|
||||||
|
const ctx = { counter: 0, log: [] };
|
||||||
|
let promptEvent: PromptEvent | null = null;
|
||||||
|
|
||||||
|
const runnerCtx = createCommandRunnerContext(registry, ctx);
|
||||||
|
runnerCtx.on('prompt', (e) => {
|
||||||
|
promptEvent = e;
|
||||||
|
});
|
||||||
|
|
||||||
|
const runPromise = runnerCtx.run('choose');
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
expect(promptEvent).not.toBeNull();
|
||||||
|
|
||||||
|
const parsed = { name: 'select', params: ['Ace'], options: {}, flags: {} };
|
||||||
|
promptEvent!.resolve(parsed);
|
||||||
|
|
||||||
|
const result = await runPromise;
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.result).toBe('Ace');
|
||||||
|
}
|
||||||
|
expect(ctx.log).toEqual(['selected Ace']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject prompt with invalid input', async () => {
|
||||||
|
const registry = createCommandRegistry<TestContext>();
|
||||||
|
|
||||||
|
const chooseRunner: CommandRunner<TestContext, string> = {
|
||||||
|
schema: parseCommandSchema('choose'),
|
||||||
|
run: async function () {
|
||||||
|
try {
|
||||||
|
await this.prompt('select <card>');
|
||||||
|
return 'unexpected success';
|
||||||
|
} catch (e) {
|
||||||
|
return (e as Error).message;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
registerCommand(registry, chooseRunner);
|
||||||
|
|
||||||
|
const ctx = { counter: 0, log: [] };
|
||||||
|
let promptEvent: PromptEvent | null = null;
|
||||||
|
|
||||||
|
const runnerCtx = createCommandRunnerContext(registry, ctx);
|
||||||
|
runnerCtx.on('prompt', (e) => {
|
||||||
|
promptEvent = e;
|
||||||
|
});
|
||||||
|
|
||||||
|
const runPromise = runnerCtx.run('choose');
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
expect(promptEvent).not.toBeNull();
|
||||||
|
|
||||||
|
promptEvent!.reject(new Error('user cancelled'));
|
||||||
|
|
||||||
|
const result = await runPromise;
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.result).toBe('user cancelled');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept CommandSchema object in prompt', async () => {
|
||||||
|
const registry = createCommandRegistry<TestContext>();
|
||||||
|
const schema = parseCommandSchema('pick <item>');
|
||||||
|
|
||||||
|
const pickRunner: CommandRunner<TestContext, string> = {
|
||||||
|
schema: parseCommandSchema('pick'),
|
||||||
|
run: async function () {
|
||||||
|
const result = await this.prompt(schema);
|
||||||
|
return result.params[0] as string;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
registerCommand(registry, pickRunner);
|
||||||
|
|
||||||
|
const ctx = { counter: 0, log: [] };
|
||||||
|
let promptEvent: PromptEvent | null = null;
|
||||||
|
|
||||||
|
const runnerCtx = createCommandRunnerContext(registry, ctx);
|
||||||
|
runnerCtx.on('prompt', (e) => {
|
||||||
|
promptEvent = e;
|
||||||
|
});
|
||||||
|
|
||||||
|
const runPromise = runnerCtx.run('pick');
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
expect(promptEvent).not.toBeNull();
|
||||||
|
expect(promptEvent!.schema.name).toBe('pick');
|
||||||
|
|
||||||
|
promptEvent!.resolve({ name: 'pick', params: ['sword'], options: {}, flags: {} });
|
||||||
|
|
||||||
|
const result = await runPromise;
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.result).toBe('sword');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow multiple sequential prompts', async () => {
|
||||||
|
const registry = createCommandRegistry<TestContext>();
|
||||||
|
|
||||||
|
const multiPromptRunner: CommandRunner<TestContext, string[]> = {
|
||||||
|
schema: parseCommandSchema('multi'),
|
||||||
|
run: async function () {
|
||||||
|
const first = await this.prompt('first <a>');
|
||||||
|
const second = await this.prompt('second <b>');
|
||||||
|
return [first.params[0] as string, second.params[0] as string];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
registerCommand(registry, multiPromptRunner);
|
||||||
|
|
||||||
|
const ctx = { counter: 0, log: [] };
|
||||||
|
const promptEvents: PromptEvent[] = [];
|
||||||
|
|
||||||
|
const runnerCtx = createCommandRunnerContext(registry, ctx);
|
||||||
|
runnerCtx.on('prompt', (e) => {
|
||||||
|
promptEvents.push(e);
|
||||||
|
});
|
||||||
|
|
||||||
|
const runPromise = runnerCtx.run('multi');
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
expect(promptEvents.length).toBe(1);
|
||||||
|
expect(promptEvents[0].schema.name).toBe('first');
|
||||||
|
|
||||||
|
promptEvents[0].resolve({ name: 'first', params: ['one'], options: {}, flags: {} });
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
expect(promptEvents.length).toBe(2);
|
||||||
|
expect(promptEvents[1].schema.name).toBe('second');
|
||||||
|
|
||||||
|
promptEvents[1].resolve({ name: 'second', params: ['two'], options: {}, flags: {} });
|
||||||
|
|
||||||
|
const result = await runPromise;
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.result).toEqual(['one', 'two']);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -7,18 +7,8 @@ describe('parseCommandSchema', () => {
|
||||||
expect(schema).toEqual({
|
expect(schema).toEqual({
|
||||||
name: '',
|
name: '',
|
||||||
params: [],
|
params: [],
|
||||||
options: [],
|
options: {},
|
||||||
flags: [],
|
flags: {},
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should parse command name only', () => {
|
|
||||||
const schema = parseCommandSchema('move');
|
|
||||||
expect(schema).toEqual({
|
|
||||||
name: 'move',
|
|
||||||
params: [],
|
|
||||||
options: [],
|
|
||||||
flags: [],
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -55,34 +45,36 @@ describe('parseCommandSchema', () => {
|
||||||
|
|
||||||
it('should parse long flags', () => {
|
it('should parse long flags', () => {
|
||||||
const schema = parseCommandSchema('move [--force] [--quiet]');
|
const schema = parseCommandSchema('move [--force] [--quiet]');
|
||||||
expect(schema.flags).toEqual([
|
expect(schema.flags).toEqual({
|
||||||
{ name: 'force' },
|
force: { name: 'force', short: undefined },
|
||||||
{ name: 'quiet' },
|
quiet: { name: 'quiet', short: undefined },
|
||||||
]);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse short flags', () => {
|
it('should parse short flags', () => {
|
||||||
const schema = parseCommandSchema('move [-f] [-q]');
|
const schema = parseCommandSchema('move [-f] [-q]');
|
||||||
expect(schema.flags).toEqual([
|
expect(schema.flags).toEqual({
|
||||||
{ name: 'f', short: 'f' },
|
f: { name: 'f', short: 'f' },
|
||||||
{ name: 'q', short: 'q' },
|
q: { name: 'q', short: 'q' },
|
||||||
]);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse long options', () => {
|
it('should parse long options', () => {
|
||||||
const schema = parseCommandSchema('move --x: string [--y: string]');
|
const schema = parseCommandSchema('move --x: string [--y: string]');
|
||||||
expect(schema.options).toEqual([
|
expect(Object.keys(schema.options)).toEqual(['x', 'y']);
|
||||||
{ name: 'x', required: true, schema: expect.any(Object) },
|
expect(schema.options.x).toMatchObject({ name: 'x', required: true });
|
||||||
{ name: 'y', required: false, schema: expect.any(Object) },
|
expect(schema.options.x.schema).toBeDefined();
|
||||||
]);
|
expect(schema.options.y).toMatchObject({ name: 'y', required: false });
|
||||||
|
expect(schema.options.y.schema).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse short options', () => {
|
it('should parse short options', () => {
|
||||||
const schema = parseCommandSchema('move -x: string [-y: string]');
|
const schema = parseCommandSchema('move -x: string [-y: string]');
|
||||||
expect(schema.options).toEqual([
|
expect(Object.keys(schema.options)).toEqual(['x', 'y']);
|
||||||
{ name: 'x', short: 'x', required: true, schema: expect.any(Object) },
|
expect(schema.options.x).toMatchObject({ name: 'x', short: 'x', required: true });
|
||||||
{ name: 'y', short: 'y', required: false, schema: expect.any(Object) },
|
expect(schema.options.x.schema).toBeDefined();
|
||||||
]);
|
expect(schema.options.y).toMatchObject({ name: 'y', short: 'y', required: false });
|
||||||
|
expect(schema.options.y.schema).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse mixed schema', () => {
|
it('should parse mixed schema', () => {
|
||||||
|
|
@ -93,13 +85,13 @@ describe('parseCommandSchema', () => {
|
||||||
{ name: 'from', required: true, variadic: false, schema: undefined },
|
{ name: 'from', required: true, variadic: false, schema: undefined },
|
||||||
{ name: 'to', required: true, variadic: false, schema: undefined },
|
{ name: 'to', required: true, variadic: false, schema: undefined },
|
||||||
],
|
],
|
||||||
flags: [
|
flags: {
|
||||||
{ name: 'force' },
|
force: { name: 'force', short: undefined },
|
||||||
{ name: 'f', short: 'f' },
|
f: { name: 'f', short: 'f' },
|
||||||
],
|
},
|
||||||
options: [
|
options: {
|
||||||
{ name: 'speed', short: 's', required: false, schema: expect.any(Object), defaultValue: undefined },
|
speed: { name: 'speed', short: 's', required: false, schema: expect.any(Object), defaultValue: undefined },
|
||||||
],
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -107,8 +99,8 @@ describe('parseCommandSchema', () => {
|
||||||
const schema = parseCommandSchema('place <piece> <region> [x...] [--rotate: number] [--force] [-f]');
|
const schema = parseCommandSchema('place <piece> <region> [x...] [--rotate: number] [--force] [-f]');
|
||||||
expect(schema.name).toBe('place');
|
expect(schema.name).toBe('place');
|
||||||
expect(schema.params).toHaveLength(3);
|
expect(schema.params).toHaveLength(3);
|
||||||
expect(schema.flags).toHaveLength(2);
|
expect(Object.keys(schema.flags)).toHaveLength(2);
|
||||||
expect(schema.options).toHaveLength(1);
|
expect(Object.keys(schema.options)).toHaveLength(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -237,39 +229,8 @@ describe('integration', () => {
|
||||||
|
|
||||||
it('should parse short alias syntax', () => {
|
it('should parse short alias syntax', () => {
|
||||||
const schema = parseCommandSchema('move <from> [--verbose: boolean -v]');
|
const schema = parseCommandSchema('move <from> [--verbose: boolean -v]');
|
||||||
expect(schema.flags).toHaveLength(1);
|
expect(Object.keys(schema.flags)).toHaveLength(1);
|
||||||
expect(schema.flags[0]).toEqual({ name: 'verbose', short: 'v' });
|
expect(schema.flags.verbose).toEqual({ name: 'verbose', short: 'v' });
|
||||||
});
|
|
||||||
|
|
||||||
it('should parse short alias for options', () => {
|
|
||||||
const schema = parseCommandSchema('move <from> [--speed: number -s]');
|
|
||||||
expect(schema.options).toHaveLength(1);
|
|
||||||
expect(schema.options[0]).toEqual({
|
|
||||||
name: 'speed',
|
|
||||||
short: 's',
|
|
||||||
required: false,
|
|
||||||
schema: expect.any(Object),
|
|
||||||
defaultValue: undefined,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should parse default value syntax', () => {
|
|
||||||
const schema = parseCommandSchema('move <from> [--speed: number = 10]');
|
|
||||||
expect(schema.options).toHaveLength(1);
|
|
||||||
expect(schema.options[0].defaultValue).toBe(10);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should parse default string value', () => {
|
|
||||||
const schema = parseCommandSchema('move <from> [--name: string = "default"]');
|
|
||||||
expect(schema.options).toHaveLength(1);
|
|
||||||
expect(schema.options[0].defaultValue).toBe('default');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should parse short alias with default value', () => {
|
|
||||||
const schema = parseCommandSchema('move <from> [--speed: number -s = 5]');
|
|
||||||
expect(schema.options).toHaveLength(1);
|
|
||||||
expect(schema.options[0].short).toBe('s');
|
|
||||||
expect(schema.options[0].defaultValue).toBe(5);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse command with short alias', () => {
|
it('should parse command with short alias', () => {
|
||||||
|
|
@ -288,3 +249,5 @@ describe('integration', () => {
|
||||||
expect(command.options.s).toBe('100');
|
expect(command.options.s).toBe('100');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue