From 4ad8c5284f3abf1ba9d2fe72bfc8965edbbe517e Mon Sep 17 00:00:00 2001 From: figulusproject <269854178+figulusproject@users.noreply.github.com> Date: Sun, 9 Aug 2026 03:59:20 -0400 Subject: [PATCH 1/2] let flags carry an optional placeholder, override flag value placeholder in auto-generated usage string --- CHANGELOG.md | 6 ++++++ README.md | 6 ++++++ src/defineCli.test.ts | 14 ++++++++++++++ src/types.ts | 2 ++ src/usage.ts | 6 +++++- 5 files changed, 33 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a89a1c4..173760e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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/). +## [Unreleased] + +### Added + +- `placeholder?: string` flag descriptor field: overrides the `` placeholder in the auto-generated usage string (e.g. ``) for a value-taking flag. + ## [1.0.1] - 2026-08-09 ### Added diff --git a/README.md b/README.md index 4b0262e..8da83cd 100644 --- a/README.md +++ b/README.md @@ -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 `` placeholder shown for value-taking flags can be overridden per-flag with `placeholder`: + +```ts +dir: { schema: z.string(), placeholder: "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 diff --git a/src/defineCli.test.ts b/src/defineCli.test.ts index dd2f935..bcd3c77 100644 --- a/src/defineCli.test.ts +++ b/src/defineCli.test.ts @@ -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 [--tag ...]"); + }); + it("prepends the label before the flags when positionals carry one", () => { const cli = defineCli({ flags: { output: { schema: z.string() } }, diff --git a/src/types.ts b/src/types.ts index 9384704..9510f80 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,6 +14,8 @@ export interface FlagDescriptor { negatable?: boolean; /** Shown in the auto-generated usage string. */ description?: string; + /** Value placeholder in the auto-generated usage string, e.g. "dir" for . Defaults to "value". */ + placeholder?: string; } export type FlagDescriptors = Record>; diff --git a/src/usage.ts b/src/usage.ts index b27d74e..4ef3e4d 100644 --- a/src/usage.ts +++ b/src/usage.ts @@ -5,6 +5,7 @@ interface UsageFlag { multiple?: boolean; negatable?: boolean; description?: string; + placeholder?: string; }; isBoolean: boolean; isOptional: boolean; @@ -26,7 +27,10 @@ export function buildUsage( } else if (isBoolean) { core = alias; } else { - const value = descriptor.multiple ? "..." : ""; + const placeholder = descriptor.placeholder ?? "value"; + const value = descriptor.multiple + ? `<${placeholder}>...` + : `<${placeholder}>`; core = `${alias} ${value}`; } From 7d3f2ed274a338a46c22a40dbd0dbff131c8b047 Mon Sep 17 00:00:00 2001 From: figulusproject <269854178+figulusproject@users.noreply.github.com> Date: Sun, 9 Aug 2026 04:00:01 -0400 Subject: [PATCH 2/2] version++ --- CHANGELOG.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 173760e..dc1fc96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ 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/). -## [Unreleased] +## [1.0.2] - 2026-08-09 ### Added diff --git a/package-lock.json b/package-lock.json index d275073..63426ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "zod-cli-flags", - "version": "1.0.1", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "zod-cli-flags", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "devDependencies": { "@types/node": "^22", diff --git a/package.json b/package.json index c1ae2aa..7ed750f 100644 --- a/package.json +++ b/package.json @@ -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",