Optique is a type-safe combinatorial CLI parser for TypeScript, inspired by Haskell's optparse-applicative and TypeScript's Zod. It takes a functional approach: you compose small, typed parsers into larger ones using combinators, and the TypeScript compiler infers the result type automatically. It supports flags, options, subcommands, inter-option dependencies, shell completion, and man page generation across Deno, Node.js, and Bun.
This is the first stable release. Optique 1.0.0 adds two integration packages and finishes the 1.0 API cleanup. It also rewrites the source-context and dependency runtime internals and fixes several hundred issues in shell completion, help output, and value parsing.
New packages
@optique/env: environment variable integration
The new @optique/env package lets you bind any parser to an environment variable as a fallback when the CLI argument is absent. The priority chain is CLI argument → environment variable → default value → error.
import { bindEnv, bool, createEnvContext } from "@optique/env"; import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { integer, string } from "@optique/core/valueparser"; import { run } from "@optique/run"; const envContext = createEnvContext({ prefix: "MYAPP_" }); const parser = object({ host: bindEnv(option("--host", string()), { context: envContext, key: "HOST", default: "localhost", }), port: bindEnv(option("--port", integer()), { context: envContext, key: "PORT", default: 3000, }), verbose: bindEnv(option("--verbose", bool()), { context: envContext, key: "VERBOSE", default: false, }), }); await run(parser, { contexts: [envContext] });
The package includes a bool() value parser that accepts all common Boolean environment variable literals: true, false, 1, 0, yes, no, on, off. When used via bindEnv(fail<T>(), ...), it also supports env-only values with no CLI option counterpart.
See the environment variable integration guide for bindEnv(), bool(), and env-only values via fail<T>(). (#86)
@optique/inquirer: interactive prompt integration
The new @optique/inquirer package wraps any parser with an interactive Inquirer.js prompt that fires when no CLI argument is provided. Ten prompt types are supported: input, password, number, confirm, select, rawlist, expand, checkbox, editor, and a prompter escape hatch for custom implementations.
import { prompt } from "@optique/inquirer"; import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { string, integer } from "@optique/core/valueparser"; import { run } from "@optique/run"; const parser = object({ name: prompt(option("--name", string()), { type: "input", message: "Enter your name:", }), port: prompt(option("--port", integer()), { type: "number", message: "Enter the port number:", default: 3000, }), }); await run(parser);
prompt() always returns an async parser (mode: "async"). It integrates cleanly with bindEnv() and bindConfig(): the prompt is skipped whenever the CLI, environment variable, or config file already supplies a value. Cancelling a prompt with Ctrl+C produces a normal parse failure rather than an unhandled rejection. (#87, #151)
See the interactive prompt guide for supported prompt types, configuration options, and bindEnv() / bindConfig() composition examples.
Breaking changes
@optique/core
Parser.$mode and ValueParser.$mode renamed to .mode
The runtime mode property no longer carries a $ prefix. Type-only markers such as Parser.$valueType, Parser.$stateType, and SourceContext.$requiredOptions keep the $ prefix. Update any code that reads .mode off a parser or value parser object.
Narrowed public extension surface (#790, #792, #793, #794)
@optique/core now exposes two public extension subpaths:
@optique/core/annotations: read-only annotation access viagetAnnotations()@optique/core/extension: wrapper helpers includinginjectAnnotations(),inheritAnnotations(),withAnnotationView(),dispatchByMode(),mapModeValue(),wrapForMode(),defineTraits(),getTraits(),delegateSuggestNodes(), andmapSourceMetadata()
The old @optique/core/mode-dispatch subpath and previously leaked internal entry points are removed. If you maintained a custom parser or wrapper that imported from internal paths, migrate to the new @optique/core/extension subpath.
Source context phase field now required (#243, #783)
SourceContext previously used an inferred mode contract to decide whether to run a two-pass parse. This contract was ambiguous in edge cases. It is now replaced by an explicit required phase field:
// Before (inferred from getAnnotations() behavior) const myContext: SourceContext = { id: ..., getAnnotations() { ... } }; // After const myContext: SourceContext = { id: ..., phase: "two-pass", // or "single-pass" getAnnotations(request) { ... }, };
createEnvContext() sets phase: "single-pass" and createConfigContext() sets phase: "two-pass". Custom contexts must migrate to the new contract. SourceContextMode, SourceContext.mode, and isStaticContext() have been removed.
SourceContext.getAnnotations() receives an explicit request object (#271, #786)
The two-pass protocol previously used undefined as a phase-1 sentinel, making it impossible for custom contexts to distinguish a genuine first-pass undefined result from “no data yet.” The method signature has changed:
// Before getAnnotations(): Annotations | undefined; // After getAnnotations(request: SourceContextRequest): Annotations; // where request.phase is "phase1" or "phase2"
If you implemented a custom SourceContext, update getAnnotations() to accept a SourceContextRequest parameter and use request.phase to distinguish the phases.
Context-required options must now be wrapped in contextOptions (#240, #241, #575, #581)
Options required by source contexts (such as getConfigPath and load for @optique/config) must now be passed inside a contextOptions property instead of as top-level runner options. This prevents name collisions with built-in runner options like args, help, and version.
// Before await run(parser, { contexts: [configContext], getConfigPath: (parsed) => parsed.config, }); // After await run(parser, { contexts: [configContext], contextOptions: { getConfigPath: (parsed) => parsed.config }, });
ValueParser.placeholder is now a required interface property (#407, #727)
Every ValueParser must now declare a placeholder property: a type-appropriate stand-in value (for example, "" for string(), 1 for port()) used during phase-1 parsing when prompt() defers its answer to phase 2. The isPlaceholderValue() function and the placeholder symbol export from @optique/core/context have been removed alongside the old DeferredPromptValue sentinel.
If you implemented a custom ValueParser, add a placeholder property with a suitable stand-in value of type T. If you used isPlaceholderValue() to detect deferred values, that check is no longer necessary. See the Core improvements section for the rationale behind this change.
valueSet() now requires a fallback parameter (#492, #747)
valueSet() used to accept a single array argument, which could produce malformed sentences like “Expected one of .” when the array was empty. It now requires a second parameter: either a fallback string or an options object with a fallback field.
// Before valueSet(choices) // After valueSet(choices, { fallback: "any value" }) // or valueSet(choices, "any value")
values() now throws TypeError when given an empty array.
Meta command configuration redesigned (#130)
The help, version, and completion fields in RunOptions (both in @optique/core and @optique/run) previously used a mode: "command" | "option" | "both" discriminant to control whether a meta feature appeared as a subcommand, a flag, or both. This has been replaced with independent command and option sub-configs:
// Before { help: { mode: "both", commandNames: ["help"], optionNames: ["--help"] } } // After { help: { command: { names: ["help"] }, option: { names: ["--help"] } } }
String shorthands ("command", "option", "both") are preserved in @optique/run for convenience. The types CompletionName, CompletionHelpVisibility, CompletionConfig, CompletionConfigBoth, CompletionConfigSingular, and CompletionConfigPlural have been removed from @optique/core. The corresponding types from @optique/run (CompletionHelpVisibility, CompletionOptionsBase, CompletionOptionsBoth, CompletionOptionsSingular, CompletionOptionsPlural, CompletionOptions) have also been removed.
UserParserNames interface simplified (#735, #741)
The leadingOptions, leadingCommands, and leadingLiterals fields have been replaced by a single leadingNames set.
Help output section ordering changed (#115)
The default section ordering in help output now uses a type-aware sort: sections containing only commands appear first, followed by mixed sections, and then sections containing only options, flags, and arguments. Untitled sections receive a slight priority boost. Previously, untitled sections were sorted first regardless of content type. A new sectionOrder callback option in DocPageFormatOptions and RunOptions can override this sort entirely.
@optique/config
runWithConfig() removed (#110)
The @optique/config/run subpath and the runWithConfig() function have been removed. Config contexts are now used directly with run(), runSync(), or runAsync() from @optique/run (or runWith() from @optique/core/facade) via the contexts option. Options like getConfigPath and load are now passed via contextOptions.
// Before import { runWithConfig } from "@optique/config/run"; await runWithConfig(parser, configContext, { args: process.argv.slice(2), getConfigPath: (parsed) => parsed.config, }); // After import { run } from "@optique/run"; await run(parser, { contexts: [configContext], contextOptions: { getConfigPath: (parsed) => parsed.config }, });
fileParser moved to createConfigContext() options (#110)
The fileParser option, previously passed to runWithConfig() at runtime, must now be provided when calling createConfigContext().
configKey symbol removed (#136)
Each ConfigContext instance now stores its data under its own unique id symbol. If you accessed config annotations directly via the exported configKey symbol, use context.id instead.
CustomLoadOptions.load now returns { config, meta } instead of raw config (#111)
The load callback must now return a ConfigLoadResult object ({ config, meta }) rather than raw config data. This allows config metadata (such as configPath and configDir) to be passed to bindConfig() key callbacks via the new second meta parameter.
bindConfig() now validates fallback values against parser constraints (#414, #777)
Config-file values and defaults now go through the inner parser's validateValue() method when available. If your config file contains values that violate CLI parser constraints (for example, an integer below a min threshold), those values are now rejected rather than silently accepted. This is a behavior change if you relied on config values bypassing CLI validation.
@optique/run
contextOptions wrapping required (#240, #241, #575, #581)
As with runWith(), context-required options passed to run(), runSync(), and runAsync() must now be wrapped in contextOptions (see the @optique/core section above).
RunOptions.help/version/completion redesigned (#130)
Object-form configurations now use { command, option } instead of { mode }. String shorthands are still accepted.
@optique/valibot and @optique/zod
placeholder option now required (#407, #727)
valibot() and zod() now require a placeholder option in their respective options objects. This value is used as a stand-in during deferred prompt resolution.
// Before valibot(schema, { metavar: "VALUE" }) zod(schema, { metavar: "VALUE" }) // After valibot(schema, { metavar: "VALUE", placeholder: "" }) zod(schema, { metavar: "VALUE", placeholder: "" })
@optique/temporal
Strict input shapes enforced (#314, #649)
Temporal plain parsers now reject ISO strings that are technically valid but wider than their advertised type. For example, plainDate() no longer accepts datetime strings like "2020-01-23T17:04:36", and plainDateTime() no longer accepts date-only strings like "2020-01-23". If you relied on this lenient behavior, use the appropriate parser for the actual format your users provide.
plainMonthDay() metavar changed (#306, #643)
The default metavar has changed from "--MONTH-DAY" to "MONTH-DAY". The previous metavar looked like a CLI option flag in help text.
@optique/core value parsers
DomainOptions.allowedTLDs renamed to allowedTlds (#345, #638)
Update all references to allowedTLDs to use allowedTlds.
choice() now fails on empty arrays, duplicates, and empty strings (#332, #371, #353)
choice([]) throws TypeError. Empty-string choices and duplicate case-insensitive choices also throw at construction time.
uuid() defaults to strict RFC 9562 validation (#334, #336, #670, #674)
The version digit must be 1 through 8, and the variant nibble must be in the 10xx layout. Use uuid({ strict: false }) to restore the previous lenient behavior.
integer() in number mode rejects values outside the safe integer range (#248, #525)
Values outside Number.MIN_SAFE_INTEGER to Number.MAX_SAFE_INTEGER now fail with an error instead of silently rounding. Use type: "bigint" for large integers.
Core improvements
Placeholder-based deferred prompt resolution (#407, #727)
The old approach used a branded DeferredPromptValue sentinel to mark fields not yet resolved by a prompt during phase-1 parsing. Because map() transforms could receive this sentinel as if it were a real value, the library had to strip sentinels from structured outputs using a proxy-based sanitization layer. That layer ran to roughly 1,000 lines and had persistent bugs around class instances with private fields.
The new approach is simpler: each ValueParser declares a placeholder value that prompt() uses as a phase-1 stand-in. map() receives structurally valid values from the start, and the sanitization machinery is gone entirely from both @optique/core and @optique/config.
fail<T>() parser (#120)
A new fail<T>() parser always fails without consuming input, declared to produce a value of type T. Its primary use is bindConfig(fail<T>(), ...) or bindEnv(fail<T>(), ...) when a value should come only from a config file or environment variable with no CLI flag counterpart.
validateValue() on Parser (#414, #777)
A new optional validateValue() method allows a parser to check whether an arbitrary value satisfies its underlying ValueParser's constraints. Built-in primitives implement it via a format()+parse() round-trip. bindEnv() and bindConfig() use this to enforce parser constraints on fallback values.
ValueParser.normalize() and Parser.normalizeValue() (#318, #742)
A new optional normalize() method on ValueParser canonicalizes values of the parser's type (for example, lowercasing domain names or normalizing MAC address formatting). withDefault() now normalizes default values through this method when available.
Granular hidden visibility controls (#113, #141)
The hidden option on option(), flag(), argument(), command(), passThrough(), group(), object(), and merge() now accepts boolean | "usage" | "doc" | "help" instead of just boolean. hidden: true keeps the existing behavior (hidden from usage, docs, and suggestions). "usage" and "doc" allow partial hiding, and "help" hides terms from usage and help listings while keeping them in shell completions and suggestion candidates.
leadingNames and acceptingAnyToken on Parser (#735, #741)
Each parser now reports which leading tokens it could match at the first buffer position via leadingNames, computed from structural semantics rather than the display-oriented usage tree. Shared-buffer compositions use priority ordering and acceptingAnyToken to exclude names unreachable behind a catch-all parser like argument(). This replaces the old extractLeadingOptionNames(), extractLeadingCommandNames(), and extractLeadingLiteralValues(), which produced incorrect results for tuple(), command(), and conditional().
Centralized dependency runtime (#750)
Shared-buffer constructs (object(), tuple(), concat(), merge()) now use a centralized dependency runtime for source collection, default filling, and derived-parser replay. The runtime is shared across nested constructs via ExecutionContext.dependencyRuntime and reads raw user input from a shared InputTrace instead of the old dependency-state wrapper protocol. Along with this refactor, a number of long-standing dependency resolution bugs have been fixed.
SourceContext now supports Symbol.dispose / Symbol.asyncDispose (#110)
Contexts that hold resources such as global registries can now implement Disposable or AsyncDisposable. runWith() and runWithSync() call dispose on all contexts in a finally block, including on early help, version, and completion exits.
Config source metadata for bindConfig() (#111)
bindConfig() key accessor callbacks now receive a second meta argument. In single-file mode, this metadata includes configPath and configDir so path-like options can be resolved relative to the config file location.
SourceContext.getAnnotations() receives runtime options (#110)
Contexts now receive runtime options (such as getConfigPath and load) passed by the runner, enabling config contexts to load files without a separate runner wrapper.
Equals-joined values on single-dash options (#134)
option() now accepts equals-joined values on single-dash multi-character options (for example, -seed=42 or -max_len=1000), in addition to the existing --option=value and /option:value formats. Single-character short options remain excluded from this joined form to avoid conflicts with short-option clustering.
Shell completion fixes
Shell completion saw fixes across Bash, zsh, fish, Nushell, and PowerShell:
- Fixed zsh completion missing the required
#compdefheader on the first line, which preventedcompinitfrom recognizing completion files saved under~/.zsh/completions/. (Need #compdef in the first line for zsh completion #766) - Fixed zsh file completion inheriting the caller's
sh_globoption, breaking extension-filtered native completion patterns like*.(json|yaml). (File path completion doesn't work #767) - Fixed zsh completion passing literal
$ext_patternto_filesinstead of expanding the variable. (zsh file completion passes literal$ext_patternto_filesinstead of expanding it #256, Fix zsh file completion passing literal$ext_patternto_files#639) - Fixed zsh completion not including hidden (dot-prefixed) files when
includeHiddenistrue. (shell completion backends ignoreincludeHidden: truefor file suggestions #262, Fix zsh completion ignoringincludeHidden: truefor file suggestions #641) - Fixed Bash completion scripts using
compgen -z, which is unsupported on macOS's default GNU Bash 3.2. File and directory completions now use glob-based iteration. (Bash file completion scripts use unsupportedcompgen -zon macOS default Bash #250, Fix Bash completion scripts using unsupportedcompgen -zon macOS default Bash #608) - Fixed PowerShell file completion stripping directory prefixes from nested path suggestions (for example,
src/now returnssrc/alpha.txtinstead of barealpha.txt). (PowerShell file completion strips directory prefixes from nested path suggestions #253, Fix PowerShell file completion stripping directory prefixes from nested paths #632) - Fixed Nushell file completion returning an empty list for non-empty path prefixes. (Nushell file completion returns an empty list for non-empty path prefixes #254, Fix Nushell file completion returning empty list for non-empty path prefixes like
src/#636) - Fixed fish, Nushell, and PowerShell completion scripts ignoring
includeHidden: truefor file suggestions. (Fish, Nushell, and PowerShell__FILE__parsers fail to strip tab-delimited metadata before comparinghiddenfield #618, Fix fish, Nushell, and PowerShell__FILE__parsers ignoringincludeHidden#619) - Fixed fish, Nushell, and PowerShell completions not enumerating hidden (dot-prefixed) files even when
includeHiddenistrue. (Fish, Nushell, and PowerShell completions do not enumerate hidden files even whenincludeHiddenis true #623, Fix hidden file enumeration in fish, Nushell, and PowerShell completions #624) - Fixed all five shells ignoring the
Suggestion.file.patternfield, causing file completions to enumerate the current directory instead of the pattern-specified path. (Shell completion scripts ignoreSuggestion.file.patternand complete the current directory instead #251, Fix shell completion scripts ignoringSuggestion.file.pattern#656) - Fixed all five shells not stripping leading dots from
Suggestion.extensions, causing extension filtering to silently fail. (Suggestion.extensionswith leading dots breaks shell completion extension filtering #647, Strip leading dots fromSuggestion.extensionsin shell completion encoding #650, Shell completion extension filters do not match the dot-prefixed extensions emitted bypath()#255, Fix shell completion extension filters to handle dot-prefixed extensions like.json#660) - Fixed shell completion scripts for all five shells emitting raw tabs and newlines in completion descriptions, corrupting the transport framing. (
encodeSuggestions()does not escape tabs or newlines in completion descriptions #247, FixencodeSuggestions()not escaping tabs or newlines in completion descriptions #642, Shell completion transports do not escape tabs or newlines in literal suggestion text #337, Escape tabs and newlines in literalSuggestion.textfor shell completion transports #663) - Fixed all five shells excluding directories when completing file-only suggestions, preventing users from descending into subdirectories. (Shell completion cannot navigate directories for file-only
Suggestion.fileentries #294, Fix shell completion excluding directories fortype: "file"suggestions #646) - Fixed the
__FILE__completion transport incorrectly encodingpatternvalues containing:(for example, Windows paths likeC:/...). Colons are now percent-encoded. (__FILE__completion transport cannot representpatternvalues that contain:#252, Fix__FILE__completion transport forpatternvalues containing:#616) - Fixed
--completionbeing recognized after the--options terminator. (The--completionoption ignores the--options terminator inrunParser()andrunWith*()#228, Fix--completionoption ignoring--options terminator inrunParser()andrunWith*()#686) - Fixed
--completionbypassing--help/--versionprecedence when help or version appears before the completion option. (The--completionoption bypasses help/version precedence inrunParser()#229, Fix--completionoption bypassing--help/--versionprecedence inrunParser()#689) - Fixed
runParser()crashing or showing help instead of handling completion when help-option names appear inside completion payloads. (runParser()can crash oncompletion ... -- --helppayloads #300, FixrunParser()crash oncompletion ... -- --helppayloads #599) - Fixed option-value completion leaking option-name candidates when the value prefix starts with
-. - Fixed
merge()andconcat()dropping dependency-aware suggestions when the dependency source and derived parser live in different sub-parsers. (Themerge()/concat()completion path drops dependency-aware suggestions #178, Fixmerge()/concat()suggest dropping dependency-aware completions #520) - Fixed dependency-aware completion ignoring
withDefault()source values. (Dependency-aware completion ignoreswithDefault()source values #186, Fixsuggest()ignoringwithDefault()on dependency sources #522)
Help and documentation output fixes
- Fixed
formatMessage(),formatUsage(), andformatDocPage()measuring string width using JavaScript.length(UTF-16 code units) instead of terminal display width. East Asian wide characters, combining marks, and emoji now wrap and align correctly. (Core text formatters measure UTF-16 code units instead of terminal display width #509, Fix formatters to measure terminal display width instead of UTF-16 code units #757) - Fixed
formatDocPage()producing lines far wider thanmaxWidthwhen the option term was wider thantermWidth. The term column now dynamically shrinks to share available width. (TheformatDocPage()formatter can ignore small but validmaxWidthvalues and emit much wider lines #513, FixformatDocPage()ignoring smallmaxWidthvalues #669) - Fixed multiple
formatDocPage()issues withmaxWidthenforcement for fixed-prefix sections such asUsage:,Examples:, andshowDefault/showChoicesdescription prefixes. (formatDocPage()fixed-prefix sections can exceedmaxWidth#672, FixformatDocPage()fixed-prefix sections exceedingmaxWidth#677) - Fixed
formatDocPage()rendering emptybrief,description,examples,author,bugs, andfooterfields as blank sections with stray whitespace. (formatDocPage()renders emptyMessagefields as blank sections and stray whitespace #472, FixformatDocPage()rendering emptyMessagefields as blank sections #728) - Fixed
formatUsage()andformatDocPage()leaving trailing whitespace when no visible usage terms remain after filtering. (formatUsage()adds trailing whitespace when no visible terms remain #473, FixformatUsage()trailing whitespace andformatDocPage()width validation #725) - Fixed
formatMessage(),formatUsage(), andformatDocPage()emitting a spurious leading newline when an oversized term is already at the start of a new line. (TheformatUsage()andformatUsageTerm()formatters pre-wrap oversize terms and leave dangling spaces #497, Fix oversize term pre-wrapping informatUsageTerm(),formatUsage(), andformatMessage()#730) - Fixed nested
optional()wrappers rendering double brackets ([[...]]) in help usage output. (Help usage rendering does not normalize nestedoptional(...)wrappers #290, Flatten nestedoptional()wrappers in usage output #745) - Fixed
getDocPage()exposing parser-owned usage terms and doc fragments by reference; mutating the returnedDocPageno longer corrupts the parser definition. (ThegetDocPage()helper exposes parser-owned usage and doc fragments by reference #500, FixgetDocPage()exposing parser-owned structures by reference #697) - Fixed
getDocPage()hanging when a parser returns success without consuming input. (ThegetDocPage()helper can hang when parser parsing makes no progress #493, FixgetDocPage()hanging when parser makes no progress #740) - Fixed
getDocPage()accepting aParseOptionsobject as theargsargument, makinggetDocPage(parser, { annotations })work without an explicit empty args array. (ThegetDocPage()helper cannot acceptannotationsas the second argument without an explicit empty args array #480, AllowParseOptionsas the second argument ofgetDocPage*()helpers #739) - Fixed
or()andlongestMatch()duplicating visible terms in documentation when branches share the same surface syntax. (or()andlongestMatch()duplicate visible terms in documentation when branches share the same surface syntax #432, Deduplicate visible terms inor()andlongestMatch()documentation #698) - Fixed
hidden: truecommands and options leaking through “Did you mean?” typo suggestions. (Typo suggestions can surfacehidden: truecommands and options #516, Filterhidden: trueterms from typo suggestions #690) - Fixed
getDocPage()preserving hidden terms from customDocFragmentsinstead of filtering them. (getDocPage()preserves hidden terms from customDocFragmentsinstead of filtering them #494, Filter hidden terms fromgetDocPage()and doc deduplication #720) - Fixed help output incorrectly interleaving meta items (
help,--help,--version) with user-defined commands when usinggroupto assign meta items to an already-existing named section. (help, --help, --version are interleaved with other commands #138) - Added
cloneUsageTerm(),cloneUsage(),cloneDocEntry(),cloneMessageTerm(), andcloneMessage()utilities for deep-copying parser documentation structures. (ThegetDocPage()helper exposes parser-owned usage and doc fragments by reference #500, FixgetDocPage()exposing parser-owned structures by reference #697) - Added
command()usageLineoption for customizing the command's own help-page usage tail, and anellipsisterm toUsageTermfor concise usage placeholders. (Overriding usage string #139) - Fixed
message()tagged template reusing interpolatedMessageTermobjects by reference. (Themessagetagged template reuses interpolatedMessageTermobjects by reference #505, Fixmessage()tagged template reusing interpolatedMessageTermobjects by reference #718) - Fixed
optionNames(),values(), andurl()message term constructors storing caller-owned arrays by reference. (Message term constructors expose caller-owned arrays andURLobjects by reference #506, Fix message term constructors exposing caller-owned arrays andURLobjects by reference #719)
Value parser correctness
- Fixed
ip()andcidr()withversion: "both"allowing IPv4-mapped IPv6 addresses to bypass IPv4 restrictions. (ip({ version: "both" })andcidr({ version: "both" })let IPv4-mapped IPv6 bypass IPv4 restrictions #339, Apply IPv4 restrictions to IPv4-mapped IPv6 inip()andcidr()#721) - Fixed
hostname()accepting dotted all-numeric strings (192.168.0.1,999.999.999.999) as valid DNS hostnames. (hostname()accepts dotted-quad numeric strings, including invalid IPv4-like input #376, Reject dotted all-numeric strings inhostname()#657) - Fixed
socketAddress()withhost: { type: "both" }allowing IP-shaped input to bypass IP restrictions by falling through to the hostname parser. (socketAddress({ host: { type: "both", ip: ... } })can bypass IP restrictions via hostname fallback #335, FixsocketAddress()IP restriction bypass via hostname fallback in"both"mode #714) - Fixed
socketAddress()accepting non-standard IPv4 literal forms (hex, octal, single decimal integer) as hostnames. (Detect alternate IPv4 literal forms (hex octets, single-integer) insocketAddress()#715, Detect alternate IPv4 literal forms insocketAddress()#717) - Fixed
socketAddress()hiding specific host and port validation errors behind a generic format error. Specific errors fromhostname(),ipv4(), andport()now propagate. (socketAddress()hides host and port validation failures behind a generic format error #322, Propagate sub-parser errors insocketAddress()#749) - Fixed
socketAddress()incorrectly splitting host-only inputs when a custom separator appears inside the hostname. (socketAddress({ separator })splits host-only inputs when the separator appears inside the hostname #360, FixsocketAddress()splitting host-only inputs when separator appears inside hostname #726) - Fixed
socketAddress()treating a trailing separator with no port (for example,"localhost:") as valid host-only input whendefaultPortis set. (socketAddress()treats an explicit empty port as if the port were omitted #325, FixsocketAddress()treating trailing separator as omitted port #759) - Fixed
hostname()accepting case variants and wildcard-localhost forms oflocalhostwhenallowLocalhost: false. (hostname({ allowLocalhost: false })only blocks lowercaselocalhost#321, Fixhostname()to reject case variants and wildcard forms oflocalhost#659) - Fixed
hostname()accepting wildcard labels outside the leftmost position or whenallowWildcardisfalse. (hostname({ allowWildcard: true })accepts wildcard labels outside the leftmost position #355, Fixhostname()accepting wildcard labels outside the leftmost position #661) - Fixed
email()withallowMultiplesplitting on commas inside quoted local parts and quoted display names. (email()breaks valid quoted commas whenallowMultipleis enabled #320, Fixemail()splitting on commas inside quoted strings whenallowMultipleis enabled #606) - Fixed
email()accepting IPv4-like dotted-quad domains (for example,user@192.168.0.1). (email()accepts IPv4-like numeric domains, including invalid ones like999.999.999.999#387, Reject all-numeric domains inemail()value parser #617) - Fixed
email()not enforcing RFC 5321 length limits (64-octet local-part, 254-octet overall, measured in UTF-8). (email()does not enforce local-part or overall address length limits #396, Fixemail()to enforce RFC 5321 local-part and address length limits #622) - Fixed
email({ lowercase: true })lowercasing the entire address instead of only the domain part. (email({ lowercase: true })lowercases the local part as well as the domain #352, Fixemail({ lowercase: true })to only lowercase the domain part #614) - Fixed
domain()accepting dotted numeric strings (192.168.0.1,999.999.999.999) as valid domains. (domain()accepts dotted-quad numeric strings such as IPv4 addresses as domains #375, Reject all-numeric dotted strings indomain()#634) - Fixed
domain()not enforcing the 253-octet total domain length limit. (domain()does not enforce the 253-octet domain length limit #395, Enforce 253-octet length limit indomain()#635) - Fixed
locale()format()dropping Unicode extension subtags. (locale().format()drops Unicode extension subtags by usingbaseName#317, Fixlocale().format()dropping Unicode extension subtags #565) - Fixed
format()inmacAddress(),domain(),ipv6(),ip(), andcidr()returning the metavar placeholder instead of the serialized value. (Several network-address value parsers returnmetavarfromformat()instead of the actual value #318, Fixformat()returning metavar in network-address value parsers, addnormalize()API for default canonicalization #742) - Fixed
url()emitting://for non-hierarchical URL schemes likemailto:andurn:. (url({ allowedProtocols })always suggests://, even for schemes likemailto:andurn:#342, Fixurl()suggestions for non-hierarchical URL schemes #678) - Fixed
string({ pattern })using statefulRegExpbehavior that could corruptlastIndexacross parse calls. The parser now creates a freshRegExpper parse. - Fixed
integer()in number mode silently rounding values outside the safe integer range. (integer()silently loses precision beyondNumber.MAX_SAFE_INTEGER#248, Rejectinteger()values outside safe integer range #525) - Fixed
float()accepting values that overflow toInfinitywhenallowInfinityisfalse. (float({ allowInfinity: false })still accepts overflow values asInfinity#242,float({ allowInfinity: false })should reject overflow values like1e309#528) - Fixed
macAddress()not normalizing single-digit octets; all octets are now zero-padded to two hex digits. (macAddress()accepts shortened octets even though it claims to validate 12-hex-digit MAC-48 addresses #319,macAddress({ outputSeparator })does not zero-pad single-digit octets during normalization #330, FixmacAddress()to reject single-digit octets in colon/hyphen formats #683, Zero-pad single-digit octets inmacAddress()normalization #723) - Fixed
cidr()discarding specific nested IPv4/IPv6 validation errors behind a generic error message. Added several new error hooks toCidrOptions.errors. (cidr()hides nested IP validation failures behind a generic CIDR error #333, Preserve nested IP validation errors incidr()#679) - Fixed
choice()accepting hex, binary, octal, and scientific notation via JavaScript'sNumber()coercion. (Numberchoice()parses hex, binary, octal, scientific, and whitespace-padded inputs viaNumber()coercion #315, Fix numberchoice()acceptingNumber()coercion forms #523) - Fixed several value parsers (
choice(),string(),uuid(),email(),domain(),url()) not snapshotting caller-owned mutable configuration at construction time, allowing post-construction mutations to silently change parse semantics. (Several value parsers retain caller-owned mutable config after construction #507, Snapshot mutable config inchoice(),string(),uuid(),email(),domain()#555) - Fixed
integer({ type: "bigint" }),port({ type: "bigint" }), andportRange({ type: "bigint" })accepting empty strings, whitespace, signed-plus strings, and non-decimal literals. (integer({ type: "bigint" })accepts empty strings, whitespace, and non-decimal literals #245,port({ type: "bigint" })andportRange({ type: "bigint" })accept non-decimal BigInt literals #249, Fixinteger({ type: "bigint" })andport({ type: "bigint" })accepting non-decimal inputs #566, Add regression tests forportRange({ type: "bigint" })input validation #572) - Added construction-time
RangeErrorfor contradictory range configurations (for example,min > max) ininteger(),float(),port(),portRange(), andcidr(). (Numeric parsers do not reject contradictorymin > maxconfigurations #349, Reject contradictorymin/maxin numeric parsers at construction time #583) - Added construction-time
RangeErrorfor non-finite bound values (NaN,Infinity,-Infinity) in numeric parsers. (Numeric parsers do not validate non-finite bound configuration likeNaNorInfinity#362, Reject non-finite bounds (NaN,Infinity) in numeric parsers #587)
Parser correctness and runtime fixes
- Fixed
multiple()not honoring its documented zero-or-more default when used as a top-level parser. Previously,parse(multiple(flag("-v")), [])failed with an error about unexpected end of input instead of returning[]. Note:withDefault(multiple(p), nonEmptyDefault)with the defaultmin: 0now returns[]rather than the configured default, becausemultiple()never reports a parse failure on empty input. To restore the old behavior, either setmin: 1on the innermultiple()or usemap(). (Themultiple()modifier does not honor its default zero-or-more semantics when used standalone #408, Make standalonemultiple(p, { min: 0 })return[]on empty input #776) - Fixed
optional()andwithDefault()discarding values from parsers whose result is produced duringcomplete()rather thanparse()(for example,constant(),bindEnv(),bindConfig()). (RemoveoptionalStyleWrapperKeymarker; useExecutionContext.phaseto distinguishprompt()probe from real complete #233, Fixoptional()/withDefault()dropping complete-only values and removeoptionalStyleWrapperKey#775) - Fixed
conditional()with an async discriminator that succeeds without consuming input, which previously caused “Unexpected option or argument” errors when branch-specific tokens were present. (conditional()with async zero-consuming discriminator cannot parse branch-specific tokens #772, Speculatively parse named branches for deferredconditional()discriminators #774) - Fixed
derive()andderiveFrom()eagerly executing default factories during parser construction to detect sync/async mode. The factory is no longer called at construction time; callers must now provide amodefield ("sync"or"async"). (derive(),deriveAsync(), andderiveFrom()eagerly execute default factories during parser construction #223, Fixderive()andderiveFrom()eagerly executing factories at construction #527) - Fixed
deriveSync(),deriveAsync(),deriveFromSync(), andderiveFromAsync()so errors thrown by the factory with default dependency values no longer prevent deferred resolution from succeeding with the actual values. (deriveSync()andderiveFrom*()still touch the default factory branch during parse even when dependencies are provided #225, Fix derived parsers touching factory default branch duringparse()even when dependencies are provided #524) - Fixed
merge()not pre-completingbindEnv()/bindConfig()-backed dependency sources for cross-parser resolution. (merge()does not pre-complete env/config-backed dependency sources for cross-parser resolution #681, Fixmerge()not pre-completing env/config-backed dependency sources for cross-parser resolution #684) - Fixed
withDefault()default thunks being evaluated more than once in nestedmerge()compositions. (Decouple dependency-system internals fromoption()/argument()and modifier implementations #750, Avoid re-evaluating nested source defaults inmerge().complete()when runtime already seeded #762, Avoid re-evaluating nested source defaults inmerge().complete()when runtime already seeded #763) - Fixed
map(withDefault(option(..., dependencySource), default), transform)leaking raw dependency defaults through shared-buffer constructs. (map(withDefault(option(..., dependencySource), ...))can leak raw default values through constructs #239, Lock inmap(withDefault(option(...), default), transform)defaults in shared-buffer constructs #779) - Fixed
runWith()andrunWithSync()discarding the original parse error when a source context's disposal also throws. The disposal error is now wrapped in aSuppressedError. (Parse failures inrunWith()andrunWithSync()are overwritten by context disposal errors #246, Fix parse errors being overwritten by disposal errors inrunWith()andrunWithSync()#771) - Fixed
runWith()andrunWithSync()aborting two-phase context collection too early when the first pass had already parsed enough data to identify context inputs but had not completed successfully. (TherunWith()runners skip phase two when the first pass fails #180, KeeprunWith()phase two alive after seedable first-pass failures #780) - Fixed
runParser()duck-typing meta-command results based on field names likehelpandversion. Internal meta results are now branded with a private symbol. (Duck-typed result classification inrunParsercould collide with user data #152) - Fixed
runParserSync()andrunWithSync()accepting async parser objects at runtime and returningPromises instead of throwing. (Sync-only runner APIs accept async parser objects at runtime and returnPromises #279, Reject async parsers in sync-only runner APIs at runtime #676) - Fixed
or()crashing with an internalTypeErrorwhen parsing started from an annotation-injected initial state. (Theor()combinator can crash when annotations are present #183) - Fixed
argument()andcommand()misinterpreting annotation-injected initial state as real parser-local state. (Theargument()andcommand()parsers misinterpret annotated initial state #187, Keep annotated state transparent inargument()andcommand()#781) - Fixed duplicate option-name validation to include
hidden: trueoptions inobject(),tuple(),merge(), and wrappedgroup()parsers. (Duplicate option-name checks ignorehidden: trueterms #510, Honor hiddenoption()terms in duplicate checks #788) - Added construction-time validation for option names in
option()andflag(), command names incommand(), labels inobject()/tuple()/merge()/group(), and program names inrunParser()anddefineProgram(). Invalid values (empty, whitespace-only, containing control characters, etc.) now throwTypeErrorat construction time instead of producing broken output later. (logOutput()andloggingOptions()do not validate invalid runtime option names like""#381, Validate option names at runtime inoption()andflag()#709,command()does not validate empty or whitespace-containing runtime command names #401, Validate command names incommand()at construction time #732, Label-aware documentation wrappers do not validate empty, whitespace-only, or multiline runtime labels #404, Validate labels at construction time inobject(),tuple(),merge(), andgroup()#737,runParser()does not validate malformed runtime program names #428, Validate program names inrunParser()anddefineProgram()#743) - Expanded fully inferred overloads for
or(),merge(),concat(), andlongestMatch()from 10–15 to 15 arguments. Calls with more than 15 arguments now fail at compile time with an actionable message suggesting nested composition instead of silently degrading tounknown. (mention the type-level limit of combinators in docs #142, Improveor()arity typing up to 15 and add overflow guidance #143, Unify arity inference limits and overflow diagnostics formerge(),concat(), andlongestMatch()#144, Set explicit 15-arity limits for combinators with safe inference #145)
@optique/config improvements
- Removed the hidden process-global fallback from
bindConfig(). CallingconfigContext.getAnnotations()manually no longer affects later plain parses unless the returned annotations are passed explicitly or the parser is run through a context-aware runner. (Active source registries can leak stale env/config data into later direct parses #234, Stale config metadata can leak into laterbindConfig()key callbacks when a newer load returnsmeta: undefined#272, Stop leaked env/config fallback state from affecting later parses #785) - Fixed
bindConfig()not propagating dependency source values to derived parsers. (ThebindEnv()/bindConfig()wrappers hide dependency source values #179, FixbindEnv()/bindConfig()hiding dependency source values from derived parsers #680) - Fixed
bindConfig()composition withbindEnv(): when no CLI token is consumed,bindConfig()no longer incorrectly marks the result as “CLI-provided,” which was causingbindEnv(bindConfig(...))to skip the environment variable fallback. - Fixed
createConfigContext()breaking sync runner flows when config loading and schema validation complete synchronously. (runSync()breaks when used with config contexts #159, Restore sync config contexts #162) - Fixed
createConfigContext()treating falsy first-pass parse results (0,false,"") as the phase-one sentinel. (createConfigContext()treats falsy parsed values as the phase-one sentinel and skips config loading #161, Fix falsy parsed values increateConfigContext()#164) - Added construction-time validation of
schema,fileParser, and related options tocreateConfigContext(). (createConfigContext()does not validate malformed runtimeschemaorfileParservalues #391, Validate malformedschema,fileParser,load, andgetConfigPathincreateConfigContext()#605) - Fixed custom
load()mode being unable to represent “no config found” without failing schema validation. Returningundefinedornullfromload()now correctly signals “no config data available.” (Customload()mode in@optique/configcannot represent “no config found” without failing validation #236, Allowload()to returnundefined/nullas a "no config" signal #770)
@optique/git fixes
- Fixed
gitCommit()andgitRef()suggesting ambiguous 7-character SHA prefixes when multiple recent commits share the same short prefix. Short SHAs are now lengthened until each suggestion is unique. (gitCommit()andgitRef()can suggest ambiguous 7-character SHAs that their own parsers reject #331, FixgitCommit()andgitRef()suggesting ambiguous short SHA prefixes #571) - Fixed
gitRef()emitting duplicate completion suggestions when a branch and tag share the same name. (gitRef()emits duplicate completion suggestions when a branch and tag share the same name #284, FixgitRef()emitting duplicate completion suggestions #569) - Fixed
gitRemoteBranch()reporting a misleading “branch not found” error when the specified remote does not exist. AddedremoteNotFoundtoGitParserErrorsfor custom error messages. (gitRemoteBranch()misdiagnoses missing remotes as missing branches #308, FixgitRemoteBranch()misdiagnosing missing remotes as missing branches #603) - Fixed error messages for
gitBranch(),gitRemoteBranch(),gitTag(), andgitRemote()producing malformed sentences (“Available branches: .”) when the list is empty. (Empty list message helpers can collapse surrounding prose into malformed sentences #492, Require fallback parameter invalueSet()for empty list safety #747) - Added construction-time validation of
remoteingitRemoteBranch(). (gitRemoteBranch()does not validate malformed runtime remote names #464, Validate malformedremoteingitRemoteBranch()#654) - Added construction-time validation of
suggestionDepthingitCommit(),gitRef(), and related functions. (gitCommit()andgitRef()do not validate invalidsuggestionDepthvalues #377, ValidatesuggestionDepthin git parser functions #570)
@optique/logtape fixes
- Fixed
createSink()misreportinggetFileSink()factory errors as a missing@logtape/filepackage. (createSink()misreports file-sink factory failures as missing@logtape/file#299, FixcreateSink()misreporting file-sink factory failures as missing@logtape/file#702) - Fixed
createSink()failing on Deno when installed from JSR because@logtape/filewas not declared in the package's import map. (@optique/logtapecannot load file sinks in Deno because@logtape/fileis not declared indeno.json#329, FixcreateSink()failing on Deno due to missing@logtape/fileimport indeno.json#703) - Fixed
createConsoleSink()ignoring falsy timestamps like0(the Unix epoch) and substituting the current time. (createConsoleSink()ignores falsy timestamps like0and substitutes the current time #311, FixcreateConsoleSink()ignoring falsy timestamps like0#705) - Fixed
createConsoleSink()silently treating invalidstreamandstreamResolverreturn values as stdout. (createConsoleSink()silently treats invalidstreamResolverreturn values as stdout #379, FixcreateConsoleSink()silently treating invalid stream values as stdout #707) - Added construction-time validation for log level options in
debug(),verbosity(), andloggingOptions(). (debug()andloggingOptions({ level: "debug" })do not validate runtime log level values #430, Validate runtime log level options indebug(),verbosity(), andloggingOptions()#711)
@optique/man improvements
- Fixed
generateManPage*()functions droppingbrief,description, andfooterfromProgrammetadata. (ThegenerateManPage*()helpers dropbrief,description, andfooterfromProgrammetadata #260,generateManPage*(): forwardbrief,description, andfooterfromProgrammetadata #598) - Fixed
formatDocPageAsMan()labeling untitled sections asOPTIONSregardless of content type. Untitled command-only sections now render asCOMMANDS, and argument-only sections asARGUMENTS. (TheformatDocPageAsMan()formatter labels untitled command sections asOPTIONS#261, FixformatDocPageAsMan()mislabeling untitled sections as OPTIONS #601) - Fixed numerous roff escaping issues: hyphens and roff special characters in program names and command names
formatDocPageAsMan()does not escape hyphens in program, command, orSEE ALSOnames #274, Escape hyphens in program names, command names, andSEE ALSOreferences informatDocPageAsMan()#542, double quotes insidevalue()termsformatMessageAsRoff()does not escape double quotes insidevalue()orvalues()terms #273, FixformatMessageAsRoff()not escaping double quotes invalue()/values()terms #544,literalterms starting with.or'formatDocPageAsMan()emits standalone raw text without roff line-start escaping #297, Escapeliteralusage terms in roff output to prevent control-line interpretation #559, section titles containing backslashes or double quotesformatDocPageAsMan()emits raw section titles into.SHmacros #301, Escape section titles in.SHroff macros #560, backslashes in metavar valuesformatUsageTermAsRoff()andformatDocPageAsMan()drop backslashes from usage and doc terms #298, FixformatUsageTermAsRoff()andformatDocPageAsMan()dropping backslashes from metavar values #563, and program names in.THand.BRmacros containing spaces or quotesformatDocPageAsMan()emits raw program andSEE ALSOnames into roff macros #302, Quote program andSEE ALSOnames in roff macros #574. - Fixed
formatUsageTermAsRoff()rendering optional and Boolean options with duplicated brackets ([[--host STRING]]) in the SYNOPSIS section. (ThegenerateManPage()formatter renders optional and boolean options with duplicated brackets #197, FixformatUsageTermAsRoff()rendering duplicated brackets in SYNOPSIS #586) - Fixed
generateManPageSync()silently producing output for async parsers instead of throwing. (Sync@optique/manAPIs accept async parser objects at runtime #291, Reject async parsers ingenerateManPageSync()at runtime #584) - Added
brief,description, andfootertoManPageOptionsso they can be passed as explicit overrides in both the parser-based and program-based APIs. (ThegenerateManPage*()helpers dropbrief,description, andfooterfromProgrammetadata #260,generateManPage*(): forwardbrief,description, andfooterfromProgrammetadata #598) - Fixed the
optique-manCLI not recognizing.tsxand.jsxinput files as needing thetsxloader on Node.js. (optique-mandoes not recognize.tsxinput as TypeScript on Node.js #280, Fixoptique-mannot recognizing.tsxand.jsxinput files on Node.js #534) - Fixed
optique-maninferring an empty program name for extensionless input files. (optique-maninfers an empty program name for extensionless input files #277, Fixoptique-maninferring empty program name for extensionless input files #551)
@optique/valibot and @optique/zod improvements
valibot()andzod()now expose choice metadata for picklist / enum schemas, enabling shell completion suggestions andshowChoicesin help text. (zod(z.enum(...))andvalibot(v.picklist(...))drop choice metadata for help/completion #281, Expose choice metadata fromzod()andvalibot()for enum-like schemas #688)valibot()andzod()now format transformed non-primitive values intelligently instead of producing[object Object]. A newformatoption allows custom formatting. (@optique/zodand@optique/valibotformat transformed object values as[object Object]in help output #285, Fixformat()for transformed non-primitive values in@optique/zodand@optique/valibot#706)zod()now uses CLI-friendly Boolean parsing forz.boolean()andz.coerce.boolean()schemas, acceptingtrue/false,1/0,yes/no,on/off. (@optique/zodpasses dangerousz.coerce.boolean()semantics through to CLI parsing #295,zod(): use CLI-friendly boolean parsing forz.boolean()andz.coerce.boolean()#712)valibot()andzod()now throwTypeErrorat construction time when given an async schema, instead of silently skipping async validations. (zod()propagates raw errors for unsupported async schemas #462, Reject unsupported async schemas inzod()andvalibot()#701)
@optique/temporal improvements
- Temporal parsers now throw
TypeErrorwhenglobalThis.Temporalis unavailable, instead of returning a generic “invalid format” error. (@optique/temporalreports valid input as format errors whenTemporalis unavailable #282, ThrowTypeErrorwhenTemporalAPI is unavailable instead of format errors #561) - Fixed the
TimeZonetype to include single-segment IANA timezone identifiers such as"GMT","EST", and deprecated aliases like"Japan". (timeZone()accepts and suggestsGMT, but exportedTimeZoneexcludes it #304, WidenTimeZonetype to include single-segment IANA identifiers #596)
Installation
deno add --jsr @optique/core @optique/run # Deno npm add @optique/core @optique/run # npm pnpm add @optique/core @optique/run # pnpm yarn add @optique/core @optique/run # Yarn bun add @optique/core @optique/run # Bun
For the new environment variable integration:
deno add jsr:@optique/env # Deno npm add @optique/env # npm pnpm add @optique/env # pnpm yarn add @optique/env # Yarn bun add @optique/env # Bun
For the new interactive prompt integration:
deno add jsr:@optique/inquirer # Deno npm add @optique/inquirer # npm pnpm add @optique/inquirer # pnpm yarn add @optique/inquirer # Yarn bun add @optique/inquirer # Bun
Full documentation is available at optique.dev. The complete changelog is at optique.dev/changelog.