feat(engine): add lightweight runtime validation and coercion for CLI arguments
This commit is contained in:
1 parent
12c4b8853b
commit
29ea5ce059
1 file changed
+58
-1
+58
-1
@@ -164,15 +164,72 @@ function registerYamlCli(filePath: string, defaultSite: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and coerces arguments based on the command's Arg definitions.
|
||||
*/
|
||||
function coerceAndValidateArgs(cmdArgs: Arg[], kwargs: Record<string, any>): Record<string, any> {
|
||||
const result: Record<string, any> = { ...kwargs };
|
||||
|
||||
for (const argDef of cmdArgs) {
|
||||
const val = result[argDef.name];
|
||||
|
||||
// 1. Check required
|
||||
if (argDef.required && (val === undefined || val === null || val === '')) {
|
||||
throw new Error(`Argument "${argDef.name}" is required.\n${argDef.help ? `Hint: ${argDef.help}` : ''}`);
|
||||
}
|
||||
|
||||
if (val !== undefined && val !== null) {
|
||||
// 2. Type coercion
|
||||
if (argDef.type === 'int' || argDef.type === 'number') {
|
||||
const num = Number(val);
|
||||
if (Number.isNaN(num)) {
|
||||
throw new Error(`Argument "${argDef.name}" must be a valid number. Received: "${val}"`);
|
||||
}
|
||||
result[argDef.name] = num;
|
||||
} else if (argDef.type === 'boolean' || argDef.type === 'bool') {
|
||||
if (typeof val === 'string') {
|
||||
const lower = val.toLowerCase();
|
||||
if (lower === 'true' || lower === '1') result[argDef.name] = true;
|
||||
else if (lower === 'false' || lower === '0') result[argDef.name] = false;
|
||||
else throw new Error(`Argument "${argDef.name}" must be a boolean (true/false). Received: "${val}"`);
|
||||
} else {
|
||||
result[argDef.name] = Boolean(val);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Choices validation
|
||||
const coercedVal = result[argDef.name];
|
||||
if (argDef.choices && argDef.choices.length > 0) {
|
||||
// Only stringent check for string/number types against choices array
|
||||
if (!argDef.choices.map(String).includes(String(coercedVal))) {
|
||||
throw new Error(`Argument "${argDef.name}" must be one of: ${argDef.choices.join(', ')}. Received: "${coercedVal}"`);
|
||||
}
|
||||
}
|
||||
} else if (argDef.default !== undefined) {
|
||||
// Set default if value is missing
|
||||
result[argDef.name] = argDef.default;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a CLI command. Handles lazy-loading of TS modules.
|
||||
*/
|
||||
export async function executeCommand(
|
||||
cmd: CliCommand,
|
||||
page: IPage | null,
|
||||
kwargs: Record<string, any>,
|
||||
rawKwargs: Record<string, any>,
|
||||
debug: boolean = false,
|
||||
): Promise<any> {
|
||||
let kwargs: Record<string, any>;
|
||||
try {
|
||||
kwargs = coerceAndValidateArgs(cmd.args, rawKwargs);
|
||||
} catch (err: any) {
|
||||
// Re-throw validation errors clearly
|
||||
throw new Error(`[Argument Validation Error]\n${err.message}`);
|
||||
}
|
||||
|
||||
// Lazy-load TS module on first execution
|
||||
const internal = cmd as InternalCliCommand;
|
||||
if (internal._lazy && internal._modulePath) {
|
||||
|
||||
Reference in new issue
Block a user