diff --git a/src/engine.ts b/src/engine.ts index 1ea8518..6b78d0b 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -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): Record { + const result: Record = { ...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, + rawKwargs: Record, debug: boolean = false, ): Promise { + let kwargs: Record; + 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) {