Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [1.0.2] - 2026-08-09

### Added

- `placeholder?: string` flag descriptor field: overrides the `<value>` placeholder in the auto-generated usage string (e.g. `<dir>`) for a value-taking flag.

## [1.0.1] - 2026-08-09

### Added
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ cli.parse(argv, overrideSchema);

A schema without `.optional()`/`.default()` is required - omitting the flag is a parse error, and it's shown without brackets in the auto-generated usage string. `.optional()`/`.default()` short-circuit on a missing flag before any coercion/transform runs, so an omitted flag resolves to `undefined`/the default rather than failing (e.g. on `Number(undefined)`).

The `<value>` placeholder shown for value-taking flags can be overridden per-flag with `placeholder`:

```ts
dir: { schema: z.string(), placeholder: "dir" } // --dir <dir>
```

**Booleans**: `negatable: true` only applies to a (possibly `.optional()`/`.default()`-wrapped) `z.boolean()` and throws at `defineCli()` time on anything else. `z.stringbool()` parses `"true"`/`"1"`/`"yes"`/`"on"`/`"y"`/`"enabled"` (case-insensitive) as `true` and their negatives as `false` - don't reach for `z.coerce.boolean()` instead, it's plain JS truthiness, so `z.coerce.boolean().parse("false")` is `true` (any non-empty string is truthy). A union of boolean literals doesn't behave like `z.boolean()` either: only a bare `z.boolean()` registers a presence-style parseArgs boolean, so `z.union([z.literal(true), z.literal(false)])` is registered as a value-taking string flag, and fails against the literal string `"true"` it would actually receive.

```ts
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "zod-cli-flags",
"version": "1.0.1",
"version": "1.0.2",
"description": "Define a CLI's flags as a single Zod schema and parse argv into a typed result.",
"type": "module",
"main": "./dist/index.js",
Expand Down
14 changes: 14 additions & 0 deletions src/defineCli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,20 @@ describe("defineCli - positionals", () => {
if (result.success) expect(result.positionals).toEqual(["one", "two"]);
});

it("substitutes a flag's placeholder into the auto-generated usage", () => {
const cli = defineCli({
flags: {
output: { schema: z.string(), placeholder: "dir" },
tag: {
schema: z.array(z.string()).default([]),
multiple: true,
placeholder: "tag",
},
},
});
expect(cli.usage).toBe("Usage: --output <dir> [--tag <tag>...]");
});

it("prepends the label before the flags when positionals carry one", () => {
const cli = defineCli({
flags: { output: { schema: z.string() } },
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export interface FlagDescriptor<T = unknown> {
negatable?: boolean;
/** Shown in the auto-generated usage string. */
description?: string;
/** Value placeholder in the auto-generated usage string, e.g. "dir" for <dir>. Defaults to "value". */
placeholder?: string;
}

export type FlagDescriptors = Record<string, FlagDescriptor<any>>;
Expand Down
6 changes: 5 additions & 1 deletion src/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ interface UsageFlag {
multiple?: boolean;
negatable?: boolean;
description?: string;
placeholder?: string;
};
isBoolean: boolean;
isOptional: boolean;
Expand All @@ -26,7 +27,10 @@ export function buildUsage(
} else if (isBoolean) {
core = alias;
} else {
const value = descriptor.multiple ? "<value>..." : "<value>";
const placeholder = descriptor.placeholder ?? "value";
const value = descriptor.multiple
? `<${placeholder}>...`
: `<${placeholder}>`;
core = `${alias} ${value}`;
}

Expand Down
Loading