Incur derives `--schema`, the `--llms` manifests, and help synopses from Zod's output view, where a defaulted field always has a value, so every defaulted argument and option was listed as required even though the CLI accepts its omission. Wrap each field that fills an omitted value as optional after registration, keeping its default, constraints, metadata, and parsed value. Fixes #1169 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
67 lines
2.3 KiB
TypeScript
67 lines
2.3 KiB
TypeScript
import { Cli, z } from "incur";
|
|
|
|
import type { CliInstance } from "./types.js";
|
|
|
|
type CommandMap = NonNullable<ReturnType<typeof Cli.toCommands.get>>;
|
|
type CommandInputs = Partial<Record<(typeof INPUT_GROUPS)[number], unknown>>;
|
|
|
|
const INPUT_GROUPS = ["args", "env", "options"] as const;
|
|
|
|
// Incur types its router key without vars, env, or globals; accept any CLI.
|
|
const registeredCommands: WeakMap<object, CommandMap> = Cli.toCommands;
|
|
|
|
/**
|
|
* Advertise parser-filled defaults as omittable CLI inputs.
|
|
*
|
|
* Incur derives `--schema`, the `--llms` manifests, and help synopses from
|
|
* Zod's output view, where a defaulted field always has a value and is
|
|
* therefore listed as required. Callers may omit such a field, so each one is
|
|
* wrapped as optional while its default, constraints, metadata, and parsed
|
|
* value stay unchanged.
|
|
*/
|
|
export const presentOmittableDefaults = (cli: CliInstance): void => {
|
|
const commands = registeredCommands.get(cli);
|
|
if (commands !== undefined) presentCommands(commands);
|
|
};
|
|
|
|
const presentCommands = (commands: CommandMap): void => {
|
|
for (const entry of commands.values()) {
|
|
if ("_group" in entry) {
|
|
presentCommands(entry.commands);
|
|
if (entry.root !== undefined) presentInputs(entry.root);
|
|
} else if (!("_alias" in entry) && !("_fetch" in entry))
|
|
presentInputs(entry);
|
|
}
|
|
};
|
|
|
|
const presentInputs = (command: CommandInputs): void => {
|
|
for (const group of INPUT_GROUPS) {
|
|
const schema = command[group];
|
|
if (schema instanceof z.ZodObject)
|
|
command[group] = withOmittableDefaults(schema);
|
|
}
|
|
};
|
|
|
|
const withOmittableDefaults = (schema: z.ZodObject): z.ZodObject => {
|
|
const omittable = Object.entries(schema.shape).filter(([, field]) =>
|
|
fillsOmission(field),
|
|
);
|
|
if (omittable.length === 0) return schema;
|
|
return schema.safeExtend(
|
|
Object.fromEntries(
|
|
omittable.map(([key, field]) => [key, optionalWithMetadata(field)]),
|
|
),
|
|
);
|
|
};
|
|
|
|
/** Zod accepts an omitted value (`optin`) but always produces one (`optout`). */
|
|
const fillsOmission = (field: z.ZodType): boolean =>
|
|
field._zod.optin === "optional" && field._zod.optout === undefined;
|
|
|
|
const optionalWithMetadata = (field: z.ZodType): z.ZodType => {
|
|
const metadata = field.meta();
|
|
return metadata === undefined
|
|
? field.optional()
|
|
: field.optional().meta(metadata);
|
|
};
|