Files
callstack__agent-device/scripts/help-conformance-command-validator.ts
Andrew Barnes 4c02b6ad2b fix(cli): reject excess positionals (#1433)
* fix(cli): reject excess positionals

* fix: preserve get ref labels in arity checks

---------

Co-authored-by: Andrew Barnes <169967362+Bortlesboat@users.noreply.github.com>
Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
2026-07-27 17:23:30 +02:00

88 lines
3.2 KiB
TypeScript

import { pathToFileURL } from 'node:url';
import { parseArgs } from '../src/cli/parser/args.ts';
import { readInputFromCli } from '../src/commands/cli-grammar.ts';
import { isCommandName } from '../src/commands/command-metadata.ts';
type ValidationKind = 'agent-device-grammar' | 'pseudo-ref';
type ValidationResult = { valid: true } | { valid: false; kind: ValidationKind; error: string };
const TARGET_POSITION_BY_COMMAND = new Map<string, number>([
['click', 0],
['fill', 0],
['get', 1],
['longpress', 0],
['press', 0],
['wait', 0],
]);
const POSITIONAL_AT_REFS_ARE_INVALID = new Set(['focus', 'scroll', 'swipe']);
const CONCRETE_REF = /^@[ec]\d+(?:~s\d+)?$/;
export function validateAgentDeviceCommand(value: unknown): ValidationResult {
if (!isStringArray(value) || value.length === 0) {
return invalid('agent-device-grammar', 'Expected a non-empty string argv array.');
}
try {
return validateParsedCommand(parseArgs(value, { strictFlags: true }));
} catch (error) {
return invalid('agent-device-grammar', error instanceof Error ? error.message : String(error));
}
}
function validateParsedCommand(parsed: ReturnType<typeof parseArgs>): ValidationResult {
if (!parsed.command) return validateGlobalFlags(parsed.flags);
const pseudoRef = targetRefCandidate(parsed.command, parsed.positionals);
if (pseudoRef && !CONCRETE_REF.test(pseudoRef)) {
return invalid('pseudo-ref', `Pseudo ref "${pseudoRef}" is not an observed @eN or @cN ref.`);
}
if (isCommandName(parsed.command)) {
readInputFromCli(parsed.command, parsed.positionals, parsed.flags);
}
return { valid: true };
}
function validateGlobalFlags(flags: ReturnType<typeof parseArgs>['flags']): ValidationResult {
return flags.version || flags.help
? { valid: true }
: invalid('agent-device-grammar', 'Missing command.');
}
function targetRefCandidate(command: string, positionals: string[]): string | undefined {
const targetPosition = TARGET_POSITION_BY_COMMAND.get(command);
if (targetPosition !== undefined) {
const candidate = positionals[targetPosition];
return candidate?.startsWith('@') ? candidate : undefined;
}
if (command === 'is') return positionals.find((positional) => positional.startsWith('@'));
if (POSITIONAL_AT_REFS_ARE_INVALID.has(command)) {
return positionals.find((positional) => positional.startsWith('@'));
}
return undefined;
}
function invalid(kind: ValidationKind, error: string): ValidationResult {
return { valid: false, kind, error };
}
function isStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every((entry) => typeof entry === 'string');
}
function validateInput(value: unknown): ValidationResult | ValidationResult[] {
if (Array.isArray(value) && value.every(isStringArray))
return value.map(validateAgentDeviceCommand);
return validateAgentDeviceCommand(value);
}
function runCli(): void {
const encodedInput = process.argv[2];
if (!encodedInput) throw new Error('Expected a JSON argv array.');
process.stdout.write(JSON.stringify(validateInput(JSON.parse(encodedInput))));
}
const executedPath = process.argv[1];
if (executedPath && import.meta.url === pathToFileURL(executedPath).href) runCli();