Optique 1.1.0: Command discovery, value parsers, and ordered grammars #834
dahlia
announced in
Announcements
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
Optique 1.1.0 is the first feature release after the stable 1.0.0 baseline. The largest addition is @optique/discover, a package for organizing larger CLIs as file-based command modules with typed handlers.
The release also adds value parsers for file sizes, colors, semantic versions, JSON, and key–value pairs;
seq()for ordered positional grammars;negatableFlag()for--foo/--no-foooptions; command aliases; async Zod and Valibot helpers; and .env file loading in @optique/env.Optique is a TypeScript CLI parser built from composable parser functions. Parser definitions drive runtime parsing, help and completion output, and the inferred value types that handlers receive.
Command discovery with @optique/discover
Optique's core combinators work well when the entire command tree fits in one module. Larger applications often want a file-per-command layout: each command owns its parser, metadata, and handler, and the application builds the command tree from whatever modules exist in a directory. The new @optique/discover package provides that layer.
Command modules default-export a
defineCommand()definition.runProgram()scans the command directory, maps file paths to command paths, builds the parser tree, and dispatches to the matched handler:A command module looks like this:
With a file layout like:
the discovered paths are
admin build,admin user add, andadmin user remove. Help, shell completion, and version output are enabled automatically through @optique/run. TypeScript checks the handler signature against the parser's inferred value type, so changing the parser immediately surfaces handler mismatches at compile time.When command modules need to be visible to a bundler or single-file packager, import them manually and pass them as a
commandsarray instead of usingdir. Both modes are mutually exclusive.The original proposal and implementation are in #812 and #818.
See the command discovery documentation for details on file naming, extension filtering, path conflicts, and when dynamic discovery fits versus static imports.
Value parsers for common CLI inputs
Five new value parsers are available in
@optique/core/valueparserstarting with this release:fileSize()numberorbigintcolor()ColorsemVer()SemVerStringorSemVerjson()JsonkeyValue()readonly [key, value]KEY=VALUEpairsThe new parsers cover common value shapes.
fileSize()converts strings like"10MB"or"1.5GiB"to byte counts in SI or IEC units;type: "bigint"handles values aboveNumber.MAX_SAFE_INTEGER.color()returns a structuredColorwithr,g,b, andafields from hex notation,rgb()/hsl()functions, or any CSS Level 4 named color includingtransparentandrebeccapurple.semVer()validates Semantic Versioning 2.0.0 strings strictly and returns either aSemVerStringtemplate-literal type or a parsed object withmajor,minor,patch, and optionalpreReleaseandmetadatafields.json()accepts any well-formed JSON and optionally restricts the root type.keyValue()parsesKEY=VALUEpairs into readonly tuples with typed keys and values.A few usage examples:
For mixed inputs,
firstOf()tries value parsers in order and returns the first success, with the result type inferred as the union of the constituent types:When every constituent enumerates choices,
firstOf()merges them for shell completion. Custom value parsers can also implement the new optionalvalidate()method onValueParserto express validation failures that the genericformat()+parse()round-trip cannot cover, which matters when constituent parsers produce overlapping string representations.Ordered grammars with
seq()The existing
tuple()combinator lets child parsers compete by priority, which works well when input order does not matter. Some grammars require declaration order: an optional positional argument before a subcommand name, for instance, needs a guarantee that the positional slot does not accidentally consume the command token.seq()solves this by applying parsers in declaration order and advancing a cursor as each succeeds:With this parser, both of these forms work as written:
Usage output follows declaration order instead of priority order, so the help text matches the grammar:
tool [PROFILE] (build TARGET | deploy ENV [--force])When the current child parser can finish without consuming more input,
seq()can skip it to reach a later command name. It does not backtrack after a later parser succeeds, so ambiguous variadic positionals still need an explicit boundary such as a command name or--.The design rationale and alternative approaches considered are discussed in #819.
negatableFlag()for paired boolean optionsMany CLI conventions offer paired options such as
--colorand--no-color, where one side explicitly enables something and the other explicitly disables it. Previously, expressing this required two separate flags and manual conflict checking after parsing. The newnegatableFlag()parser handles both sides as a single unit:The parser returns
truewhen the positive flag appears andfalsewhen the negative flag appears. Providing both on the same invocation is a conflict; providing the same side twice is a duplicate. Both sides support aliases. Optique renders the positive and negative names in one help entry to communicate that they control the same Boolean value.Without
withDefault()oroptional(), one side is required:The original proposal and implementation are in #801 and #802.
Command aliases
Commands now accept an
aliasesoption for short or legacy names. Aliases parse identically to the canonical name, appear in shell completion, and surface in typo suggestions, but they are hidden from usage and help output:Aliases share the command namespace with sibling commands. Optique throws at construction time if an alias collides with another command name or alias in
or(),longestMatch(),object(), ormerge(). Sequential compositions likeseq()andtuple()are exempt because their children are positional parts of the same command line, not alternatives at the same position.Async Zod and Valibot helpers
The existing
zod()andvalibot()helpers are synchronous and reject schemas that require async execution. This release addszodAsync()in @optique/zod andvalibotAsync()in @optique/valibot for schemas with async refinements, transforms, or pipe steps:Both helpers preserve the behavior of their synchronous counterparts: metavar inference, choice enumeration, completion suggestions, formatting, custom error messages, and fallback validation for values from source contexts such as
bindEnv()andbindConfig()..env file support in @optique/env
createEnvContext()now accepts anenvFileoption that loads .env files as an internal fallback layer below real environment variables. Loaded values are used bybindEnv()but never written toprocess.envorDeno.env.envFile: trueloads .env from the current working directory. An array loads files in order, with later files overriding earlier ones. Missing files are skipped. The built-in parser supports standard dotenv syntax: comments, quoted values, variable expansion, and literal single-quoted strings. Command substitution is opt-in viaenvFile.substitute.bindEnv()now resolves values in this order:IPv6 in
socketAddress()socketAddress()now accepts bracketed IPv6 endpoints such as[::1]:8080and host-only IPv6 literals when adefaultPortis configured. Two new options control IP version filtering:host.ipv4andhost.ipv6provide separate restriction objects for each version.format()andnormalize()now emit bracketed notation for IPv6 hosts. The existinghost.ipoption remains as an IPv4 compatibility alias.Bug fixes
Shell completion now correctly includes root-level meta options such as
--helpand--version, including plus-prefixed and single-dash aliases, in suggestions from empty prompts and after subcommands. Previously these were omitted or incorrectly triggered when option values happened to look like help/version flags.In
object()parsers that expect no CLI input, the specific error from a required field'scomplete()is now surfaced instead of the generic “No matching option, command, or argument found.” The fallback message when no input is expected at all is now “No value provided.”When multiple parser fields share the same deferred dependency node, resolution now yields the same result object across all of them instead of distinct but structurally equal objects.
bindEnv()andbindConfig()now emit a distinct error message when their respective contexts are not registered with the runner, so missing runner context no longer looks like a parsing failure.@optique/temporal no longer fails on runtimes whose Temporal implementation rejects curated IANA timezone links such as
CET. The parser now applies a cross-runtime allowlist before delegating single-segment identifiers to runtime Temporal validation.Installation
For command discovery:
For environment variable support:
If you have ideas for future improvements or encounter any issues, please let us know through GitHub Issues. For more information about Optique and its features, visit the documentation or check out the full changelog.
Beta Was this translation helpful? Give feedback.
All reactions