Files
reverse-engineering-anything/src/cli/argumentErrors.ts
T
ShuaixinHuang 1dc83b160d fix(cli): align ADB directory and package query schemas (#8974)
* fix(cli): align ADB directory and package query schemas

* fix(cli): preserve ADB selector correction details
2026-11-05 17:30:06 +08:00

109 lines
3.9 KiB
TypeScript

import { Errors } from "incur";
import { z } from "zod";
import { projectInputIssues } from "../domain/inputIssueProjection.js";
import { encodeMarkdownTableCell } from "./cliOutputFormatter.js";
import type { CliInstance } from "./types.js";
import { visitRegisteredCommands } from "./visitRegisteredCommands.js";
const ARGUMENT_GUIDANCE =
"REA could not read the command arguments. Check the named argument and its expected type, then try again.";
const adbPackageCommands = new Set([
"pull-adb-package",
"inspect-adb-package",
"uninstall-adb-package",
"start-adb-app",
"stop-adb-app",
]);
const isAdbSelector = (command: string, field: string): boolean =>
(command.includes("-adb-") && field === "serial") ||
(adbPackageCommands.has(command) && field === "package") ||
(command === "list-adb-directory" && field === "device-path") ||
(command === "resolve-adb-packages" && field === "query") ||
(command === "start-adb-activity" &&
["action", "component", "data-uri"].includes(field));
/** Preserve parser correction fields without embedding its serialized cause. */
export const presentCliArgumentErrors = (cli: CliInstance): void => {
visitRegisteredCommands(cli, (command, path) => {
command.middleware = [
async (_context, next) => {
try {
await next();
} catch (cause: unknown) {
if (!(cause instanceof Errors.ValidationError)) throw cause;
const failure = new Errors.ValidationError({
message: ARGUMENT_GUIDANCE,
fieldErrors: adbSelectorConstraints(path, cause),
});
// Incur's constructor appends cause.message as display details. Keep
// the original error separately, without copying it into the message.
Object.defineProperty(failure, "cause", { value: cause });
throw failure;
}
},
...(command.middleware ?? []),
];
});
};
/** Encode only at the final Markdown presentation boundary, after redaction. */
export const encodeCliArgumentErrors = (cli: CliInstance): void => {
visitRegisteredCommands(cli, (command) => {
command.middleware = [
async (context, next) => {
try {
await next();
} catch (error: unknown) {
if (
context.format === "md" &&
error instanceof Errors.ValidationError
) {
error.fieldErrors = error.fieldErrors.map((field) => ({
...field,
...(field.code === undefined
? {}
: { code: encodeMarkdownTableCell(field.code) }),
path: encodeMarkdownTableCell(field.path),
expected: encodeMarkdownTableCell(field.expected),
received: encodeMarkdownTableCell(field.received),
message: encodeMarkdownTableCell(field.message),
}));
}
throw error;
}
},
...(command.middleware ?? []),
];
});
};
/** Retain safe selector constraints dropped by Incur's parser projection. */
const adbSelectorConstraints = (
command: string,
error: Errors.ValidationError,
): Errors.FieldError[] => {
if (!command.includes("-adb-") || !(error.cause instanceof z.ZodError))
return error.fieldErrors;
return error.fieldErrors.map((field, index) => {
const issue =
error.cause instanceof z.ZodError ? error.cause.issues[index] : undefined;
if (
issue === undefined ||
issue.path.length !== 1 ||
issue.path[0] !== field.path ||
issue.code !== field.code ||
!isAdbSelector(command, field.path) ||
(issue.code !== "too_big" && issue.code !== "invalid_format")
)
return field;
const projected = projectInputIssues([issue], undefined)[0];
if (projected?.maximum !== undefined)
return { ...field, maximum: projected.maximum };
if (projected?.expected === "regex") return { ...field, expected: "regex" };
return field;
});
};