Optique changelog
Version 1.0.0
To be released.
@optique/core
Fixed duplicate option-name validation to include
hidden: trueoptions inobject(),tuple(),merge(), and wrappedgroup()parsers. Hidden options still consume CLI syntax, so hidden-visible and hidden-hidden collisions now throwDuplicateOptionErrorduring parser construction by default. [#510, #788]Replaced the sentinel-based two-pass
SourceContextcontract with an explicitSourceContextRequestobject.getAnnotations()andgetInternalAnnotations()now receivephase: "phase1"/"phase2"requests, so successful first-pass values ofundefinedare no longer ambiguous. This removes the need for context-localundefinedwrapping workarounds and fixes custom two-pass contexts that previously could not distinguish phase 1 from a realundefinedparse result. [#271, #786]Added the optional
Parser.validateValue()method, which lets a parser check whether an arbitrary value satisfies its underlyingValueParser's constraints (e.g., regex patterns, numeric bounds,choice()values). Built-in primitives (option(),argument()) implement it via aformat()+parse()round-trip; combinator wrappers (optional(),withDefault(),group(),command(),nonEmpty()) forward it from inner parsers.multiple()validates each array element through the inner parser and additionally enforces its ownmin/maxarity rules against the array length. Dependency-derived value parsers (deriveFrom/derive) are exempt because theirformat()rebuilds from default dependency values rather than the live resolved values.map(),or(),longestMatch(),merge(), andconcat()intentionally do not forward this method. Used bybindEnv()andbindConfig()to enforce parser constraints on fallback values. [#414, #777]Added
provisionalfield to the success variant ofParserResult. Whentrue, it indicates that the parse succeeded tentatively: the parser matched something (e.g., a zero-consuming discriminator resolved to a branch key) but the selected sub-parser has not consumed any input yet. Outer combinators likeor()use this to avoid treating provisional successes as definitive zero-consumed fallback candidates. [#232, #773]Added
ParseFrame,ExecutionContext, andExecutionPhasetypes to support the separation of parser-local state from shared execution context.ParserContextnow includes an optionalexecfield for accessing the shared execution context. [#750, #751, #752, #753, #756, #760, #761]Shared-buffer constructs (
object(),tuple(),concat(),merge()) now use a centralized dependency runtime for source collection, default filling, and derived-parser replay. Dependency-aware replay now reads raw user input from a sharedInputTraceinstead of relying on the old dependency-state wrapper protocol. The runtime is shared across nested constructs viaExecutionContext.dependencyRuntime. Aside from thewithDefault()aftermap()fallback change noted below, user-facing behavior is unchanged. [#750, #753, #755, #761, #765]Top-level
parseSync()andparseAsync()now create aDependencyRuntimeContexttogether with anInputTrace, so top-level primitive parsers and construct-owned parsers now share the same dependency replay model. Top-levelsuggestSync()andsuggestAsync()likewise build a dependency runtime from the current parser state so dependency-aware suggestions use the same registry model as construct-owned parsers. [#750, #754, #755, #764, #765]Fixed wrapped dependency sources in shared-buffer constructs.
tuple()andconcat()now seed dependency-aware parse, completion, and suggestion flows through the same wrapper contract used byobject()andmerge(), sobindConfig(),bindEnv(), andprompt()compositions expose fallback-resolved source values to derived parsers consistently.tuple().suggest()also keeps explicit source matches and validation failures sticky, so later derived suggestions no longer fall back to defaults after an invalid source token. [#750, #768, #769]Removed the last built-in uses of the old dependency-state bridge in
option(),argument(),optional(),withDefault(), andmultiple(). These parsers now keep plain parser-local state and rely on metadata, the shared runtime, and the input trace for dependency behavior. [#750, #755, #765]Defaults applied after
map()no longer act as dependency-source fallbacks. They affect only the mapped output value unless the underlying dependency source was provided explicitly. [#750, #755, #765]Fixed
map(withDefault(option(..., dependencySource), default), transform)leaking raw dependency defaults through shared-buffer constructs.object(),tuple(), andmerge()now preserve the mapped fallback value on the no-input path, matching top-level parsing. This does not change dependency-resolution semantics: mapped fallbacks still do not register as dependency-source values for downstream derived parsers. [#239, #779]Fixed redundant replay of derived-parser factories when all dependency values came from defaults. Previously,
resolveSingleDeferred()always re-invoked the user-supplied factory even when the resolved values were identical to those already used for the preliminary result duringparse(). Non-idempotent factories (e.g., those building parsers from mutable state) could see a second evaluation that rejected input the first evaluation accepted. [#750, #754, #764]Fixed the public
parse(),parseSync(),parseAsync(),suggest(),suggestSync(),suggestAsync(),getDocPage(),getDocPageSync(), andgetDocPageAsync()entrypoints treatingannotations: {}as a state-wrapping operation instead of a no-op. Previously, passing an empty annotations object caused primitive parser states to be wrapped in an injected annotation wrapper and non-primitive states (plain objects, arrays,Date,Map,Set,RegExp, and class instances) to be cloned, so custom parsers could observe a changed state shape or identity even though there was no annotation data to carry. An annotations record with no own symbol keys now behaves identically to omitting theannotationsoption entirely:injectAnnotations()short-circuits,injectAnnotationsIntoState()bypasses injection, and the top-levelparseSync()/parseAsync()unwrap step is no longer triggered. [#484, #778]Fixed
withDefault()default thunks being evaluated more than once in nestedmerge()compositions. When an outermerge()Phase 1 had already seeded a dependency source value, nestedobject()/merge()children would re-evaluate the same default thunk during their own Phase 1. Non-idempotent thunks (e.g., with side effects) could produce inconsistent values. [#750, #762, #763]Fixed
multiple()continuing to open fresh item slots after reaching itsmaxbound. Once the bound is reached,multiple()now stops consuming additional input so later parsers in shared-buffer compositions can see the remaining tokens instead of having them absorbed into an overfullmultiple()state. [#755, #765]Fixed
multiple()failing to honor its documented zero-or-more default when used as a top-level parser. Previously,parse(multiple(flag("-v")), [])reported “Expected an option, but got end of input.” instead of succeeding with[], even thoughmultiple()is documented as “zero or more”. The empty-array semantics only worked whenmultiple()was wrapped inobject(),optional(), orwithDefault(), which absorbed the inner parser's failure on their own.multiple(p, { min: 0 }).parse()(the defaultmin) now absorbs zero-consumption inner failures and defers tocomplete(), which returns[]— mirroring howoptional()already handles the same case.multiple()withmin > 0still propagates zero-consumption inner failures so that outer wrappers likeoptional()andwithDefault()continue to absorb them, so for exampleparse(optional(multiple(p, { min: 1 })), [])still resolves toundefinedandparse(withDefault(multiple(p, { min: 1 }), fallback), [])still resolves tofallback.withDefault(multiple(p), nonEmptyDefault)(with the defaultmin: 0), however, no longer falls back to its configured default on empty input because the wrappedmultiple()never reports a parse failure: such compositions now return[]frommultiple()directly. Users who want the previous “use default when there are no matches” behavior can either setmin: 1on the innermultiple()or wrap the result withmap(), e.g.map(multiple(p), xs => xs.length > 0 ? xs : fallback). [#408, #776]Parser.complete()andParser.shouldDeferCompletion()now accept an optionalExecutionContextparameter. All built-in parser implementations (option(),argument(),optional(),withDefault(),map(),object(),tuple(),merge(),concat(), etc.) forward this parameter through the call chain. Existing implementations without the parameter continue to work. [#750, #751, #752, #756, #760]Documented and regression-tested
runWith()cleanup ordering for async parser completion.Symbol.dispose/Symbol.asyncDisposenow explicitly guarantee that cleanup begins only after the fullrunWith()promise settles, including later asynchronouscomplete()work. [#269]Added
createParserContext()factory function for constructingParserContextfrom aParseFrameand anExecutionContext. [#750, #751, #752, #756, #760]Added generalized APIs to
SourceContextandParserinterfaces so that packages like @optique/config and @optique/inquirer can integrate with core through public interfaces instead ofSymbol.for()+Reflect.get()duck typing:SourceContext.getInternalAnnotations(): optional method for contexts to inject additional annotations during collectionSourceContextRequest: explicit phase-1 / phase-2 request object forSourceContext.getAnnotations()andSourceContext.getInternalAnnotations()Parser.shouldDeferCompletion(): optional method that combinators (optional(),withDefault(),group()) forward from inner parsers
This removes core's hidden dependency on @optique/config and @optique/inquirer implementation details, enabling third-party alternative implementations. [#588]
Added
optionsparameter toSourceContext.getAnnotations(). Contexts now receive runtime options (e.g.,getConfigPath,load) passed by the runner, enabling config contexts to load files without a separaterunWithConfig()wrapper. [#110]Renamed
SourceContext._requiredOptionsto$requiredOptionsto follow the$prefix convention used byValueParser.$mode,Parser.$mode, etc. [#110]Added optional
Symbol.disposeandSymbol.asyncDisposemethods toSourceContext. Contexts that hold resources (e.g., global registries) can now implementDisposable/AsyncDisposablefor automatic cleanup.runWith()andrunWithSync()call dispose on all contexts in afinallyblock. [#110]Context-required options passed to
runWith(),runWithSync(), andrunWithAsync()must now be wrapped in acontextOptionsproperty instead of being passed as top-level keys. This prevents name collisions with runner options such asargs,help, andcolors. [#240, #241, #575, #581]Added
fail<T>()parser: always fails without consuming input, declared to produce a value of typeT. Its primary use is as the inner parser forbindConfig(fail<T>(), { … })when a value should come only from a config file and has no corresponding CLI flag. [#120]Fixed
runWith(),runWithSync(), and the higher-levelrun()helpers aborting two-phase context collection too early when the first pass had already parsed enough data to identify context inputs but still did not complete successfully. Dynamic contexts now receive a best-effort first-pass value extracted from parser state in that case, so compositions likebindConfig(fail<T>(), …)can still resolve config-only required values. Deferred or otherwise unresolved fields in that phase-two value may beundefined. Phase two is still skipped when no usable first-pass seed exists. [#180, #780]Fixed
runWith(),runWithSync(), and the higher-levelrun()helpers preserving a context's phase-1 annotations into the final parse when that same context returned an empty annotation object in phase 2. In two-phase runs, each context's phase-2 annotation set is now treated as that context's final snapshot for the second parse pass. Returning{}from phase-twogetAnnotations()now clears that context's earlier phase-1 contribution instead of letting stale data override later contexts. [#231, #782]Fixed
runParser(),runWith(), andrunWithSync()treating built-in help, version, and completion tokens as globally reserved. These runner features are now parser-aware: command and option forms only trigger when the user parser leaves them unconsumed, so positional values likehelpand option values like--helpcan be parsed as ordinary data. Startup validation now also permits user parsers to reuse built-in meta names and aliases. [#230, #784]Breaking change: Replaced
SourceContext's inferredmodecontract with an explicit requiredphasefield whose value must be"single-pass"or"two-pass".SourceContextMode,SourceContext.mode, andisStaticContext()have been removed.runWith(),runWithSync(), and higher-level runners now decide whether to perform phase 2 solely fromcontext.phase, which fixes contexts that emit non-empty phase-1 annotations and still need phase-2 refinement.createEnvContext()now declaresphase: "single-pass"andcreateConfigContext()declaresphase: "two-pass". Consumers must migrate custom contexts to this new API contract. [#243, #783]Fixed
or()crashing with an internalTypeErrorwhen parsing started from an annotation-injected initial state. Exclusive branch selection now treats annotations as transparent parser context, so annotated calls throughparse(),parseAsync(), and context-aware runners (run()/runSync()) either select a branch normally or return an ordinary parse failure instead of throwing. [#183]Fixed
or()andlongestMatch()throwing an internalTypeErrorwhile generating suggestions from an annotation-injected initial state. Annotated calls throughsuggest(),suggestSync(),suggestAsync(), and typo-driven “Did you mean?” error flows now treat exclusive parser state as transparent runtime context, so they return ordinary completion candidates or no suggestions instead of crashing. Wrapper compositions that forward exclusive suggestions, such asgroup(or(...))andmap(longestMatch(...)), inherit the fix as well. [#184]Fixed
argument()andcommand()misinterpreting annotation-injected initial state as real parser-local state. Annotated calls throughparse(),suggest(),runWith(),runWithSync(),run(), andrunSync()now treat annotations as transparent runtime context for these primitives, so positional arguments, subcommands, and top-level value-suggestion flows behave the same way they do without annotations. Wrapper compositions that forward primitive state directly, such asgroup(argument(...)), inherit the fix as well. [#187, #781]Fixed top-level
multiple()dropping suggestions when annotations are present around wrapped parsers with meaningful suggestion behavior such asargument(choice(...))andcommand(...). Annotated calls through the publicsuggest(),suggestSync(), andsuggestAsync()entrypoints now preserve the wrapped parser's completion behavior for baremultiple(...)roots and forwarding wrappers likegroup(multiple(...)). [#189]Expanded
or()'s fully inferred overloads from 10 to 15 parser arguments, so large alternative sets keep precise union inference without collapsing tounknownat 11+ arguments. [#142, #143]Added a type-level arity guard for
or()calls with more than 15 parser arguments. Instead of degrading tounknown, oversized calls now fail at compile time with an actionable message that recommends nestingor()calls. [#142, #143]Expanded
merge()'s fully inferred overloads from 10 to 15 parser arguments, so larger merged parser sets keep precise object inference instead of degrading unexpectedly. [#144, #145]Expanded
concat()andlongestMatch()fully inferred overloads from 5 to 15 parser arguments, so larger compositions keep precise tuple/union inference. [#144, #145]Added type-level arity guards for
merge(),concat(), andlongestMatch()calls above 15 parser arguments. Oversized calls now fail at compile time with actionable messages that recommend nested composition. [#144, #145]Added runtime validation to
or(),longestMatch(),merge(), andconcat(): these combinators now throwTypeErrorat construction time when called with zero parser arguments. Previously they relied only on compile-time arity guards, so JavaScript callers could create parsers with inconsistent behavior. [#403, #696]Added per-argument runtime validation to
or(),longestMatch(),merge(), andconcat(): each parser argument is now checked to be a validParserobject at construction time, producing a clearTypeErrorinstead of an internal crash when non-parser values are passed. [#406, #700]Added runtime validation of option names to
option()andflag(): these functions now throwTypeErrorat construction time for empty strings, whitespace-only strings, strings with control characters, strings with embedded whitespace, the options terminator"--", or strings without a valid prefix. TheOptionNametype now enforces at least one character after the prefix at compile time in most cases, though"--"can still type-check and is rejected at runtime only. Theoption()andflag()overloads now require at least one option name argument. [#381, #709]Added runtime validation of program names to
formatUsage()and broadened existing validation informatDocPage(). Both functions now throwTypeErrorfor non-string values, empty strings, whitespace-only strings, and strings with control characters. PreviouslyformatUsage()had no validation at all, andformatDocPage()only rejected newlines. [#431, #724]Added runtime validation of the command name to
command(): the function now throwsTypeErrorat construction time for empty strings, whitespace-only strings, strings with embedded whitespace, and strings with control characters. [#401, #732]Added meta name collision detection to
runParser(). The runner rejects collisions among built-in meta features (help, version, completion), including option aliases that shadow completion'sname=valueprefix form. User-defined parser names are no longer rejected here; runner meta handling is now parser-aware, so ordinary parser data may reuse built-in meta names and aliases when the parser consumes them first. [#227, #736, #230, #784]Added
leadingNamesandacceptingAnyTokenproperties to theParserinterface. Each combinator now reports which leading tokens (option names, command names, and literal values) it could match at the first buffer position, computed from its structural semantics rather than the display-orientedusagetree. Shared-buffer compositions (tuple(),object(),merge(),concat()) use priority ordering andacceptingAnyTokento exclude names that are unreachable behind a catch-all parser likeargument().conditional()with a default branch now includes the default branch's leading names. This replaces the usage-tree-basedextractLeadingOptionNames(),extractLeadingCommandNames(), andextractLeadingLiteralValues(), which produced incorrect results fortuple()(priority-based sorting),command()(inner usage spreading), andconditional()(missing literal terms for argument discriminators). [#735, #741]Simplified
UserParserNamesinterface: replacedleadingOptions,leadingCommands, andleadingLiteralswith a singleleadingNamesset. [#735, #741]Added
extractLiteralValues()to @optique/core/usage.extractOptionNames()andextractCommandNames()now accept an optionalincludeHiddenparameter for callers that needhidden: trueterms included in the result. [#227, #736]Added optional
optionValueproperty to theliteralvariant ofUsageTerm. Whentrue, the literal was derived from an option's metavar byconditional()and represents an option value rather than a standalone positional token. [#734, #738]Breaking change: Added
placeholderproperty toValueParserinterface (required) andParserinterface (optional). Every value parser now provides a type-appropriate stand-in value (e.g.,""forstring(),1forport()) used during deferred prompt resolution.option()andargument()setParser.placeholderfrom the value parser, and combinators likemap(),optional(), andwithDefault()propagate it through the parser chain. [#407, #727]Added optional
placeholderoverride tostring(),integer(),float(), andport()options. This allows customizing the stand-in value when downstreammap()transforms or constraints require a specific value.integer()andfloat()automatically derive placeholders frommin/maxconstraints;port()defaults tomin(which itself defaults to1). [#407, #727]Replaced the
DeferredPromptValuesentinel with type-appropriate placeholder values during two-phase parsing.prompt()now usesParser.placeholderinstead of a branded sentinel when deferring, somap()transforms always receive valid values and two-pass contexts observe structurally valid objects. Note thatmap()intentionally does not propagatedeferredKeys, so mapped structured outputs may still carry placeholder values for deferred fields. This removes the entire sanitization machinery (~1000 lines of proxy-based stripping code) from @optique/core and @optique/config. [#307, #407, #727]Breaking change: Removed
placeholdersymbol andisPlaceholderValue()from@optique/core/context. These were part of the sentinel-based deferred prompt mechanism that has been replaced by theValueParser.placeholderapproach. [#407, #727]Added the
@optique/core/mode-dispatchsubpath export so sibling packages can share internal sync/async dispatch helpers without duplicating them. [#157]Changed
formatMessage()to render double newlines (\n\n) intext()terms as double newlines in the output, instead of collapsing them to a single newline. This makes paragraph breaks visually distinct from explicitlineBreak()terms, which render as a single newline.Changed the default section ordering in help output to use a smart 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 so the main (untitled) section appears before titled sections of a similar classification. Within each group, the original relative order is preserved. Previously, untitled sections were sorted first regardless of content type. This is a breaking change for help output layout. [#115]
Added
sectionOrderoption toDocPageFormatOptions(informatDocPage()),RunOptions(inrunParser()), andRunOptions(in @optique/run'srun()). When provided, the callback overrides the default smart sort to give full control over section ordering in help output. [#115]Added display-oriented command usage customization:
UsageTermnow supports anellipsisterm for concise usage placeholders.command()now accepts ausageLineoption (Usageor callback) to customize the command's own help-page usage tail.
This allows compact command help output such as
Usage: myapp config ...without changing parse behavior or completion behavior. [#139]Fixed
formatDocPage()rendering emptyMessagearrays as blank sections and stray whitespace. Emptybrief,description,examples,author,bugs, andfooterfields are now treated as absent. [#472, #728]Fixed
formatUsage()andformatUsageTerm()leaving trailing whitespace when no visible usage terms remain after filtering (e.g., empty usage array or all-hidden terms) or when inter-term separator spaces precede a line wrap.formatDocPage()also no longer renders a trailing space in theUsage:line for empty usage. ThemaxWidthvalidation informatDocPage()now uses content-aware width estimation based on visible usage terms, replacing the previous fixed-offset heuristic. [#473, #725]Fixed
formatUsage(),formatUsageTerm(), andformatMessage()emitting a spurious leading newline (or double newline) when an oversize term that exceedsmaxWidthis already at the start of a new line. [#497, #730]Fixed
formatMessage(),formatUsage(), andformatDocPage()previously measuring string width using JavaScript.length(UTF-16 code units). They now use terminal display width, so East Asian wide characters, combining marks, and emoji wrap and align correctly. [#509, #757]Fixed
getDocPage()preserving hidden terms from customDocFragmentsinstead of filtering them.buildDocPage()now filters out entries whose terms are doc-hidden before assembling the finalDocPage. Additionally,deduplicateDocEntries()anddeduplicateDocFragments()now skip hidden entries before deduplicating, so hidden terms cannot influence the ordering of visible entries. Titled sections are now positioned at the first fragment containing visible entries, and titled sections with only hidden entries are omitted entirely. [#494, #720]Fixed
getDocPage(),getDocPageSync(), andgetDocPageAsync()silently misinterpreting aParseOptionsobject as theargsarray when passed as the second argument. All three functions now acceptParseOptionsas the second argument directly, makinggetDocPage(parser, { annotations })work without requiring an explicit empty args array. [#480, #739]Fixed
getDocPage(),getDocPageSync(), andgetDocPageAsync()hanging when a parser returns success without consuming input. The functions now detect no-progress iterations and break out of the loop, matching the existing guards inparse()andsuggest(). [#493, #740]Fixed
normalizeUsage()reusing leafUsageTermobjects by reference, which caused mutations of the normalized result to propagate back to the original usage tree. [#504, #722]Fixed
or()andlongestMatch()duplicating visible terms in documentation when branches share the same surface syntax. [#432, #698]Fixed sync completion paths in
runParser(),runParserSync(), andrunWithSync()silently returningPromiseobjects when a completion callback (onShoworonError) returns aPromise. These paths now throwRunParserErrorconsistently with the existing sync/async mismatch guard on help and parse-error paths. [#264, #673]Fixed
runParserSync()andrunWithSync()accepting async parser objects at runtime and returningPromises instead of throwing. These sync-only APIs now validateparser.$modeat runtime and throwTypeErrorif the parser is not synchronous. [#279, #676]Fixed
runWith(),runWithSync(), andrunWithAsync()silently accepting multiple source contexts with the sameid. Duplicate ids now throw aTypeErrorinstead of producing order-dependent results. [#495, #746]Fixed
runWith()andrunWithSync()discarding the original parse error when a source context's disposal also throws. The disposal error now wraps both failures in aSuppressedError(following TC39 conventions) instead of silently replacing the parse error. [#246, #771]Fixed constructs dropping values from parsers that only produce results in
complete().object()now runs a zero-consumption pass after its greedy loop so that purely non-interactive child parsers (e.g.,multiple(constant(...))) can update their state even when they returnconsumed: [].or()now accepts a unique non-consuming, non-interactive branch as a fallback when no branch consumed input and the buffer is empty.conditional()defers zero-consuming discriminators tocomplete()in async mode, and resolves them duringparse()in sync mode. [#232, #773]conditional()now speculatively parses named branches when the discriminator is an async parser that succeeds without consuming input (e.g.,prompt(option(...))). If exactly one branch can consume tokens, it is tentatively selected during parse and verified against the resolved discriminator during the complete phase. This fixes the “Unexpected option or argument” error that occurred when branch-specific tokens were present but the discriminator was deferred to the complete phase. [#772, #774]Fixed
optional()andwithDefault()crashing when the parser's state is an annotation-injected object instead ofundefined. The state discrimination inmodifiers.tsnow usesArray.isArray(state)to distinguish the wrapped inner state[TState]from the initial state, instead oftypeof state === "undefined". This allows annotation injection to work correctly for parsers withundefinedinitial states (e.g.,fail()used withbindConfig()), which was broken by the earlier 0.10.6 fix that skipped injection entirely. [#131]Fixed annotation injection for parsers with primitive initial states. Annotations are now preserved without corrupting the parser result value (e.g.,
constant("ok")remains"ok"instead of becoming an annotation object). This affects annotation-enabled parse/run paths, includingrunWith()andrunWithSync(). [#146]Fixed
ip()andcidr()withversion: "both"allowing IPv4-mapped IPv6 addresses (e.g.,::ffff:192.168.0.1) to bypass IPv4 restrictions such asallowPrivate: false. The embedded IPv4 address is now checked against the configured IPv4 restrictions. [#339, #721]Fixed
ipv6(),ip(), andcidr()accepting IPv4-mapped IPv6 addresses with leading-zero octets (e.g.,::ffff:01.02.03.04). The embedded IPv4 portion now uses the same strict octet validation asipv4(). [#393, #744]Fixed
hostname()accepting dotted all-numeric strings (e.g.,192.168.0.1,999.999.999.999) that resemble IPv4 addresses rather than DNS hostnames. This also affectssocketAddress()withhost: { type: "hostname" }. [#376, #657]Fixed
socketAddress()withhost: { type: "both" }allowing IP-shaped input to bypass IP restrictions (e.g.,allowPrivate: false) by falling through to the hostname parser. IP-shaped input is now routed exclusively to the IP parser based on lexical form, and specific restriction error messages (e.g., “192.168.1.1 is a private IP address.”) are propagated instead of the generic “invalid format” error. [#335, #714]Fixed
socketAddress()withhost: { type: "both" }accepting alternate IPv4 literal forms (hex octets like0x7f.0x0.0x0.0x1, single hex integers like0x7f000001, and octal integers like017700000001) as hostnames, bypassing IP restrictions. These non-standard forms are now detected and rejected with a specific error message. Detection is bounded to values that fit within the 32-bit IPv4 range, so hex-prefixed hostnames outside that range (e.g.,0x100000000) remain valid. Plain decimal host labels (e.g.,1234,2130706433) also remain valid hostnames. This also affectssocketAddress()withhost: { type: "hostname" }. [#715, #717]Fixed
socketAddress()incorrectly splitting host-only inputs when a custom separator appears inside the hostname (e.g.,"toronto"withseparator: "to"was parsed as{ host: "toron", port: 80 }instead of{ host: "toronto", port: 80 }). When port is optional (defaultPortis set andrequirePortis false), the parser now disambiguates separator positions by validating host/port split candidates first, and falls back to treating the full input as a hostname only when no valid split exists. [#360, #726]Fixed
socketAddress()hiding specific host and port validation errors behind a generic format error. When sub-parsers likehostname(),ipv4(), orport()reject a value for a specific reason (e.g., “Hostname ‘localhost’ is not allowed.”, “Expected a port number greater than or equal to 1,024”), the specific error is now propagated instead of being replaced with “Expected a socket address in format host:port”. Customerrors.invalidFormatstill takes precedence when configured. [#322, #749]Fixed
socketAddress()rendering the separator as a quoted value in the default format error message (e.g.,host":"portinstead ofhost:port). [#324, #758]Fixed
socketAddress()treating a trailing separator with no port (e.g.,"localhost:") as valid host-only input whendefaultPortis set. The trailing separator now correctly fails with a missing-port error, since the explicit separator signals intent to specify a port. Host-only input without a separator (e.g.,"localhost") continues to use the default port. [#325, #759]Fixed
hostname()accepting case variants oflocalhost(e.g.,LOCALHOST,LocalHost) and wildcard-localhost forms (e.g.,*.localhost) whenallowLocalhostis set tofalse. DNS hostnames are case-insensitive, so all variants are now correctly rejected. This also affectssocketAddress()withhost: { type: "hostname", hostname: { allowLocalhost: false } }. [#321, #659]Fixed
hostname()accepting wildcard labels (*) outside the leftmost position (e.g.,foo.*.com,example.*) and even whenallowWildcardisfalse(e.g., bare*). [#355, #661]Fixed
hostname()anddomain()silently coercing invalid runtime option types. Non-boolean values forallowWildcard,allowUnderscore,allowLocalhost,allowSubdomains, andlowercaseare now rejected with aTypeErrorinstead of being silently coerced by JavaScript truthiness. [#366, #664]Fixed
locale()value parser'sformat()method dropping Unicode extension subtags (e.g.,en-US-u-ca-buddhistwas formatted asen-US). The method now usesIntl.Locale.toString()instead ofbaseNameto preserve the full locale identifier. [#317, #565]Fixed
format()inmacAddress(),domain(),ipv6(),ip(), andcidr()value parsers to return the serialized value instead of the metavar placeholder (e.g.,"MAC","DOMAIN"). [#318, #742]Added
ValueParser.normalize()optional method for canonicalizing values of typeT(e.g., MAC address case/separator normalization, domain lowercasing). Built-in implementations delegate toparse()internally, returning invalid values unchanged. [#318, #742]Added
Parser.normalizeValue()optional method. Primitive parsers (option(),argument()) implement this by delegating toValueParser.normalize(). Value-preserving combinator wrappers (includingoptional(),withDefault(),nonEmpty(),group(),command(),multiple(),object(),tuple()) forward it from inner parsers. [#318, #742]withDefault()now normalizes default values through the inner parser'snormalizeValue()when available, so runtime defaults match the representation thatparse()would produce. Exclusive combinators (or(),longestMatch()) and multi-source combinators (merge()) do not forward normalization because the active branch or key ownership is unknown at default time. [#318, #742]Fixed
url()parser'ssuggest()emitting://for non-hierarchical URL schemes likemailto:andurn:. Suggestions now use:for non-hierarchical schemes and://only for special schemes (http,https,ftp,ws,wss,file) as defined by the WHATWG URL Standard. [#342, #678]Fixed
hidden: truecommands and options leaking through “Did you mean?” typo suggestions. Fully hidden terms are now excluded from typo-suggestion candidates. [#516, #690]Fixed
findSimilar()preserving duplicate candidates, which could generate duplicated “Did you mean?” suggestion entries. [#517, #729]Extended
hiddenvisibility controls frombooleantoboolean | "usage" | "doc" | "help"across primitive parsers:option(),flag(),argument(),command(), andpassThrough().group(),object(), andmerge()now also supporthiddenwith the same values, and wrapper/parserhiddenvalues are combined as a union.hidden: truekeeps 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. [#113, #141]Redesigned meta command configuration (help, version, completion) in
RunOptionsto use independentcommand/optionsub-configs instead ofmode: "command" | "option" | "both". Each meta command now accepts{ command?: true | CommandSubConfig; option?: true | OptionSubConfig }where at least one must be specified (enforced at the type level).CommandSubConfigsupportsnames(first = display name, rest = hidden aliases vialongestMatch()),group, andhidden.OptionSubConfigsupportsnames(all shown in help),group, andhidden. This is a breaking change. [#130]Removed
CompletionName,CompletionHelpVisibility,CompletionConfig,CompletionConfigBoth,CompletionConfigSingular, andCompletionConfigPluraltypes. Completion naming is now controlled viaCommandSubConfig.namesandOptionSubConfig.names. [#130]Added support for equals-joined values on single-dash multi-character options in
option()(e.g.,-seed=42,-max_len=1000), in addition to existing--option=valueand/option:valueformats. Single-character short options (e.g.,-v) remain excluded from this joined form to avoid conflicts with short-option clustering.flag()now also rejects this joined form consistently for Boolean flags. [#134 by Maxwell Koo]Added
SourceContextModetype ("static" | "dynamic") and optionalmodefield toSourceContext. When set,isStaticContext()reads this field directly instead of callinggetAnnotations(), preventing any side effects thatgetAnnotations()might have (such as mutating a global registry).createEnvContext()setsmode: "static";createConfigContext()setsmode: "dynamic". Existing custom contexts that omit the field are unaffected.Fixed
string({ pattern })to avoid statefulRegExpbehavior leaking across parse calls when the pattern usesgoryflags. The parser now evaluates a fresh regular expression per parse, so repeated calls are deterministic and no longer mutate the caller'spattern.lastIndex.Fixed
string({ pattern })to validate thatpatternis a realRegExpat construction time. Previously, a non-RegExpvalue reaching the parser through an untyped path would silently produce an always-matching regular expression; it now throws aTypeError. [#388, #512]Fixed option-value completion in
option()so value-position suggestions no longer leak option-name candidates when the value prefix starts with-(for example, after--mode --). This restores monotonic prefix-filtering behavior for value suggestions and aligns with parse context (option name consumed ⇒ value expected).Tightened program name validation for shell completion to require names to start with an alphanumeric character or underscore, rejecting names like
-or.. Previously, running from stdin (node -,deno eval) could infer-as the program name, producing broken completion scripts. [#235, #568]Fixed PowerShell file completion stripping directory prefixes from nested path suggestions. Completing
src/now returnssrc/alpha.txtinstead of barealpha.txt. [#253, #632]Fixed Nushell file completion returning an empty list for non-empty path prefixes. Completing
src/now correctly returns files under that directory. [#254, #636]Fixed zsh file completion passing literal
$ext_patternto_filesinstead of expanding the variable, so extension filtering (e.g.,*.json,*.yaml) now works correctly. [#256, #639]Fixed
encodeSuggestions()for zsh, fish, Nushell, and PowerShell not escaping tabs and newlines in completion descriptions. Descriptions containinglineBreak()terms or tab characters now have those characters replaced with spaces so the shell completion protocol is not corrupted. [#247, #642]Fixed shell completion for file-only
Suggestion.fileentries (type: "file") excluding directories entirely, which prevented users from descending into subdirectories during path completion. All five shell backends (Bash, zsh, fish, Nushell, PowerShell) now include directories as navigation targets alongside files. [#294, #646]Fixed
encodeSuggestions()not stripping leading dots fromSuggestion.extensions, which caused extension filtering to silently fail in all five shell backends. Extensions like[".ts", ".json"](as produced bypath()) are now normalized to["ts", "json"]before encoding. [#647, #650]Fixed generated shell completion scripts not stripping leading dots from extension filters, so that dot-prefixed extensions (e.g.,
.json) in the transport protocol are handled correctly in Bash, fish, Nushell, and PowerShell. Also fixed aSplit-Patherror in PowerShell that caused extension filtering to silently fail when the completion prefix was empty, and rewrote the PowerShell extension matching to use the-inoperator instead of nestedForEach-Objectpipelines. [#255, #660]Fixed shell completion scripts ignoring
Suggestion.file.pattern, which caused file completions to enumerate the current directory instead of the pattern-specified path. All five shell backends (Bash, zsh, fish, Nushell, PowerShell) now use the transported pattern as the glob base when it is non-empty. [#251, #656]Fixed
runParser()missing-shell help for--completionrendering in command form (myapp completion [SHELL] [ARG...]) instead of option form. When the user invokes--completionwithout a shell name, the error message now shows option-form usage and respects custom option names (e.g.,--completions). Previously, option-only mode showed no follow-up help at all, and both-mode showed only the command form. [#275, #668]Clarified that the
--completionearly-return scanner intentionally uses first-match semantics: the first--completionis the meta option and all subsequent arguments (including tokens that look like--completion) are treated as opaque completion payload. Added tests to document this behavior. [#364, #670]Fixed duplicate detection for equals-joined option syntax in
option()when the value parser produces deferred or dependency-source state (for example,DerivedValueParserorDependencySource). Repeated--option=valueinputs now report a duplicate-option error instead of silently overwriting the earlier value. [#149]Fixed
withDefault().getDocFragments()crashing when a function-based default throws during help generation. Help output now skips evaluating doc-only defaults when a custommessageis provided, and otherwise omits the default display instead of throwing. [#150]Fixed async
runParser()help handling forcommand --helpvalidation. In the async validation path,displayHelpwas referenced before initialization, which could throwReferenceErrorinstead of showing help.displayHelpis now defined before async validation is invoked.Fixed
runParser()duck-typing meta-command results based on field names likehelpandversion. Internal meta results are now branded with a private symbol, so custom parsers can safely return user data objects with overlapping field names without being misclassified. [#152]Fixed
runParser()swallowing legitimate callback exceptions fromonError,help.onShow,version.onShow, andcompletion.onShowwhile trying to detect zero-argument handlers. These callbacks now always receive the numeric exit code, and any real exception they throw is propagated unchanged. [#153]Fixed source-context cleanup in
runWith()andrunWithSync(). Disposal now continues across all contexts even if an earlier dispose call throws, andrunWithSync()no longer skips contexts that only implementSymbol.asyncDisposewhen that cleanup completes synchronously. [#154]Fixed phase-two source-context inputs when later integrations need to inspect parsed values before the final parse completes. During the second pass of
runWith()/runWithSync(), deferred prompt placeholders are now scrubbed from parsed values before they reach later dynamic contexts, while preserving earlier-context precedence and stable parsed-value identity within the phase-two pass. [#177, #490]Fixed help output incorrectly interleaving meta items (
help,--help,--version) with user-defined commands when usinggroup: "…"to assign meta items to a named section that already exists in user-defined parsers. Same-named sections from different parsers are now merged into a single section. Additionally, when meta items are ungrouped and the user's commands are in a titled section, the meta items now appear after the user's section rather than before it. [#138]Fixed
merge()andconcat()dropping dependency-aware suggestions when the dependency source and derived parser live in different sub-parsers. Thesuggest()methods now build aDependencyRegistryfrom the full composed state before delegating to child parsers, matching the behavior thatobject()already had. [#178, #520]Fixed
merge()not pre-completingbindEnv()/bindConfig()-backed dependency sources for cross-parser resolution. When a dependency source wrapped withbindEnv()orbindConfig()lived in one child parser ofmerge(), derived parsers in a different child parser could not see the resolved value. The fix pre-completes dependency source fields from child parsers before resolving deferred states, and preserves annotations when extracting child parser states during completion. [#681, #684]Fixed dependency-aware completion ignoring
withDefault()source values. When a dependency source was wrapped withwithDefault()and no explicit CLI value was provided,suggest()returned an empty array instead of suggesting values based on the default. [#186, #522]Fixed
deriveSync(),deriveAsync(),deriveFromSync(), andderiveFromAsync()so that errors thrown by the factory with default dependency values during the initial parse no longer prevent deferred resolution from succeeding with the actual dependency values. Errors from the default branch are now caught and produce a preliminary failure result, which gets overridden whenparseWithDependency()runs with the actual values.deriveAsync()also no longer calls the factory during parser construction. [#225, #524]Fixed
derive()andderiveFrom()eagerly executing default factories during parser construction to detect sync/async mode. The factory is no longer called at construction time; instead, callers must provide a requiredmodefield ("sync"or"async") that declares the factory's return mode. Callers with async factories should passmode: "async"or usederiveAsync()/deriveFromAsync(). [#223, #527]Fixed
derive(),deriveSync(),deriveAsync(),deriveFrom(),deriveFromSync(), andderiveFromAsync()so thatformat()andsuggest()no longer throw when the factory throws on the default dependency value.format()now falls back toString(value)andsuggest()yields empty suggestions instead of propagating the exception. [#224, #531]Fixed dependency-value collection for single-dependency derived parsers (from
.derive()) inside constructs likeobject(),tuple(), etc. When the dependency source was absent from the registry,collectDependencyValues()now falls back to stored default values, matching the behavior that the multi-dependency path (fromderiveFrom()) already had. [#238, #748]The
integer()parser in number mode now rejects values outside the safe integer range (Number.MIN_SAFE_INTEGERtoNumber.MAX_SAFE_INTEGER). Previously, such values were silently rounded, losing precision. A newunsafeIntegererror callback inIntegerOptionsNumber.errorsallows customizing the error message. Usetype: "bigint"for values beyond this range. [#248, #525]Fixed
float()to reject numeric strings that overflow toInfinity(e.g.,1e309) whenallowInfinityisfalse(the default). Previously, only literal"Infinity"strings were rejected. [#242, #528]Changed
choice()to throwTypeErrorwhencaseInsensitiveistrueand multiple choices normalize to the same lowercase value (e.g.,"JSON"and"json"). Previously the first match won silently. [#310, #533]Changed
choice()to throwTypeErrorwhen the choices array is empty. Previouslychoice([])created an unsatisfiable parser with a malformed error message. [#332, #536]Fixed
choice()to deduplicate identical values in the choices list. Previously, duplicate entries appeared in suggestions, error messages, and help text. [#353, #537]Changed
choice()to reject empty strings in the choices array. Previously, empty-string choices were silently accepted but produced invisible items in help text, error messages, and shell completions. [#371, #546]Changed
choice()to reject non-BooleancaseInsensitiveoption values at runtime. Previously, truthy non-Boolean values like"no"silently enabled case-insensitive matching due to JavaScript truthiness coercion. [#389, #548]Changed
port()to reject non-BooleandisallowWellKnownoption values at runtime. Previously, truthy non-Boolean values like"no"silently enabled well-known port restrictions due to JavaScript truthiness coercion. [#370, #573]Changed
portRange()to reject non-BooleandisallowWellKnownandallowSingleoption values at runtime. Previously, truthy non-Boolean values like"no"silently enabled those behaviors due to JavaScript truthiness coercion. [#370, #573]Changed
portRange()andsocketAddress()to rejectseparatorvalues that contain digit characters (including Unicode decimal digits such as Arabic-Indic١). Digit-containing separators caused ambiguous splitting of numeric port input, silently reinterpreting single ports as ranges. [#378, #576]Changed
portRange()andsocketAddress()to reject emptyseparatoroption values at construction time. An empty separator makes the grammar ill-defined because the parser cannot determine token boundaries. [#327, #713]Changed
choice()to throwTypeErrorwhenNaNis included in the number choices array. Previously,NaNwas silently filtered out but still advertised in suggestions and help output, creating an unsatisfiable parser with contradictory diagnostics. [#363, #553]Fixed
choice()to snapshotcaseInsensitiveat construction time, preventing post-construction option mutation from causingparse()andsuggest()to diverge. [#508, #554]Fixed number
choice()accepting hex (0x10), binary (0b10), octal (0o10), scientific notation (2e0), empty strings, and whitespace-only strings via JavaScript'sNumber()coercion. Number choices now accept the canonical string representation and equivalent decimal spellings (e.g.,"8.0"for8). Alternate scientific-notation spellings are only accepted for values whose canonical form uses scientific notation (e.g.,"1e21"for1e+21). [#315, #523]Fixed several value parsers (
choice(),string(),uuid(),email(),domain(),url()) to snapshot caller-owned mutable configuration (arrays, error callbacks, and options) at construction time. Previously, mutating the original config object or array after parser construction could silently change parse semantics, suggestions, and error messages.choice()now also freezes its publicchoicesproperty, and all snapshotted arrays (allowedVersions,allowedDomains,allowedTLDs,allowedProtocols) are frozen to prevent mutation through error callbacks. [#507, #555]Fixed
optional(),withDefault(), andgroup()dropping the config-prompt deferral hook (@optique/config/deferPromptUntilResolved) from inner parsers. These combinators now forward the hook so thatprompt(optional(bindConfig(...)))and similar compositions correctly defer interactive prompts until phase-two config resolution. [#385, #535]Fixed
shouldDeferCompletionforwarding inoptional()andwithDefault(): the forwarded hook now unwraps the outer state ([TState] | undefined) to the innerTStatebefore delegating to the inner parser's hook, and propagates annotations from the outer array to the inner element so annotation-based checks work correctly. [#590, #592]Fixed
optional()andwithDefault()not propagating annotations from outer state into inner parser elements duringcomplete(), which preventedbindConfig()from resolving config values through wrapper combinators. [#385, #535]Fixed proxy-based sanitization of deferred prompt values breaking class methods that access private fields. Methods on non-plain objects are now invoked with temporarily sanitized own properties on the original instance, allowing private field access to work correctly through the sanitized view. [#307, #558]
Fixed
integer({ type: "bigint" }),port({ type: "bigint" }), andportRange({ type: "bigint" })to reject empty strings, whitespace, signed-plus strings, and non-decimal literals (0x,0b,0o) that theBigInt()constructor would otherwise accept. [#245, #249, #566, #572]Fixed
portRange()andsocketAddress()to derive the defaultmetavarfrom the customseparatoroption (e.g.,portRange({ separator: ":" })now defaults toPORT:PORTinstead of always usingPORT-PORT). [#323, #579]formatDocPage()now throwsTypeErrorwhenprogramNamecontains a CR or LF character, or when a section title is empty, whitespace-only, or contains a CR or LF character. [#429, #479, #580]Fixed
formatDocPage()ignoring smallmaxWidthvalues. WhenmaxWidthwas smaller than the layout budget (termIndent + termWidth + 2), the formatter produced lines far wider than requested. The term column now dynamically shrinks to share the available width with the description column. [#513, #669]Numeric parsers (
integer(),float(),port(),portRange(),cidr()) now throwRangeErrorat construction time when given contradictory range configurations (e.g.,min > max). Previously, these parsers silently created unsatisfiable parsers that rejected every input. [#349, #583]Fixed
map()applying transforms to deferred prompt placeholders during phase-one parsing.map(prompt(bindConfig(…)), fn)no longer throws before config resolution runs. [#296, #585]Numeric parsers (
integer(),float(),port(),cidr()) now throwRangeErrorat construction time when given non-finite bound values (e.g.,NaN,Infinity,-Infinity). Previously, these values silently disabled or distorted range validation. [#362, #587]Numeric parsers
integer(),port(), andportRange()now reject invalid runtimetypediscriminant values at construction time. Previously, unsupported values silently fell back to number mode. [#368, #589]Fixed
getDocPage()/getDocPageSync()/getDocPageAsync()losing inner option/argument documentation when called on a top-levelcommand()parser with no arguments. [#200, #595]Fixed
runParser()crashing or showing help instead of handling completion when help-option names (e.g.,--help) appear inside completion payloads. Help-option scanning now only checks the argument immediately after the completion command name, not the entireargsarray. [#300, #599]Fixed
email()withallowMultiplesplitting on commas inside quoted local parts and quoted display names. [#320, #606]Fixed
email({ allowedDomains: [] })silently disabling domain filtering instead of rejecting all domains, making it consistent withurl({ allowedProtocols: [] })anddomain({ allowedTLDs: [] }). [#341, #610]Fixed
email({ allowDisplayName: true })accepting malformed inputs containing multiple angle-bracket groups or bare<email>wrappers without a display name. [#338, #611]Fixed
email({ lowercase: true })lowercasing the entire address instead of only the domain part; the local part (including quoted local parts) is now preserved. [#352, #614]Fixed
email()accepting IPv4-like dotted-quad domains (e.g.,user@192.168.0.1,user@999.999.999.999) as valid email addresses. Unbracketed IPv4-like dotted quads are now rejected. [#387, #617]Fixed
email()accepting addresses that exceed RFC 5321 length limits (64-octet local-part maximum and 254-octet overall address maximum, measured in UTF-8). [#396, #622]Fixed
email({ allowMultiple: true })round-trip:format()now joins addresses with,(comma-space) instead of bare,, so quoted local parts containing commas (e.g.,"Doe, John"@example.com) survive aformat()→parse()cycle. [#354, #626]Fixed
email()not validating malformedallowedDomainsentries at construction time; entries like"@example.com","example.com."," example.com ", or non-string values now throw aTypeError. [#348, #629]Renamed
DomainOptions.allowedTLDstoallowedTldsfor naming consistency. [#345, #638]Fixed
domain()not validating malformedallowedTldsentries at construction time; entries like".com"," com ", or non-string values now throw aTypeError. [#345, #638]Fixed
domain()accepting contradictory configuration such asallowSubdomains: falsewithminLabels: 3. This combination creates an unsatisfiable parser, so it is now rejected at construction time with aTypeError. [#350, #630]Fixed
domain()andhostname()accepting invalid structural constraints such asminLabels: 0,maxLength: -1,minLabels: NaN, ormaxLength: 1.5. These values are now rejected at construction time with aRangeError. [#351, #631]Fixed
domain()accepting dotted numeric strings such as192.168.0.1,999.999.999.999, or1.2as valid domains. Inputs where every label is purely numeric are now rejected when two or more labels are present. Single-label numeric domains (e.g.,"123") remain valid. [#375, #634]Fixed
domain()not enforcing the 253-octet total domain length limit.domain()now rejects domains exceeding 253 octets by default, matchinghostname()'s existing behavior. AmaxLengthoption is available for custom limits, and atooLongentry inerrorsallows customizing the error message. [#395, #635]Fixed
__FILE__completion transport unable to representpatternvalues containing:(e.g., Windows drive-letter prefixes likeC:/...). Colons in the pattern field are now percent-encoded (%3A) so that the colon-delimited field boundaries stay intact. Users must regenerate or re-source their shell completion script after upgrading for this fix to take effect. [#252, #616]Fixed Bash completion scripts using
compgen -zwhich is unsupported on macOS's default GNU Bash 3.2. File and directory completions now use glob-based iteration instead, matching the pattern already used for extension-filtered completions. [#250, #608]Fixed fish, Nushell, and PowerShell completion scripts ignoring
includeHidden: truefor file suggestions. The shell-side__FILE__parsers now strip tab-delimited metadata before splitting by colon, so thehiddenfield is compared correctly. Users must regenerate or re-source their shell completion script after upgrading for this fix to take effect. [#618, #619]Fixed fish, Nushell, and PowerShell completions not enumerating hidden (dot-prefixed) files even when
includeHiddenistrue. The parsedhiddenflag only controlled the post-enumeration filter, but each shell's native file listing command excluded hidden files by default. Fish now additionally globs$current.*, Nushell usesls -a, and PowerShell passes-ForcetoGet-ChildItemwhen hidden files are requested. [#623, #624]Fixed zsh completion not including hidden (dot-prefixed) files when
includeHiddenistrue. The zsh backend now temporarily enablesglob_dotsbefore calling_filesor_directorieswhen hidden files are requested. [#262, #641]Fixed shell completion transports emitting raw tabs and newlines in literal suggestion text, which corrupted shell-specific transport framing. Control characters in
Suggestion.textare now replaced with spaces, matching the existing description sanitization. [#337, #663]runParser()now validates version values at runtime — empty strings, strings containing control characters, and non-string values are rejected withTypeError. [#439, #645]runParser()now validates meta command and option names (for help, version, and completion) eagerly at startup. Empty arrays, empty strings, whitespace-only names, and names containing whitespace or control characters are rejected withTypeError. Option names without a valid prefix (--,-,/, or+) are also rejected. [#425, #648]Fixed
url()not validating malformedallowedProtocolsentries at construction time; entries like"https"(missing trailing colon),"https://", or non-string values now throw aTypeError. [#344, #653]uuid()now enforces strict RFC 9562 validation by default: the version digit must be 1 through 8, and the variant nibble must follow the RFC 9562 layout (10xx). The nil and max UUIDs are accepted as special standard values. Useuuid({ strict: false })for the previous lenient behavior that accepts any hex digit in the version and variant positions. [#334, #336, #670, #674]uuid()now validatesallowedVersionsat construction time: each version must be an integer between 1 and 8, and duplicates are automatically removed. [#357, #675]Fixed
formatDocPage()fixed-prefix sections exceedingmaxWidth. TheUsage:label,Examples:/Author:/Bugs:section labels, andshowDefault/showChoicesdescription prefixes were not covered by the minimummaxWidthvalidation, allowing the formatter to silently emit lines wider than requested.formatDocPage()now raises the minimummaxWidthto account for all fixed-width labels actually in use. [#672, #677]Fixed
cidr()discarding specific nested IPv4/IPv6 validation errors (e.g., private network, loopback, multicast restrictions) behind a generic “Expected a valid CIDR notation” error message. AddedprivateNotAllowed,loopbackNotAllowed,linkLocalNotAllowed,multicastNotAllowed,broadcastNotAllowed,zeroNotAllowed, anduniqueLocalNotAllowederror hooks toCidrOptions.errorsso callers can customize these diagnostics. [#333, #679]url(),domain(), andemail()now reject empty allow-lists (allowedProtocols: [],allowedTlds: [],allowedDomains: []) at construction time with aTypeError, instead of silently creating unsatisfiable parsers with malformed error messages. Their default error messages for disallowed values now usevalueSet()withlocale: "en-US"for consistent list formatting (each item styled individually), replacing the previous.join(", ")approach. [#340, #682]Changed
macAddress()to accept and normalize single-digit octets (e.g.,0:1:2:3:4:5becomes00:01:02:03:04:05) in colon-separated and hyphen-separated formats. All octets are now zero-padded to two hexadecimal digits, ensuring canonical MAC-48 output and correct round-tripping withoutputSeparator. [#319, #330, #683, #723]Fixed
macAddress()to validateseparator,outputSeparator, andcaseoptions at construction time. Unsupported runtime values now throwTypeErrorinstead of silently falling through to arbitrary behavior. [#347, #685]Fixed
--completionoption (and--help/--version) being recognized after the--options terminator inrunParser(),runWith(), andrunWithSync(). Tokens after--are now correctly treated as positional data and no longer trigger meta-option early-exit paths. [#228, #686]Fixed
--completionoption bypassing--help/--versionprecedence inrunParser(). When--helpor--versionappears before the--completionoption (e.g.,--help --completion bash), the help or version path now correctly takes precedence. Tokens after--completionremain opaque completion payload and are not re-interpreted as meta flags. [#229, #689]Fixed
formatDocPage()rendering blank or malformed rows for degenerate entry terms (e.g., option with empty names, empty command/argument/literal, empty exclusive branches) and hidden entries in customDocPageinput. Such entries are now silently skipped. [#488, #687]Fixed
formatDocPage()using the wrong hidden visibility check: it applied usage-level filtering (isUsageHidden()) instead of doc-level filtering (isDocHidden()). As a result,hidden: "doc"terms leaked into doc output, whilehidden: "usage"terms were incorrectly omitted. [#488, #687]Added
contextoption toUsageTermFormatOptionsforformatUsageTerm(). Setcontext: "doc"to apply doc-level hidden filtering instead of the default usage-level filtering. [#488, #687]Fixed
formatDocPage()rendering emptychoicesanddefaultmessages as malformed suffixes (e.g.,(choices: ),[]) when theMessagearray was empty. Empty arrays are now treated as absent. [#469, #692]Fixed
formatDocPage()producing malformed output(choices: , ...)whenshowChoices.maxItemsis0.maxItemsmust now be at least1; values below1throw aRangeError. [#471, #694]Fixed
deduplicateSuggestions()silently droppingincludeHidden: truewhen merging file suggestions that differ only inincludeHidden. Duplicates are now merged withincludeHidden: truepreferred, since it is a superset of the non-hidden variant. [#518, #693]Fixed
deduplicateSuggestions()treating file suggestions with the sameextensionsin different order as distinct. Extensions are now compared as a set, so[".json", ".yaml"]and[".yaml", ".json"]correctly deduplicate to one suggestion. [#519, #695]Fixed
getDocPage()exposing parser-owned usage terms and doc fragments by reference. Mutating the returnedDocPageor thedefaultUsageLineargument in a command'susageLinecallback no longer corrupts the parser definition. [#500, #697]Added deep-clone utilities for parser structures:
cloneUsageTerm()andcloneUsage()in@optique/core/usagecloneDocEntry()in@optique/core/doccloneMessageTerm()andcloneMessage()in@optique/core/message
These are used internally by
getDocPage()to isolate returned documentation pages from parser-owned state, and are also available as public APIs for consumers who need to deep-copy these structures. [#500, #697]Fixed
normalizeUsage()to strip degenerate usage terms instead of preserving them unchanged. Empty-named options, empty-named commands, empty-metavar arguments, and container terms (optional,multiple,exclusive) whose terms array is empty after recursive normalization are now removed. Exclusive branches representing valid zero-token alternatives and empty-value literals are preserved; branches that become empty because all their content was malformed are removed. [#485, #716]Fixed
message()tagged template reusing interpolatedMessageTermobjects andMessagearrays by reference, allowing mutation of the returned message to corrupt the original interpolated values. Interpolated terms are now deep-cloned viacloneMessageTerm(). [#505, #718]Fixed
optionNames(),values(), andurl()message term constructors storing caller-owned arrays andURLobjects by reference, allowing later caller-side mutations to corrupt already-created terms. These constructors now defensively copy their inputs. [#506, #719]Fixed
runWith()andrunWithSync()skipping context disposal (Symbol.dispose/Symbol.asyncDispose) on early help, version, and completion exits. Contexts are now disposed on every exit path, including early meta-command exits. [#226, #733]Added construction-time validation for labels in
object(),tuple(),merge(), andgroup(). Labels that are empty, whitespace-only, or contain control characters now throwTypeErrorimmediately instead of producing broken help output. [#404, #737]runParser()anddefineProgram()now validate program names up front, rejecting empty strings, strings with control characters, and non-string values with aTypeError. Previously, invalid names were only caught during help/error output formatting. [#428, #743]Fixed nested
optional()wrappers rendering double brackets ([[...]]) in help usage output. Parsers likeoptional(optional(option(...)))andwithDefault(optional(option(...)), ...)now correctly display single brackets ([...]). [#290, #745]Breaking change:
valueSet()now requires a second parameter: either a fallback string or an options object with afallbackfield. When the values array is empty, the fallback text is returned as a single text term (an empty fallback string produces an emptyMessage). This prevents surrounding prose inmessagetemplates from collapsing into malformed sentences (e.g., “Expected one of .”). [#492, #747]values()now throwsTypeErrorwhen given an empty array. [#492, #747]Fixed
optional()andwithDefault()discarding values from parsers whose useful result is produced duringcomplete()rather thanparse()(e.g.,constant(),bindEnv(),bindConfig()). Wrappers placed under these modifiers inobject()constructs and at the top level now correctly preserve completed values, andwithDefault()falls back to its configured default only when the wrapped parser produces no value at all. [#233, #775]Removed the internal
optionalStyleWrapperKeysymbol.object()'s zero-consumption pass no longer needs a magic marker; interactive wrappers likeprompt()now distinguish completability probes from real completion viaExecutionContext.phase. [#233, #775]optional()andwithDefault()now propagate annotations from the outer state into the inner parser's initial state. When the outer state is an annotation wrapper (e.g., fromparse(parser, args, { annotations })), the inner parser'sparse()now receives an annotated initial state so thatbindEnv()/bindConfig()wrappers underoptional()/withDefault()can resolve their fallbacks at top level. [#233, #775]
@optique/config
Regression-tested and documented concurrent reuse of a shared
ConfigContextinstance.run(),runAsync(), andrunWith()now explicitly guarantee run-scoped annotation snapshots, so one run's phase-two config load cannot overwrite another run's loaded config even when both reuse the same context concurrently. The config integration docs also now spell out that manualgetAnnotations()calls affect low-level parsing only when their returned annotations are passed back explicitly. [#270]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. This also fixes stale config values and metadata surviving later empty, metadata-less, or failing probes. [#234, #272, #785]Fixed
bindConfig()not re-validating fallback values (values loaded from the config file and configured defaults) against the inner CLI parser's constraints. Previously, constraints likeinteger({ min: 1024 })orstring({ pattern })could be silently bypassed through config-sourced values and defaults. Both config values and defaults are now routed through the inner parser'svalidateValue()method when available, including when the bound parser serves as a dependency source for a derived parser. Values that fail the inner parser's constraints are rejected with the same error that a CLI-sourced value would produce. Fallback values that flow throughmap()are exempt because the mapped output type no longer matches the inner parser's constraints; dependency-derived value parsers (derive/deriveFrom) are also exempt because theirformat()rebuilds from default dependency values. This is a behavior change for any code that relied on constraint-violating config values or defaults being accepted. [#414, #777]Removed
configKeysymbol. EachConfigContextinstance now stores its data under its own uniqueidsymbol (i.e.,context.id) so that multiple config contexts can coexist without overwriting each other in merged annotations. This is a breaking change for any code that accessed annotations directly viaconfigKey. [#136]Removed
runWithConfig()and the@optique/config/runsubpath export. Config contexts are now used directly withrun(),runSync(), orrunAsync()from @optique/run (orrunWith()from@optique/core/facade) via thecontextsoption. Context-specific options likegetConfigPathandloadare passed alongside the standard runner options. This is a breaking change. [#110]Moved
fileParseroption fromrunWithConfig()runtime options intocreateConfigContext()options. The file parser is now stored in the context at creation time. [#110]Changed
ConfigContextRequiredOptionsto make bothgetConfigPathandloadoptional fields (with runtime validation that at least one is provided). [#110]Added
ConfigContextimplementation ofSymbol.disposefor automatic cleanup of the global config registry. [#110]Fixed
createConfigContext()breaking sync runner flows. When config loading and schema validation complete synchronously, config contexts now return annotations without a Promise so documentedrunSync()andrunWithSync()config fallbacks work again. [#159, #162]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 even when the CLI option was absent.Fixed
bindConfig()silently swallowing exceptions thrown bykeycallbacks. Errors now propagate to the caller instead of being treated as a missing config value. [#259, #549]Added config-source metadata support for
bindConfig()key accessors. Accessor callbacks now receive a secondmetaargument, and single-file mode now provides defaultConfigMetavalues (configPath,configDir) so path-like options can be resolved relative to the config file location. [#111]Changed
CustomLoadOptions.loadto return{ config, meta }(ConfigLoadResult<TConfigMeta>) instead of raw config data. This makes metadata extensible for custom multi-file loaders and allowscreateConfigContext<T, TConfigMeta>()to carry a custom metadata type through tobindConfig()key callbacks. [#111]Fixed
bindConfig()andConfigLoadResulttype signatures to reflect that config metadata can be absent. Key callbacks now receiveTConfigMeta | undefined, andConfigLoadResult.metais typed asTConfigMeta | undefinedto match runtime behavior. [#155]Fixed
createConfigContext()treating falsy first-pass parse results such as0,false, and""as the phase-one sentinel. Dynamic config loading now skips only when the parsed value is actuallyundefined, so top-level primitive parsers still reach the second config-loading phase correctly. [#161, #164]Fixed phase-two
load(parsed)/getConfigPath(parsed)inputs for unresolved prompt-backed values. Config callbacks now receive scrubbed parsed values with deferred prompt placeholders normalized toundefined, while keeping the original parsed object identity when no sanitization is needed. [#177, #490]Fixed proxy-based sanitization of deferred prompt values breaking class methods that access private fields. Methods on non-plain objects are now invoked with temporarily sanitized own properties on the original instance, allowing private field access to work correctly through the sanitized view. [#307, #558]
createConfigContext()now validatesschemaandfileParserat construction time, andgetAnnotations()validatesloadandgetConfigPathtypes. Malformed values now throw aTypeErrorimmediately instead of surfacing as late internal errors during annotation loading. [#391, #605]createConfigContext()now validates thatgetConfigPath()returns a string orundefinedat runtime. Malformed return values (objects, Promises, numbers, etc.) now throw aTypeErrorinstead of crashing with a rawpath.resolve()error. [#416, #609]bindConfig()now throws aTypeErrorwhenkeyis not a valid property key (string, number, or symbol) or function, instead of silently coercing the value. [#398, #627]bindConfig()now throws aTypeErrorat parse time when akeycallback returns aPromiseor thenable, instead of silently leaking the thenable as the parsed value. [#400, #628]createConfigContext()now validatesload()return values at runtime. Malformed results (missingconfigproperty, non-object or array returns, plain thenables returned directly fromload(), or Promise-valuedconfig/metafields) now throwTypeErrorinstead of causing silent failures. [#411, #655]Fixed custom
load()mode being unable to represent “no config found” without failing schema validation.load()can now returnundefinedornulldirectly to signal that no config data is available;bindConfig()falls back to defaults, matching the behavior ofgetConfigPathmode when the path isundefinedor the file is missing. AConfigLoadResultwithconfig: undefinedorconfig: nullis still validated against the schema as before. [#236, #770]Fixed
bindConfig()not propagating dependency source values to derived parsers. When adependency()option was wrapped withbindConfig(), the config-resolved value was invisible to derived parsers, which fell back todefaultValue()instead. [#179, #680]
@optique/env
The @optique/env package was introduced in this release, providing environment variable integration via source contexts. [#86, #135]
Removed the hidden process-global fallback from
bindEnv(). CallingenvContext.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. [#234, #785]Fixed
bindEnv()not re-validating fallback values (environment variable values parsed by a looser env-levelparserand configured defaults) against the inner CLI parser's constraints. Previously, constraints likeinteger({ min: 1024 })orstring({ pattern })could be silently bypassed through an environment variable or default. Both environment variable values and defaults are now routed through the inner parser'svalidateValue()method when available, including when the bound parser serves as a dependency source for a derived parser. Fallback values that flow throughmap()are exempt because the mapped output type no longer matches the inner parser's constraints; dependency-derived value parsers (derive/deriveFrom) are also exempt because theirformat()rebuilds from default dependency values. This is a behavior change for any code that relied on constraint-violating environment values or defaults being accepted. [#414, #777]Added
createEnvContext()for creating static environment contexts with optional key prefixes and custom source functions.Added
bindEnv()for parser fallback behavior with priority order CLI > environment > default.Added
bool()value parser for common environment Boolean literals (true,false,1,0,yes,no,on,off).bool()now provides value completion suggestions for all accepted literals, not just the canonicaltrue/false. [#268, #562]Added support for env-only values via
bindEnv(fail<T>(), ...)when a value should not be exposed as a CLI option.createEnvContext()now validatesprefixat runtime, rejecting non-string values with aTypeError. [#384, #652]createEnvContext()now throws aTypeErrorwhensourceis not a function, instead of deferring the crash to environment lookup time. [#390, #600]bindEnv()now throws aTypeErrorwhenkeyis not a string, instead of deferring the crash to environment lookup time. [#398, #627]bindEnv()now produces a clear failure whenEnvSourcereturns a non-string value, instead of leaking it through or crashing downstream value parsers. [#399, #633]bindEnv()now throws aTypeErrorwhenparseris not a validValueParser, instead of deferring the crash to environment lookup time. [#415, #637]Fixed
bindEnv()not propagating dependency source values to derived parsers. When adependency()option was wrapped withbindEnv(), the env-resolved value was invisible to derived parsers, which fell back todefaultValue()instead. [#179]
@optique/git
Fixed
gitRef()emitting duplicate completion suggestions when a branch and tag share the same name. [#284, #569]Fixed
gitCommit()andgitRef()suggesting abbreviated commit SHAs shorter than the typed prefix, which caused shell completion frontends to drop the suggestions. Suggested OIDs are now at least as long as the prefix. [#569]gitCommit(),gitRef(), and other git parser functions now throw aRangeErrorwhensuggestionDepthis not a positive integer, instead of silently accepting invalid values. [#377, #570]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. [#331, #571]Fixed
gitRemoteBranch()reporting a misleading “branch not found” error when the specified remote does not exist. The parser now correctly diagnoses the missing remote. AddedremoteNotFoundtoGitParserErrorsfor custom error messages in this case. [#308, #603]gitRemoteBranch()now validates theremoteparameter at construction time, rejecting empty, whitespace-only, control-character-containing, multiline, or non-string values with aTypeError. [#464, #654]Fixed error messages for
gitBranch(),gitRemoteBranch(),gitTag(), andgitRemote()to omit the “Available …” suffix when the list is empty, instead of producing malformed sentences. [#492, #747]
@optique/inquirer
The @optique/inquirer package was introduced in this release, providing interactive prompt fallback integration via Inquirer.js. [#87, #137]
prompt(bindEnv(...))andprompt(bindConfig(...))no longer skip prompts based on stale source state from earlier manualgetAnnotations()calls. Prompts are skipped only when the current parse carries explicit annotations or runner-provided contexts. [#234, #785]Added
prompt()for wrapping any parser with an interactive prompt that fires when no CLI value is provided. Supports ten prompt types:input,password,number,confirm,select,rawlist,expand,checkbox,editor, and a customprompterescape hatch.Added
ChoiceandExpandChoiceinterfaces for selection-type prompts (select,rawlist,expand,checkbox).Re-exports
Separatorfrom@inquirer/promptsfor use in choice lists.prompt()always returns an async parser ($mode: "async") and integrates cleanly withbindEnv()andbindConfig()— the prompt is skipped whenever the CLI, environment variable, or config file supplies a value.Fixed
prompt(bindEnv(...))andprompt(bindConfig(...))insidetuple()andconcat(). These shared-buffer compositions now skip the prompt when env/config fallback resolves the dependency source, and dependency-awaresuggest*()calls now offer the correct derived values. [#750, #768, #769]Fixed
prompt()leavingExitPromptErroruncaught when a user cancels an Inquirer prompt with ^C. Prompt cancellation is now converted into a normal parse failure (Prompt cancelled.) instead of surfacing as an unhandled promise rejection. [#151]Fixed
prompt()not attempting inner parser completion when the CLI state is wrapped byoptional()(array form). When the inner parser carries a config-prompt deferral hook,prompt()now delegates to the inner parser'scomplete()so that config values propagate through wrapper combinators likeoptional(bindConfig(...)). [#385, #535]Fixed
prompt(optional(...))andprompt(withDefault(...))insideobject()silently skipping the prompt and returning the optional/default value instead. [#288, #540]Fixed
prompt()double-wrapping already-optional inner parsers, which caused help usage to render[[--name STRING]]instead of[--name STRING]. [#289, #582]Fixed
prompt()crashing with an internalTypeErrorwhen an unsupportedtypevalue is passed at runtime through an untyped path. It now throws a clearTypeErrorwith the invalid type name up front. [#386, #612]Behavioral change:
prompt()no longer re-validates prompted values through the inner parser's constraint pipeline. The CLI path and the prompt path are now treated as independent value sources. Previously, prompted values were fed back through a synthetic parse, which caused false rejections when value-transforming combinators likemap()changed the value domain. This means prompted values are no longer checked against constraints likeinteger({ min, max }),string({ pattern }), orchoice()— those constraints only apply to CLI input. Use the prompt config'svalidate,min/max/step, orchoicesoptions to enforce equivalent constraints on prompted values. [#392, #613, #615, #621]Added
validatesupport toSelectConfig,RawlistConfig,ExpandConfig, andCheckboxConfigprompt configurations. Returntrueto accept, or a string error message to reject and re-prompt. [#620, #625]Refactored
prompt()to detect completability probes viaExecutionContext.phaseinstead of a sentinelinitialState. Prompts continue to fire only during the real completion phase, are still skipped when environment variables or config files provide a value, and cancellation still surfaces as a parse failure. This removes the dependency on @optique/core's internaloptionalStyleWrapperKeysymbol. [#233, #775]
@optique/logtape
debug(),verbosity(), andloggingOptions()now validate log level options (debugLevel,normalLevel,baseLevel,default) at runtime and throwTypeErrorfor invalid values. Previously, invalid strings passed via type assertions (e.g.,as never) would silently leak into successful parse results. [#430, #711]Fixed
createSink()misreportinggetFileSink()factory errors as missing@logtape/filepackage. Previously, any error thrown after the dynamic import (e.g., file permission errors) was caught and rewritten as an installation hint. Now only actual import failures produce the installation message; factory errors propagate as-is. [#299, #702]Fixed
logOutput()to request hidden-file completion for dot-prefixed paths. Previously, tab-completion for paths like.orsrc/.would not suggest hidden files or directories. [#292, #699]Fixed
createSink()failing on Deno when installed from JSR because@logtape/filewas not declared in the package's import map. The dynamic import could not resolve the bare specifier even if the user had installed@logtape/file. [#329, #703]Fixed
createConsoleSink()ignoring falsy timestamps like0and substituting the current time. A truthiness check onrecord.timestamptreated the valid Unix epoch timestamp0as absent. [#311, #705]Fixed
createConsoleSink()silently treating invalidstreamandstreamResolverreturn values as stdout. Invalid staticstreamvalues now throwTypeErrorwhenstreamResolveris not provided, and invalidstreamResolverreturn values throwTypeErrorat log time. [#379, #707]ConsoleSinkOptions.streamnow also acceptsnull, which is treated the same asundefined(defaults to"stderr"). [#379, #707]Fixed
logOutput()not validating empty runtimemetavarvalues. Previously, passing an empty string asmetavarwould silently produce malformed help output. Now it throwsTypeErrorlike all other value parsers. [#459, #708]Fixed
loggingOptions()not validating invalid runtimeleveldiscriminants. Previously, passing an unsupportedlevelvalue (e.g., from untyped JavaScript) would cause an internal crash during parser assembly. Now it throwsTypeErrorwith a clear message. [#373, #710]
@optique/temporal
Temporal parsers now throw a
TypeErrorwhenglobalThis.Temporalis unavailable, instead of silently returning an “invalid format” error. This makes it clear that the runtime lacks Temporal support and a polyfill is needed. [#282, #561]Fixed
TimeZonetype to include single-segment IANA timezone identifiers such as"GMT","EST", and deprecated aliases like"Japan"and"Cuba". These were already accepted at runtime by thetimeZone()parser but excluded from the static type. [#304, #596]Changed the default metavar for
plainMonthDay()from"--MONTH-DAY"to"MONTH-DAY". The previous metavar confused help text because it looked like a CLI option flag (e.g.,--birthday --MONTH-DAY), and was inconsistent with the canonical format"01-23"produced byformat(). [#306, #643]Temporal plain parsers now enforce strict input shapes matching their advertised types, rejecting wider ISO forms that were previously silently accepted. For example,
plainDate()no longer accepts datetime strings like"2020-01-23T17:04:36", andplainDateTime()no longer accepts date-only strings like"2020-01-23". [#314, #649]
@optique/run
Added
stdout,stderr, andonExitoptions torun(),runSync(), andrunAsync()for dependency injection of process-integrated behavior. This allows embedding and test environments to capture output and control exit handling without monkey-patchingprocess.stdout/process.stderrorprocess.exit. By default, behavior is unchanged (process.stdout.write,process.stderr.write, andprocess.exit). [#112]Redesigned
RunOptions.help,RunOptions.version, andRunOptions.completionto use the newcommand/optionsub-config structure from @optique/core. String shorthands ("command","option","both") and the version string shorthand are preserved for convenience. Object configurations now use{ command, option }instead of{ mode }. [#130]Removed
CompletionHelpVisibility,CompletionOptionsBase,CompletionOptionsBoth,CompletionOptionsSingular,CompletionOptionsPlural, andCompletionOptionstypes. [#130]Fixed
run(),runSync(), andrunAsync()overload resolution forProgramvalues used withcontexts. Context-awareProgramcalls now preserve the correct return type and accept context-specific runner options instead of falling back to the plainProgramoverloads. The stricter plain-Programoverloads also reject option variables whose types add keys outsideRunOptions, preserving the tighter direct-call checks introduced by this change set. [#160, #163]Fixed
runSync()accepting async parser objects (includingProgramwrapping async parsers) at runtime and returningPromises instead of throwing.runSync()now validatesparser.$modeat runtime and throwsTypeErrorif the parser is not synchronous. [#279, #676]Fixed
run(),runSync(), andrunAsync()to follow the parser-aware help, version, and completion semantics from @optique/core. Built-in meta requests now yield to ordinary parser data, while genuine meta requests still stop after phase 1 and bypass phase-two context refinement and process handling as before. [#230, #784]Fixed
path()extension validation for dotfiles (e.g.,.env,.gitignore) and multi-part extensions (e.g.,.tar.gz,.d.ts). Previously,extname()only returned the last extension segment, so these cases were incorrectly rejected. [#309, #530]Fixed
path()to reject empty and whitespace-only strings. Previously, these were silently accepted as valid paths in non-mustExistmodes. [#343, #538]Fixed
path()applyingextensionsvalidation even whentype: "directory"is set. Extensions are now skipped for directory-type paths. [#257, #539]Fixed
path()to reject configurations where bothmustExistandmustNotExistare set. Previously, the contradictory configuration was silently accepted. [#358, #541]Fixed
path().suggest()not enabling hidden-file completion for nested dotfile prefixes likesrc/.ornested/path/.. Previously, only bare dot prefixes (e.g.,.) setincludeHidden. [#258, #543]Fixed
path()to reject invalid runtimetypevalues. Previously, unsupported values like"files"were silently accepted, causing inconsistent behavior between parsing and suggestions. [#361, #545]Context-required options passed to
run(),runSync(), andrunAsync()must now be wrapped in acontextOptionsproperty instead of being passed as top-level keys. This prevents name collisions with runner options such ashelp,programName, andversion. [#240, #241, #575, #581]path()now throwsTypeErrorat construction time whenmustExist,mustNotExist, orallowCreatereceive non-Boolean values. Previously, JavaScript truthiness silently coerced invalid values like"no"intotrue. [#383, #591]Fixed
path()to reject non-string extension entries at construction time. Previously, non-string values (e.g., numbers) bypassed the leading-dot validation and leaked into error messages and completion payloads. [#346, #651]
@optique/man
Fixed
formatDateForMan()to throw aRangeErrorfor invalidDateobjects instead of formatting them as"undefined NaN". [#266, #607]Fixed
formatDocPageAsMan()to includeDocEntry.choicesin man page output. Previously, available choices were silently dropped. [#265, #604]Fixed
formatDocPageAsMan()to fall back toDocPage.examples,DocPage.author, andDocPage.bugswhen the correspondingManPageOptionsfields are absent. Previously, those page-level metadata fields were silently ignored unless duplicated in the options. [#263, #602]Fixed
formatDocPageAsMan()to respect commandusageLineoverrides in the SYNOPSIS section. Previously, the man page formatter always rendered the default command usage, ignoring customusageLinevalues. [#237, #593]Changed
ManPageOptions.seeAlso[].sectiontype fromnumbertoManPageSectionand added runtime validation that rejects invalid section numbers (must be integers 1–8). Previously, fractional or negative numbers were silently serialized into malformed.BRcross-references. [#380, #578]generateManPageSync(),generateManPageAsync(), andgenerateManPage()now validate that the input is a genuine OptiqueParserorProgramup front, instead of accepting malformed objects and crashing with an internalgetDocFragments is not a functionerror. [#305, #567]Fixed
generateManPage()leaving empty wrappers and dangling separators in SYNOPSIS when hidden terms are nested insideoptional,multiple, orexclusiveusage nodes. Empty wrapper nodes are now collapsed and exclusive separators are cleaned up after hidden terms are removed. [#222, #526]Fixed
formatUsageTermAsRoff()incorrectly hidinghidden: "doc"terms from SYNOPSIS. Since SYNOPSIS is usage output, only usage-hidden terms (hidden: true,"usage","help") are now suppressed. [#221, #222, #526, #529]The
optique-manCLI now rejects empty strings for--name,--date,--version-string, and--manualoptions instead of generating malformed.THheader lines. [#283, #532]Fixed
optique-mannot recognizing.tsxand.jsxinput files as needing thetsxloader on Node.js, causingERR_UNKNOWN_FILE_EXTENSIONerrors instead of the intended TypeScript-handling flow. [#280, #534]Fixed
optique-manCLI not defaulting--dateto the current date when omitted. The help text documented “defaults to the current date” butundefinedwas passed through, producing an empty date field in the.THheader. [#276, #547]Fixed
formatDocPageAsMan()not escaping hyphens and roff special characters (backslashes, line-start periods/quotes) in program names, command names, andSEE ALSOreference names. [#274, #542]Fixed
formatMessageAsRoff()not escaping double quotes insidevalue()andvalues()terms, which produced malformed roff output. [#273, #544]Fixed
optique-maninferring an empty program name for extensionless input files.inferNameFromPath()returned an empty string becausebase.slice(0, -0)yields""in JavaScript. [#277, #551]Fixed
optique-manaccepting malformed parser-like or program-like exports that pass shallow validation but crash later with internal generation errors. TheisParser()guard now checks for thegetDocFragmentsmethod, andisProgram()now validates thatmetadata.nameis a string and that the nestedparserpassesisParser(). Both guards also catch exceptions from throwing accessors. [#278, #552]formatDocPageAsMan()and thegenerateManPage*()functions now throw aTypeErrorwhennameis an empty string, instead of generating malformed man page output. [#286, #556]formatDocPageAsMan()and thegenerateManPage*()functions now throw aRangeErrorwhensectionis not a valid man page section number (1–8), instead of generating malformed man page output. [#287, #557]Fixed
formatDocPageAsMan()emittingliteralusage/doc terms without roff line-start escaping. Values starting with.or'(e.g.,.env) were interpreted by roff as requests instead of visible text. [#297, #559]Fixed
formatDocPageAsMan()emitting raw section titles into.SHmacros without escaping or quoting. Backslashes and double quotes in group labels are now escaped so they render literally in the man page. [#301, #560]Fixed
formatUsageTermAsRoff()andformatDocPageAsMan()dropping backslashes from metavar values in usage and doc terms. Roff consumed sequences like\Tas escapes, corrupting the rendered man page. [#298, #563]Fixed
formatDocPageAsMan()treating emptydate,version, andmanualstrings as present values, producing malformed.THheaders with empty quoted fields. Empty strings are now treated as absent. [#303, #564]Fixed
formatDocPageAsMan()emitting raw program andSEE ALSOnames into roff macros (.TH,.B,.BR) without quoting. Names containing spaces, backslashes, or quotes were corrupted by the renderer because roff parsed them as macro syntax. [#302, #574]generateManPageSync()now throwsTypeErrorat runtime when given an async parser or program. Previously it silently produced output; now callers must usegenerateManPageAsync()orgenerateManPage()for async parsers. [#291, #584]Fixed
formatUsageTermAsRoff()rendering optional and Boolean options with duplicated brackets (e.g.,[[--host STRING]]) in the SYNOPSIS section. Theoptionalandmultiplewrappers now avoid adding redundant brackets around inneroptionterms that already imply optionality. [#197, #586]Fixed
generateManPage*()droppingbrief,description, andfooterfromProgrammetadata. These fields are now forwarded to the man page output. Addedbrief,description, andfootertoManPageOptionsso they can also be passed as explicit overrides in both the parser-based and program-based APIs. [#260, #598]Fixed
formatDocPageAsMan()labeling untitled sections asOPTIONSregardless of entry kinds. Untitled command-only sections now render asCOMMANDS, and untitled argument-only sections render asARGUMENTS. [#261, #601]
@optique/valibot
valibot()now exposes choice metadata forv.picklist(), string-valuedv.literal()schemas, andv.union()schemas composed entirely of string literals. Help text withshowChoicesenabled now displays valid choices (e.g.,(choices: debug, info, warn, error)), and shell completion suggests matching values. [#281, #688]valibot()now infers the metavar"CHOICE"instead of"VALUE"forv.literal()schemas with string values, and forv.union()schemas composed entirely of string literals. [#281, #688]valibot()now validates themetavaroption at runtime and throws aTypeErrorfor empty strings. Previously, an emptymetavarwould silently produce malformed help output. [#460, #691]valibot()now throws aTypeErrorat construction time when given an async schema (e.g.,v.pipeAsync()withv.checkAsync()). Previously, async validations were silently skipped by the synchronoussafeParse()call. [#462, #701]valibot()now detects top-level async schemas returned fromv.lazy()at parse time and throws a clearTypeError. Previously,v.lazy()wrapping an async schema (e.g.,v.pipeAsync()) silently returned{ success: true, value: undefined }. [#461, #731]valibot()now formats transformed non-primitive values intelligently instead of producing[object Object].Datevalues use.toISOString()for stable output. Plain objects useJSON.stringify(), while arrays useString()to preserve round-trip compatibility for common transforms. A newformatoption inValibotParserOptionsallows custom formatting. [#285, #706]Breaking change:
valibot()now requires aplaceholderoption inValibotParserOptions. This value is used as a type-appropriate stand-in during deferred prompt resolution. [#407, #727]
@optique/zod
Breaking change:
zod()now requires aplaceholderoption inZodParserOptions. This value is used as a type-appropriate stand-in during deferred prompt resolution. [#407, #727]zod()now exposes choice metadata forz.enum(), string-valuedz.nativeEnum()andz.literal()schemas, andz.union()schemas composed entirely of string literals. Help text withshowChoicesenabled now displays valid choices (e.g.,(choices: debug, info, warn, error)), and shell completion suggests matching values. [#281, #688]zod()now infers the metavar"CHOICE"instead of"VALUE"forz.literal()schemas with string values, and forz.union()schemas composed entirely of string literals. [#281, #688]zod()now validates themetavaroption at runtime and throws aTypeErrorfor empty strings. Previously, an emptymetavarwould silently produce malformed help output. [#460, #691]zod()now throws aTypeErrorwhen an async Zod schema (e.g., async refinements) is used. Previously, the raw Zod error was propagated unhandled. [#462, #701]zod()now formats transformed non-primitive values intelligently instead of producing[object Object].Datevalues use.toISOString()for stable output. Plain objects useJSON.stringify(), while arrays useString()to preserve round-trip compatibility for common transforms. A newformatoption inZodParserOptionsallows custom formatting. [#285, #706]zod()now uses CLI-friendly Boolean parsing forz.boolean()andz.coerce.boolean()schemas instead of JavaScript truthiness semantics. Accepted literals (case-insensitive):true/false,1/0,yes/no,on/off. Recognized boolean schemas expose CLI-friendly completion metadata (choices/suggest()) when applicable. [#295, #712]
Version 0.10.7
Released on March 4, 2026.
@optique/core
- Fixed
argument()parser returning repeated suggestions after a value had already been consumed. Thesuggest()method now returns an empty iterable whencontext.stateis non-null (i.e., an argument has been parsed). Additionally,multiple()now passesparser.initialStateinstead of the last consumed inner state when requesting suggestions from its inner parser, so thatmultiple(argument(...))continues to suggest remaining choices after the first value has been consumed. [#133 by Ben van Enckevort]
Version 0.10.6
Released on February 20, 2026.
@optique/core
Fixed
runWith()(and by extensionrunWithConfig()) crashing withTypeError: Cannot read properties of undefinedwhen the top-level parser has anundefinedinitial state, such aswithDefault(object({...})). The root cause was thatinjectAnnotationsIntoParser()unconditionally spreadparser.initialStateinto a new object with the annotation key, turningundefinedinto{ [annotationKey]: annotations }. This corrupted the state for parsers likewithDefault()andoptional()that rely ontypeof state === "undefined"to distinguish the uninitialized state from a wrapped inner state. The fix skips annotation injection when the initial state isnullorundefined. The same guard was applied to annotation injection inparseSync(),parseAsync(),suggestSync(),suggestAsync(), andgetDocPage(). [#131]Fixed
formatDocPage()to respectmaxWidthwhen the rendered option term is wider thantermWidth(default: 26). Previously, the description column width was calculated assuming the term occupied exactlytermWidthcharacters. When the actual term was wider (e.g.,-p, --package-manager PACKAGE_MANAGERat 38 chars), the description column started further right on the first output line, butformatMessage()was still given the fulldescColumnWidthbudget, allowing lines to exceedmaxWidthby as much as the term's extra width. The fix passes the extra width asstartWidthtoformatMessage()so the first-line budget is correctly narrowed. The same correction is applied when appending default values (showDefault) and available choices (showChoices). [#132]Fixed
formatDocPage()to account for the closing suffix (]for default values,)for choices) when word-wrapping content. Previously, the suffix was appended afterformatMessage()had already filled the description column to capacity, producing lines that were one character too wide.
Version 0.10.5
Released on February 20, 2026.
@optique/core
Fixed meta options (
--help,--version,--completion) being absent from both the usage line and the options list in the full help page, even though they appear correctly in the usage line shown above parse errors. The root cause was thathelpGeneratorParser—the parser used to produce the help page—was built from the user's parser and any meta commands, but never included the meta option parsers (helpOption,versionOption,completionOption). As a result,getDocFragments()was never called on those parsers, so their entries were silently omitted. The fix adds the meta option parsers to thecommandParserslist whenever the corresponding mode is"option"or"both". [#127]Fixed
formatDocPage()to respectmaxWidthwhen appending default values and available choices viashowDefaultandshowChoices. Previously, the appended text was concatenated onto the description string after word-wrapping had already been applied, with no awareness of how much of the current line was already occupied, so lines could far exceedmaxWidth. [#129]
Version 0.10.4
Released on February 19, 2026.
@optique/core
Fixed
formatMessage()to correctly handle alineBreak()term that is immediately followed by a newline character in the source template literal. Previously, the newline after${lineBreak()}was normalized to a space (as single\ncharacters in text terms are), producing a spurious leading space at the start of the next line. The newline immediately following alineBreak()term is now dropped instead of being converted to a space.Fixed meta commands (
help,version,completion,completions) disappearing from the subcommand list in help output when the parser uses awithDefault(or(...))construct. The root cause was thatgetDocPage()used ado...whileloop, which ran the parser at least once even with an empty argument buffer. BecausewithDefault(or(...))allows the inner parser to succeed without consuming any tokens, thelongestMatchcombinator would record the user's parser as “selected” and subsequently return only that parser's doc fragments—silently dropping the meta command entries. The loop is now awhileloop that skips parsing entirely when the buffer is empty. [#121]
Version 0.10.3
Released on February 18, 2026.
@optique/core
Fixed how
briefanddescriptionare displayed in subcommand help pages. [#118, #119]Each help page now shows
briefat the very top (before the Usage line) anddescriptionbelow the Usage line, consistent with how the root-level help page works.The
run()-levelbriefanddescriptionno longer bleed into a subcommand's help page. Previously, when a subcommand had nodescriptionof its own, the description supplied torun()would appear on the subcommand's help page (e.g.repro file --helpwould show “Description for repro CLI” even though that text describes the top-level program, not thefilesubcommand). Now, only the subcommand's ownbriefanddescription(if any) are shown; run-level docs are used only for the root-level help page.When
command()is wrapped withgroup(), the command'sbrief,description, andfooterare now correctly forwarded to the help page. Previouslygroup()only forwardeddescription, sobriefwas silently dropped and the run-level brief appeared instead.
Fixed contradictory “Did you mean?” suggestion when a subcommand name is provided at the wrong level. Previously, a structure like
command("file", or(add, remove))given the inputadd --helpwould reportExpected command file, but got add.and then illogically suggestDid you mean add?even thoughaddis only valid insidefile. Suggestions incommand()errors now derive fromextractLeadingCommandNames(), which limits candidates to commands that are actually valid at the current parse position, not commands nested inside other commands. The same scoping is applied when a customnotMatchedcallback receives itssuggestionsargument. [#117]Fixed
group()label leaking into a selected subcommand's own nested command list. Previously, when agroup()-wrapped command (e.g.,alias) itself contained further subcommands (e.g.,delete,set), viewingalias --helpwould show those inner commands under the outer group's label (e.g., “Additional commands:”), which was incorrect. The fix compares current command entries against the initial set of commands that the group originally labeled; the label is now applied only when the visible commands are the group's own top-level commands, not commands from a deeper level. [#116]
Version 0.10.2
Released on February 18, 2026.
@optique/core
Fixed
group()incorrectly applying its label to subcommand flags in help output. Whengroup()wrapscommand()parsers (viaor()), the group label is now only shown for the command list at the top level, not for the inner flags/options after a command is selected. [#114]Fixed
merge()not propagatingbrief,description, andfooterfrom inner parsers in help output. When using themerge(or(...commands), globalOptions)pattern, subcommand help (e.g.,myapp subcommand --help) now correctly displays the command's description. Previously, these fields were silently discarded bymerge().
@optique/config
- Fixed
runWithConfig()showing a rawSyntaxErrorstack trace when a config file contains malformed JSON. It now prints a friendly error message that includes the file path. [#109]
Version 0.10.1
Released on February 17, 2026.
@optique/core
- Fixed usage string collapsing to a single line when
completionwithname: "both"created a nested exclusive term (e.g.,(completion | completions)) inside an outer exclusive.normalizeUsageTerm()now distributes an exclusive that appears as the first term of a branch across the remaining terms, so that[exclusive(A, B), C]is flattened into separate branches[A, C]and[B, C]. This allowsformatUsage()withexpandCommands: trueto properly render each alternative on its own line.
@optique/config
- Fixed
bindConfig()usage rendering whendefaultis provided. The generated usage term is now wrapped as optional, so usage strings correctly show the bound option in square brackets ([]).
Version 0.10.0
Released on February 15, 2026.
@optique/core
Added annotations system for passing runtime context to parsers. This allows parsers to access external runtime data during both
parse()andcomplete()phases, enabling use cases like config file fallbacks, environment-based validation, and shared context. [#83]The annotations system uses symbol-keyed records to avoid naming conflicts between packages. Annotations are passed via the new
ParseOptionsparameter and can be accessed using thegetAnnotations()helper function.New exports from
@optique/core/annotations:annotationKey: Symbol for storing annotations in parser state.Annotations: Type for annotation records (symbol-keyed objects).ParseOptions: Options interface for parse functions.getAnnotations(): Helper function to extract annotations from state.
Updated function signatures (all now accept optional
ParseOptions):parse(),parseSync(),parseAsync()suggest(),suggestSync(),suggestAsync()getDocPage(),getDocPageSync(),getDocPageAsync()
import { parse, getAnnotations } from "@optique/core/parser"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; // Define annotation key for your package const configDataKey = Symbol.for("@myapp/config"); // Two-pass parsing with config file const firstPass = parse(parser, args); const configData = await loadConfig(firstPass.value.configPath); const finalResult = parse(parser, args, { annotations: { [configDataKey]: configData, }, }); // Access annotations in custom parser function myCustomParser() { return { // ... parser implementation complete: (state) => { const annotations = getAnnotations(state); const config = annotations?.[configDataKey]; // Use config data for fallback values }, }; }This is a backward-compatible change. Existing code continues to work unchanged as the
optionsparameter is optional and annotations are only used when explicitly provided.See the runtime context extension guide for detailed documentation and usage patterns.
Added
SourceContextsystem andrunWith()functions for composing multiple data sources with automatic priority handling. This builds on the annotations system to provide a higher-level abstraction for common use cases like environment variable fallbacks and config file loading. [#85]The source context system enables packages to provide data sources in a standard way with clear priority ordering. Earlier contexts in the array override later ones, making it natural to implement fallback chains like CLI > environment variables > configuration file > default values.
New exports from
@optique/core/context:SourceContext: Interface for data sources that provide annotations.Annotations: Re-exported from@optique/core/annotations.isStaticContext(): Helper to check if a context is static.
New exports from
@optique/core/facade:runWith(): Run parser with multiple source contexts (async).runWithSync(): Sync-only variant ofrunWith().runWithAsync(): Explicit async variant ofrunWith().
The
runWith()function uses a smart two-phase approach:- Collect annotations from all contexts (static return immediately, dynamic may return empty)
- First parse with Phase 1 annotations
- Call
getAnnotations(parsed)on all contexts with parsed result - Second parse with merged annotations from both phases
If all contexts are static, the second parse is skipped for optimization.
The
runWith()function ensures that help, version, and completion features always work, even when config files are missing or contexts would fail to load. Users can always access--help,--version, and completion without requiring a valid environment setup. [#92]import { runWith } from "@optique/core/facade"; import type { SourceContext } from "@optique/core/context"; const envContext: SourceContext = { id: Symbol.for("@myapp/env"), getAnnotations() { return { [Symbol.for("@myapp/env")]: { HOST: process.env.MYAPP_HOST, } }; } }; const result = await runWith( parser, "myapp", [envContext], { args: process.argv.slice(2) } );Added
url()message component for displaying URLs with clickable hyperlinks in terminals that support OSC 8 escape sequences. URLs are validated usingURL.canParse()and stored asURLobjects internally. When colors are enabled, URLs are rendered with OSC 8 hyperlink sequences making them clickable in compatible terminals. When quotes are enabled, URLs are wrapped in angle brackets (<>).import { message, url } from "@optique/core/message"; const helpMsg = message`Visit ${url("https://example.com")} for details.`;New exports:
url(): Creates a URL message term from a string or URL object.link(): Alias forurl(), useful for avoiding naming conflicts with theurl()value parser from@optique/core/valueparser.
Added
lineBreak()message component for explicit single-line breaks in structured messages. Unlike single\nintext()terms (which are treated as soft breaks and rendered as spaces),lineBreak()always renders as a hard line break.import { commandLine, lineBreak, message } from "@optique/core/message"; const examples = message`Examples:${lineBreak()} Bash: ${commandLine(`eval "$(mycli completion bash)"`)}${lineBreak()} zsh: ${commandLine(`eval "$(mycli completion zsh)"`)}`;Added
@optique/core/programmodule withProgramandProgramMetadatainterfaces. These provide a structured way to bundle a parser with its metadata (name, version, description, etc.), creating a single source of truth for CLI application information. [#82]New exports:
Program<M, T>: Interface bundling a parser with metadata.ProgramMetadata: Interface for program metadata including name, version, brief, description, author, bugs, examples, and footer.defineProgram(): Helper function for automatic type inference when creatingProgramobjects. This eliminates the need to manually specifyProgram<"sync", InferValue<typeof parser>>type annotations.
Added support for displaying
author,bugs, andexamplesmetadata fields in help text. When these fields are provided inProgramMetadataorRunOptions, they are now displayed in the help output in the following order: Examples → Author → Bugs (before the footer). Each section has a bold label (when colors are enabled) and indented content for clear visual separation.import { defineProgram } from "@optique/core/program"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; import { message } from "@optique/core/message"; const prog = defineProgram({ parser: option("--name", string()), metadata: { name: "myapp", version: "1.0.0", brief: message`A sample CLI application`, description: message`This tool processes data efficiently.`, author: message`Jane Doe <jane@example.com>`, }, });Updated
runParser()to acceptProgramobjects directly. The new API automatically extracts program name and metadata from theProgramobject, eliminating the need to pass these separately. Both the newProgram-based API and the originalparser-based API are supported. [#82]import { runParser } from "@optique/core/facade"; // Program-based API (recommended for applications with metadata) const result = runParser(prog, ["--name", "Alice"]); // Parser-based API (useful for simple scripts) const result = runParser(parser, "myapp", ["--name", "Alice"], { brief: message`A sample CLI application`, // ... });Added inter-option dependency support via
@optique/core/dependencymodule. This allows one option's valid values to depend on another option's parsed value, enabling dynamic validation and context-aware shell completion. [#74, #76]New exports:
dependency(): Creates a dependency source from an existing value parser.deriveFrom(): Creates a derived parser from multiple dependency sources.DependencySource<M, T>: A value parser that can be referenced by other parsers.DerivedValueParser<M, T>: A value parser whose behavior depends on other parsers' values.
The dependency system uses deferred parsing: dependent options store their raw input during initial parsing, then re-validate using actual dependency values after all options are collected.
import { dependency } from "@optique/core/dependency"; import { choice } from "@optique/core/valueparser"; // Create a dependency source const modeParser = dependency(choice(["dev", "prod"] as const)); // Create a derived parser that depends on the mode const logLevelParser = modeParser.derive({ metavar: "LEVEL", factory: (mode) => { if (mode === "dev") { return choice(["debug", "info", "warn", "error"]); } else { return choice(["warn", "error"]); } }, defaultValue: () => "dev" as const, });Dependencies work seamlessly with all parser combinators (
object(),subcommands(),or(),longestMatch(),multiple(), etc.) and support both sync and async parsers.Added
nonEmpty()modifier that requires the wrapped parser to consume at least one input token to succeed. This enables conditional default values and help display logic when usinglongestMatch(). [#79, #80]import { longestMatch, object } from "@optique/core/constructs"; import { nonEmpty, optional, withDefault } from "@optique/core/modifiers"; import { constant, option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; // Without nonEmpty(): activeParser always wins (consumes 0 tokens) // With nonEmpty(): helpParser wins when no options are provided const activeParser = nonEmpty(object({ mode: constant("active" as const), cwd: withDefault(option("--cwd", string()), "./default"), key: optional(option("--key", string())), })); const helpParser = object({ mode: constant("help" as const) }); const parser = longestMatch(activeParser, helpParser);Added
port()value parser for TCP/UDP port numbers with support for bothnumberandbiginttypes, range validation, and well-known port restrictions. The parser validates port numbers (1–65535 by default) and optionally enforces custom ranges or disallows well-known ports (1–1023). [#89]import { option } from "@optique/core/primitives"; import { port } from "@optique/core/valueparser"; // Basic port parser (1-65535) option("--port", port()) // Non-privileged ports only option("--port", port({ min: 1024, max: 65535 })) // Disallow well-known ports option("--port", port({ disallowWellKnown: true })) // Using bigint type option("--port", port({ type: "bigint" }))Added
ipv4()value parser for IPv4 address validation with filtering options for private, loopback, link-local, multicast, broadcast, and zero addresses. The parser validates IPv4 addresses in dotted-decimal notation (e.g., “192.168.1.1”) and provides fine-grained control over which address types are accepted. [#89]import { option } from "@optique/core/primitives"; import { ipv4 } from "@optique/core/valueparser"; // Basic IPv4 parser (allows all types) option("--ip", ipv4()) // Public IPs only (no private/loopback) option("--public-ip", ipv4({ allowPrivate: false, allowLoopback: false })) // Server binding address option("--bind", ipv4({ allowZero: true, allowPrivate: true }))Added
hostname()value parser for DNS hostname validation with support for wildcards, underscores, localhost filtering, and length constraints. The parser validates hostnames according to RFC 1123 with configurable options for special cases like service discovery records and SSL certificate domains. [#89]import { option } from "@optique/core/primitives"; import { hostname } from "@optique/core/valueparser"; // Basic hostname parser option("--host", hostname()) // Allow wildcards for SSL certificates option("--domain", hostname({ allowWildcard: true })) // Reject localhost for remote connections option("--remote", hostname({ allowLocalhost: false })) // Service discovery records (allow underscores) option("--srv", hostname({ allowUnderscore: true }))Added
email()value parser for email address validation with support for display names, multiple addresses, domain filtering, and quoted local parts. The parser validates email addresses according to simplified RFC 5322 addr-spec format with practical defaults for common use cases. [#89]import { option } from "@optique/core/primitives"; import { email } from "@optique/core/valueparser"; // Basic email parser option("--email", email()) // Multiple email addresses option("--to", email({ allowMultiple: true })) // Allow display names option("--from", email({ allowDisplayName: true })) // Restrict to company domains option("--work-email", email({ allowedDomains: ["company.com"] })) // Convert to lowercase option("--email", email({ lowercase: true }))Added
socketAddress()value parser for socket addresses in “host:port” format. The parser validates both hostname and IPv4 addresses combined with port numbers, with support for default ports, custom separators, and host type filtering. Returns aSocketAddressValueobject containing both host and port. [#89]import { option } from "@optique/core/primitives"; import { socketAddress } from "@optique/core/valueparser"; // Basic socket address (requires port) option("--endpoint", socketAddress({ requirePort: true })) // With default port option("--server", socketAddress({ defaultPort: 80 })) // IP addresses only option("--bind", socketAddress({ defaultPort: 8080, host: { type: "ip" } })) // Non-privileged ports only option("--listen", socketAddress({ defaultPort: 8080, port: { min: 1024 } }))Added
portRange()value parser for port ranges (e.g., “8000-8080”). The parser validates port ranges with support for bothnumberandbiginttypes, custom separators, single port mode, and min/max constraints. Returns aPortRangeValueobject containingstartandendports. [#89]import { option } from "@optique/core/primitives"; import { portRange } from "@optique/core/valueparser"; // Basic port range option("--ports", portRange()) // Allow single port (returns range with start === end) option("--ports", portRange({ allowSingle: true })) // Custom separator option("--ports", portRange({ separator: ":" })) // Non-privileged ports only option("--ports", portRange({ min: 1024 })) // Using bigint type option("--ports", portRange({ type: "bigint" }))Added
macAddress()value parser for MAC (Media Access Control) addresses. The parser validates MAC-48 addresses in multiple formats (colon-separated, hyphen-separated, Cisco dot notation, or no separator) with support for case conversion and output normalization. Returns a formatted string. [#89]import { option } from "@optique/core/primitives"; import { macAddress } from "@optique/core/valueparser"; // Accept any format option("--mac", macAddress()) // Normalize to uppercase colon-separated option("--mac", macAddress({ outputSeparator: ":", case: "upper" })) // Cisco format only option("--mac", macAddress({ separator: ".", case: "lower" }))Added
domain()value parser for domain name validation. The parser validates domain names according to RFC 1035 with configurable options for subdomain filtering, TLD restrictions, minimum label requirements, and case normalization. Returns a formatted string. [#89]import { option } from "@optique/core/primitives"; import { domain } from "@optique/core/valueparser"; // Accept any valid domain option("--domain", domain()) // Root domains only (no subdomains) option("--root", domain({ allowSubdomains: false })) // Restrict to specific TLDs option("--domain", domain({ allowedTLDs: ["com", "org", "net"] })) // Normalize to lowercase option("--domain", domain({ lowercase: true }))Added
ipv6()value parser for IPv6 address validation. The parser validates and normalizes IPv6 addresses to canonical form (lowercase, compressed using::notation) with support for full addresses, compressed notation, and IPv4-mapped IPv6 addresses. Configurable address type restrictions for loopback, link-local, unique local, multicast, and zero addresses. Returns a normalized string. [#89]import { option } from "@optique/core/primitives"; import { ipv6 } from "@optique/core/valueparser"; // Accept any IPv6 address option("--ipv6", ipv6()) // Global unicast only (no link-local, no unique local) option("--public-ipv6", ipv6({ allowLinkLocal: false, allowUniqueLocal: false })) // No loopback addresses option("--ipv6", ipv6({ allowLoopback: false }))Added
ip()value parser that accepts both IPv4 and IPv6 addresses. The parser can be configured to accept only IPv4, only IPv6, or both (default). Delegates toipv4()andipv6()parsers based on version setting, with intelligent error handling that preserves specific validation errors. Returns a normalized IP address string. [#89]import { option } from "@optique/core/primitives"; import { ip } from "@optique/core/valueparser"; // Accept both IPv4 and IPv6 option("--ip", ip()) // IPv4 only option("--ipv4", ip({ version: 4 })) // Public IPs only (both versions) option("--public-ip", ip({ ipv4: { allowPrivate: false, allowLoopback: false }, ipv6: { allowLinkLocal: false, allowUniqueLocal: false } }))Added
cidr()value parser for CIDR notation validation. The parser validates CIDR notation like192.168.0.0/24or2001:db8::/32and returns a structured object with the normalized IP address, prefix length, and IP version. Supports prefix length constraints and delegates IP validation toipv4()andipv6()parsers. [#89]import { option } from "@optique/core/primitives"; import { cidr } from "@optique/core/valueparser"; // Accept both IPv4 and IPv6 CIDR option("--network", cidr()) // IPv4 CIDR only with prefix constraints option("--subnet", cidr({ version: 4, minPrefix: 16, maxPrefix: 24 }))Removed deprecated
runexport. UserunParser()instead. The old name was deprecated in v0.9.0 due to naming conflicts with@optique/run'srun()function. [#65]Removed deprecated
RunErrorexport. UseRunParserErrorinstead. This was renamed in v0.9.0 for consistency with therunParser()rename. [#65]Added
helpVisibilityto completion configuration inrunParser()andrunWith*()APIs. This allows keeping both singular and plural completion aliases enabled at runtime while controlling whether help shows singular, plural, both, or none of the completion entries. Type definitions now enforce validnameandhelpVisibilitycombinations at compile time. [#99]Added
showChoicesoption toformatDocPage()andrunParser()for displaying valid choices in help output. When enabled, options and arguments backed bychoice()value parsers automatically show their valid values in help text, similar to the existingshowDefaultfeature. [#106]import { runParser } from "@optique/core/facade"; // Basic usage — shows "(choices: json, yaml, xml)" runParser(parser, "myapp", args, { showChoices: true }); // Custom format — shows "{json | yaml | xml}" runParser(parser, "myapp", args, { showChoices: { prefix: " {", suffix: "}", label: "" }, });New APIs:
ValueParser.choices: Optional array of valid choices, populated automatically by thechoice()function.DocEntry.choices: Formatted choices as aMessagefor help rendering.ShowChoicesOptions: Interface for customizing choices display (prefix, suffix, label, maxItems).DocPageFormatOptions.showChoices: Enables choices display in formatted help output.RunOptions.showChoices: Passes through toformatDocPage().
Added
groupoption for meta-commands (help, version, completion) inRunOptions. When specified, the meta-command appears under a titled section in help output instead of alongside user-defined commands. Commands sharing the same group name are merged into a single section. [#107]The
groupoption is only available when the meta-command has a command mode ("command"or"both"). When the mode is"option", thegroupoption is blocked at the type level (group?: never).import { runParser } from "@optique/core/facade"; runParser(parser, "myapp", args, { help: { mode: "both", group: "Other" }, version: { mode: "both", value: "1.0.0", group: "Other" }, completion: { mode: "both", group: "Other" }, });
@optique/config
The @optique/config package was introduced in this release, providing configuration file support with type-safe validation using Standard Schema. [#84, #90]
Added
createConfigContext()function for creating configuration contexts with Standard Schema validation.Added
bindConfig()function to bind parsers to configuration values with automatic fallback handling.Added
runWithConfig()function for running parsers with two-pass parsing to load and validate configuration files. The function delegates torunWith()internally, automatically providing help, version, and shell completion support. These features work even when config files are missing or invalid, ensuring users can always access help documentation. [#93]Added
ConfigContextinterface implementingSourceContextfor composable data source integration.Added
configKeysymbol for storing config data in annotations.Added
SingleFileOptionsinterface for single-file config loading with optionalfileParserfor custom file formats.Added
CustomLoadOptionsinterface withloadcallback for multi-file merging scenarios (system, user, project config cascading).RunWithConfigOptionsis a discriminated union ofSingleFileOptionsandCustomLoadOptions.
The package uses a two-phase parsing approach to ensure proper priority (CLI > config file > default values). See the config file integration guide for usage examples.
@optique/run
Added
contextsoption torun(),runSync(), andrunAsync()for source context support. When provided, the runner delegates torunWith()(orrunWithSync()) from@optique/core/facade, which handles annotation collection, two-phase parsing, and context disposal automatically. Context-specific options (e.g.,getConfigPath,load) are passed through alongside the standard runner options. [#110]import { runAsync } from "@optique/run"; const result = await runAsync(parser, { contexts: [configContext], getConfigPath: (parsed) => parsed.config, help: "both", });Updated
run(),runSync(), andrunAsync()to acceptProgramobjects directly. The new API automatically extracts program name and metadata from theProgramobject. Both the newProgram-based API and the originalparser-based API are supported. [#82]import { run } from "@optique/run"; // Program-based API (recommended for applications with metadata) const result = run(prog, { help: "both", colors: true, }); // Parser-based API (useful for simple scripts) const result = run(parser, { programName: "myapp", help: "both", colors: true, brief: message`A sample CLI application`, // ... });Added
completion.helpVisibilitytorun()options. This controls whether completion aliases are shown in help and usage output ("singular","plural","both", or"none") independently from which aliases are accepted at runtime viacompletion.name. [#99]Added
showChoicesoption torun(),runSync(), andrunAsync(). This passes through to the underlyingformatDocPage()call, enabling valid choice values to be displayed in help output. [#106]Added
groupoption for help, version, and completion configurations. When specified, the corresponding meta-command appears under a titled section in help output. Thegroupoption is available when the mode is"command"or"both"(blocked at the type level for"option"mode). [#107]import { run } from "@optique/run"; run(parser, { help: { mode: "both", group: "Other" }, version: { value: "1.0.0", mode: "both", group: "Other" }, completion: { mode: "both", group: "Other" }, });
@optique/man
The @optique/man package was introduced in this release, providing man page generation from parser metadata. This allows CLI applications to generate Unix man pages that stay synchronized with parser definitions. [#77]
Added
generateManPage()function for generating man pages from sync parsers.Added
generateManPageSync()function for explicit sync-only man page generation.Added
generateManPageAsync()function for generating man pages from async parsers.Added
ManPageOptionsinterface for configuring man page output (name, section, description, date, source, manual, authors, seeAlso, bugs).Added
formatDocPageAsMan()function for convertingDocPageobjects to man page format.Added
formatMessageAsRoff()function for converting OptiqueMessageobjects to roff markup.Added
escapeRoff()function for escaping special roff characters.Added
escapeHyphens()function for escaping hyphens in option names.Updated
generateManPage(),generateManPageSync(), andgenerateManPageAsync()to acceptProgramobjects directly. The metadata (name,version,author,bugs,examples) is automatically extracted from the program, eliminating duplication. [#82]import { defineProgram } from "@optique/core/program"; import { generateManPage } from "@optique/man"; const prog = defineProgram({ parser: myParser, metadata: { name: "myapp", version: "1.0.0", author: message`Hong Minhee`, }, }); // Metadata is automatically extracted const manPage = generateManPage(prog, { section: 1 });Added
optique-manCLI tool for generating man pages from TypeScript/JavaScript files that export aProgramorParser. This enables automated man page generation as part of build processes. [#77]# Generate man page from a Program export optique-man ./src/cli.ts -s 1 # Use a named export instead of default optique-man ./src/cli.ts -s 1 -e myProgram # Write output to a file optique-man ./src/cli.ts -s 1 -o myapp.1The CLI supports:
- Loading TypeScript files directly (Deno, Bun, Node.js 25.2.0+ native, or Node.js with
tsxinstalled). - Extracting metadata from
Programobjects or using command-line options forParserobjects. - Overriding metadata via
--name,--date,--version-string, and--manualoptions.
- Loading TypeScript files directly (Deno, Bun, Node.js 25.2.0+ native, or Node.js with
Version 0.9.10
Released on February 19, 2026.
@optique/core
- Fixed meta commands (
help,version,completion,completions) disappearing from the subcommand list in help output when the parser uses awithDefault(or(...))construct. The root cause was thatgetDocPage()used ado...whileloop, which ran the parser at least once even with an empty argument buffer. BecausewithDefault(or(...))allows the inner parser to succeed without consuming any tokens, thelongestMatchcombinator would record the user's parser as “selected” and subsequently return only that parser's doc fragments—silently dropping the meta command entries. The loop is now awhileloop that skips parsing entirely when the buffer is empty. [#121]
Version 0.9.9
Released on February 18, 2026.
@optique/core
Fixed how
briefanddescriptionare displayed in subcommand help pages. [#118, #119]Each help page now shows
briefat the very top (before the Usage line) anddescriptionbelow the Usage line, consistent with how the root-level help page works.The
run()-levelbriefanddescriptionno longer bleed into a subcommand's help page. Previously, when a subcommand had nodescriptionof its own, the description supplied torun()would appear on the subcommand's help page (e.g.repro file --helpwould show “Description for repro CLI” even though that text describes the top-level program, not thefilesubcommand). Now, only the subcommand's ownbriefanddescription(if any) are shown; run-level docs are used only for the root-level help page.When
command()is wrapped withgroup(), the command'sbrief,description, andfooterare now correctly forwarded to the help page. Previouslygroup()only forwardeddescription, sobriefwas silently dropped and the run-level brief appeared instead.
Fixed contradictory “Did you mean?” suggestion when a subcommand name is provided at the wrong level. Previously, a structure like
command("file", or(add, remove))given the inputadd --helpwould reportExpected command file, but got add.and then illogically suggestDid you mean add?even thoughaddis only valid insidefile. Suggestions incommand()errors now derive fromextractLeadingCommandNames(), which limits candidates to commands that are actually valid at the current parse position, not commands nested inside other commands. The same scoping is applied when a customnotMatchedcallback receives itssuggestionsargument. [#117]Fixed
group()label leaking into a selected subcommand's own nested command list. Previously, when agroup()-wrapped command (e.g.,alias) itself contained further subcommands (e.g.,delete,set), viewingalias --helpwould show those inner commands under the outer group's label (e.g., “Additional commands:”), which was incorrect. The fix compares current command entries against the initial set of commands that the group originally labeled; the label is now applied only when the visible commands are the group's own top-level commands, not commands from a deeper level. [#116]
Version 0.9.8
Released on February 18, 2026.
@optique/core
Fixed
group()incorrectly applying its label to subcommand flags in help output. Whengroup()wrapscommand()parsers (viaor()), the group label is now only shown for the command list at the top level, not for the inner flags/options after a command is selected. [#114]Fixed
merge()not propagatingbrief,description, andfooterfrom inner parsers in help output. When using themerge(or(...commands), globalOptions)pattern, subcommand help (e.g.,myapp subcommand --help) now correctly displays the command's description. Previously, these fields were silently discarded bymerge().
Version 0.9.7
Released on February 17, 2026.
@optique/core
- Fixed usage string collapsing to a single line when
completionwithname: "both"created a nested exclusive term (e.g.,(completion | completions)) inside an outer exclusive.normalizeUsageTerm()now distributes an exclusive that appears as the first term of a branch across the remaining terms, so that[exclusive(A, B), C]is flattened into separate branches[A, C]and[B, C]. This allowsformatUsage()withexpandCommands: trueto properly render each alternative on its own line.
Version 0.9.6
Released on February 14, 2026.
@optique/core
- Fixed the
completioncommand appearing before user-defined commands in the usage line shown on parse errors. When usingcompletionin command mode withor()-combined commands, the error output now lists user commands first and meta-commands (version, completion, help) after them. [#107]
Version 0.9.5
Released on February 13, 2026.
@optique/core
Fixed contradictory suggestion messages for subcommand-only options at the root command level. Previously, when an option that belongs to a subcommand was entered before specifying the subcommand (for example,
mycli --fooflag 123where--fooflagbelongs tomycli foo), the parser could reportUnexpected option or subcommandand still suggest the same option. Suggestions now only include options and commands that are valid at the current parse position. [#98]Fixed
--completionand--completionswithout a shell value in option mode reporting a generic parse error. Previously, inputs likemycli --completioncould fall through to normal argument parsing and showUnexpected option or subcommand, which was misleading. These now produce the dedicated completion error for a missing shell name. [#100]
Version 0.9.4
Released on February 12, 2026.
@optique/core
- Fixed
mycli subcommand --helpdisplaying thebriefanddescriptionpassed torun()instead of those passed tocommand(). Now, when showing help for a specific subcommand, the subcommand's ownbrief,description, andfootertake priority over therun()-level values. Therun()-level values are used as fallback only when the subcommand does not define its own. [#95]
Version 0.9.3
Released on February 12, 2026.
@optique/core
- Fixed
run()showing top-level help instead of an error when--helpis used with an invalid subcommand (e.g.,mycli nonexistent --help). Previously, the help handler did not validate whether the commands extracted before--helpwere actually recognized by the parser, so it silently fell back to displaying root-level help. Now, the commands are validated against the parser, and an appropriate error message with usage information is shown instead. [#97] - Fixed nested subcommand help showing sibling subcommand usage lines. Previously, when using
or(topLevel, command("nested", or(foo, bar))), runningmycli nested foo --helpwould incorrectly display usage lines for bothfooandbarsubcommands. Now, only the selected subcommand's usage is shown. [#96]
Version 0.9.2
Released on January 30, 2026.
@optique/core
Fixed
command()withhidden: trueincorrectly hiding inner argument/option descriptions when the hidden command is matched and executed. Previously, when a hidden command was invoked with--help, the help text would not show descriptions for the command's arguments and options, even when those arguments and options were not hidden. Now, thehiddenoption only applies when the command is shown in a command list (when the command is not matched), and inner parser documentation is displayed normally when the hidden command is actually executed. [#88]Fixed
formatUsage()incorrectly showing hidden commands in expanded usage lines. Previously, when usingexpandCommands: trueoption, hidden commands would appear in the usage output even though they were correctly excluded from the command list. Now, hidden commands are filtered out from expanded usage lines while remaining fully functional for parsing.
Version 0.9.1
Released on January 19, 2026.
@optique/core
- Fixed
command()failing to parse inner parser when buffer is empty after command match. Previously, when usingcommand()with an inner parser that can succeed with zero tokens (likelongestMatch()orobject()with all optional fields), parsing["dev"]would fail with “No matching option, command, or argument found” even though the inner parser should succeed. Now, thecomplete()method first gives the inner parser a chance to run with an empty buffer before completing. [#81]
Version 0.9.0
Released on January 6, 2026.
@optique/core
Added sync/async mode support to
ParserandValueParserinterfaces. The newM extends Mode = "sync"type parameter enables type-safe distinction between synchronous and asynchronous parsers. [#52, #70]New types:
Mode: Type alias for"sync" | "async".ModeValue<M, T>: ReturnsTfor sync mode,Promise<T>for async.ModeIterable<M, T>: ReturnsIterable<T>for sync,AsyncIterable<T>for async.CombineModes<T extends readonly Mode[]>: Returns"async"if any element is"async", otherwise"sync".InferMode<P>: Extracts the mode from a parser type.
All parsers now include a
$modeproperty indicating their execution mode. Combinators automatically propagate modes from their constituent parsers, resulting in"async"mode if any child parser is async.New functions for explicit mode handling:
parseSync(): Parses with a sync-only parser, returning directly.parseAsync(): Parses with any parser, returning a Promise.suggestSync(): Gets suggestions from a sync-only parser.suggestAsync(): Gets suggestions from any parser as a Promise.getDocPageSync(): Gets documentation page from a sync-only parser.getDocPageAsync(): Gets documentation page from any parser as a Promise.runParserSync(): Runs a sync-only parser, returning the result directly.runParserAsync(): Runs any parser, returning a Promise of the result.
This change is backward compatible. Existing code continues to work unchanged as all parsers default to sync mode.
import type { ValueParser } from "@optique/core/valueparser"; import { integer } from "@optique/core/valueparser"; import { parseSync, parseAsync } from "@optique/core/parser"; const args: readonly string[] = ["42"]; // Sync parser (default) const syncParser: ValueParser<"sync", number> = integer(); // Custom async parser const asyncParser: ValueParser<"async", string> = { $mode: "async", metavar: "REMOTE", async parse(input) { const data = await fetch(input); return { success: true, value: await data.text() }; }, format: (v) => v, }; // Type-safe parsing const syncResult = parseSync(syncParser, args); // Returns directly const asyncResult = await parseAsync(asyncParser, args); // Returns PromiseFixed a shell command injection vulnerability in the shell completion script generators. The
programNameparameter in shell completion generators (bash,zsh,fish,nu,pwsh) was directly interpolated into shell scripts without validation, which could allow arbitrary command execution if an attacker-controlled program name was used.The fix adds strict validation of program names, allowing only alphanumeric characters, underscores, hyphens, and dots. Programs with names containing shell metacharacters or other special characters will now throw an error when generating completion scripts.
Added documentation warning about potential Regular Expression Denial of Service (ReDoS) attacks when using the
patternoption instring()value parser. Users providing custom patterns should be aware that maliciously crafted input can cause exponential backtracking in vulnerable patterns. The documentation recommends avoiding patterns with nested quantifiers and suggests using tools like safe-regex to validate patterns before use.Renamed
run()function torunParser()to avoid naming conflict with@optique/run'srun()function. IDE autocomplete was suggesting both functions when typingrun, causing confusion. The oldrun()export is still available but deprecated and will be removed in a future major version. [#54]Renamed
RunErrorclass toRunParserErrorfor consistency with therunParser()rename. The oldRunErrorexport is still available but deprecated and will be removed in a future major version. [#54]Added support for
optional()andwithDefault()wrappers insidemerge(). Previously,merge(optional(or(...)), object({...}))would fail when the optional parser didn't match any input. Now, parsers that succeed without consuming input (likeoptional()when nothing matches) are handled correctly, allowing the next parser in the merge to process remaining arguments. [#57]// Now works correctly const parser = merge( optional( or( object({ verbosity: map(multiple(flag("-v")), v => v.length) }), object({ verbosity: map(flag("-q"), () => 0) }), ), ), object({ file: argument(string()) }), ); // myapp file.txt → { file: "file.txt" } // myapp -v -v file.txt → { verbosity: 2, file: "file.txt" }Added
hiddenoption to all primitive parsers (option(),flag(),argument(),command(),passThrough()). Whenhidden: true, the parser is excluded from help text, shell completion suggestions, and “Did you mean?” error suggestions, while remaining fully functional for parsing. This is useful for deprecated options, internal debugging flags, and experimental features. [#62]// Deprecated option: still works, but not shown in help const parser = object({ output: option("-o", "--output", string()), outputLegacy: option("--out", string(), { hidden: true }), }); // Internal debugging flag const debug = flag("--trace-internal", { hidden: true });Added compile-time and runtime validation for
metavarto reject empty strings. Themetavarproperty inValueParserand related option interfaces now uses a non-empty string type (NonEmptyString), providing TypeScript errors when an empty string literal is passed. Additionally, value parser factory functions (string(),integer(),float(),choice(),url(),locale(),uuid()) now throw aTypeErrorat runtime if an emptymetavaris provided. [#63]- Added
@optique/core/nonemptymodule. - Added
NonEmptyStringtype. - Added
isNonEmptyString()type guard function. - Added
ensureNonEmptyString()assertion function.
- Added
Extended
choice()value parser to support number literals in addition to strings. This enables type-safe enumeration of numeric values like bit depths, port numbers, or other numeric options where only specific values are valid. [#64]// Bit depth choice - returns ValueParser<8 | 10 | 12> const bitDepth = choice([8, 10, 12]); // Port selection const port = choice([80, 443, 8080]);The implementation uses function overloads to provide proper type inference for both string and number choices. The
caseInsensitiveoption remains available only for string choices, enforced at the type level.- Added
ChoiceOptionsBaseinterface. - Added
ChoiceOptionsStringinterface. - Added
ChoiceOptionsNumberinterface. - Deprecated
ChoiceOptionsinterface in favor ofChoiceOptionsString.
- Added
Added
valueSet()function for formatting choice lists with locale-aware separators. This function usesIntl.ListFormatto format lists according to locale conventions with appropriate conjunctions like “and” or “or”, making it suitable for error messages inchoice()value parsers and similar contexts.import { message, valueSet } from "@optique/core/message"; const choices = ["error", "warn", "info"]; // Conjunction: "error", "warn" and "info" const msg1 = message`Expected ${valueSet(choices)}.`; // Disjunction: "error", "warn" or "info" const msg2 = message`Expected ${valueSet(choices, { type: "disjunction" })}.`; // Korean: "error", "warn" 또는 "info" const msg3 = message`${valueSet(choices, { locale: "ko", type: "disjunction" })}`;The exact formatting depends on the locale. If no locale is specified,
valueSet()uses the system default locale, which may vary across environments. For consistent formatting, specify a locale explicitly.- Added
ValueSetOptionsinterface. - Added
valueSet()function.
- Added
@optique/run
Added sync/async mode support to the
run()function. [#52, #70]New functions for explicit mode handling:
runSync(): Runs with a sync-only parser, returning the parsed value directly.runAsync(): Runs with any parser, returning aPromiseof the parsed value.
The existing
run()function continues to work unchanged for sync parsers. For async parsers, userunAsync()orawait run().import { run, runSync, runAsync } from "@optique/run"; import { object } from "@optique/core/constructs"; import { option } from "@optique/core/primitives"; import { string } from "@optique/core/valueparser"; // Example sync parser const syncParser = object({ name: option("-n", string()) }); // Example async parser (with async value parser) const asyncParser = object({ data: option("-d", { $mode: "async", metavar: "URL", async parse(input) { const res = await fetch(input); return { success: true, value: await res.text() }; }, format: (v) => v, }), }); // Sync parser (default behavior, unchanged) const syncResult = run(syncParser); // Explicit sync-only (compile error if parser is async) const syncOnlyResult = runSync(syncParser); // Async parser support const asyncResult = await runAsync(asyncParser);Added
mustNotExistoption to thepath()value parser. When set totrue, the parser rejects paths that already exist on the filesystem, which is useful for output files to prevent accidental overwrites. [#56]// Output file must not exist const outputFile = path({ mustNotExist: true }); // With extension validation const reportFile = path({ mustNotExist: true, extensions: [".json", ".csv"] });Refactored
PathOptionsinto a discriminated union type to enforce mutual exclusivity betweenmustExistandmustNotExistoptions at compile time. Setting both totruenow produces a TypeScript error instead of undefined runtime behavior. [#56]Added
pathAlreadyExistserror customization option toPathErrorOptionsfor thepath()value parser. This allows custom error messages when a path already exists while using themustNotExistoption. [#56]The
path()value parser now validates thatmetavaris non-empty, throwing aTypeErrorif an empty string is provided. [#63]
@optique/temporal
- All temporal value parsers now validate that
metavaris non-empty, throwing aTypeErrorif an empty string is provided. [#63]
@optique/git
The @optique/git package was introduced in this release, providing async value parsers for validating Git references (branches, tags, commits, remotes) using isomorphic-git. [#71, #72]
- Added
gitBranch()value parser for validating local branch names. - Added
gitRemoteBranch(remote)value parser for validating remote branch names on a specified remote. - Added
gitTag()value parser for validating tag names. - Added
gitRemote()value parser for validating remote names. - Added
gitCommit()value parser for validating commit SHAs (full or shortened). - Added
gitRef()value parser for validating any Git reference (branches, tags, or commits). - Added
createGitParsers()factory function for creating parsers with shared configuration. - Added
GitParsersinterface for the factory return type. - Added
GitParserOptionsinterface for parser configuration. - Added automatic shell completion suggestions for branches, tags, remotes, and commits.
Version 0.8.16
Released on February 19, 2026.
@optique/core
- Fixed meta commands (
help,version,completion,completions) disappearing from the subcommand list in help output when the parser uses awithDefault(or(...))construct. The root cause was thatgetDocPage()used ado...whileloop, which ran the parser at least once even with an empty argument buffer. BecausewithDefault(or(...))allows the inner parser to succeed without consuming any tokens, thelongestMatchcombinator would record the user's parser as “selected” and subsequently return only that parser's doc fragments—silently dropping the meta command entries. The loop is now awhileloop that skips parsing entirely when the buffer is empty. [#121]
Version 0.8.15
Released on February 18, 2026.
@optique/core
Fixed how
briefanddescriptionare displayed in subcommand help pages. [#118, #119]Each help page now shows
briefat the very top (before the Usage line) anddescriptionbelow the Usage line, consistent with how the root-level help page works.The
run()-levelbriefanddescriptionno longer bleed into a subcommand's help page. Previously, when a subcommand had nodescriptionof its own, the description supplied torun()would appear on the subcommand's help page (e.g.repro file --helpwould show “Description for repro CLI” even though that text describes the top-level program, not thefilesubcommand). Now, only the subcommand's ownbriefanddescription(if any) are shown; run-level docs are used only for the root-level help page.When
command()is wrapped withgroup(), the command'sbrief,description, andfooterare now correctly forwarded to the help page. Previouslygroup()only forwardeddescription, sobriefwas silently dropped and the run-level brief appeared instead.
Fixed contradictory “Did you mean?” suggestion when a subcommand name is provided at the wrong level. Previously, a structure like
command("file", or(add, remove))given the inputadd --helpwould reportExpected command file, but got add.and then illogically suggestDid you mean add?even thoughaddis only valid insidefile. Suggestions incommand()errors now derive fromextractLeadingCommandNames(), which limits candidates to commands that are actually valid at the current parse position, not commands nested inside other commands. The same scoping is applied when a customnotMatchedcallback receives itssuggestionsargument. [#117]Fixed
group()label leaking into a selected subcommand's own nested command list. Previously, when agroup()-wrapped command (e.g.,alias) itself contained further subcommands (e.g.,delete,set), viewingalias --helpwould show those inner commands under the outer group's label (e.g., “Additional commands:”), which was incorrect. The fix compares current command entries against the initial set of commands that the group originally labeled; the label is now applied only when the visible commands are the group's own top-level commands, not commands from a deeper level. [#116]
Version 0.8.14
Released on February 18, 2026.
@optique/core
Fixed
group()incorrectly applying its label to subcommand flags in help output. Whengroup()wrapscommand()parsers (viaor()), the group label is now only shown for the command list at the top level, not for the inner flags/options after a command is selected. [#114]Fixed
merge()not propagatingbrief,description, andfooterfrom inner parsers in help output. When using themerge(or(...commands), globalOptions)pattern, subcommand help (e.g.,myapp subcommand --help) now correctly displays the command's description. Previously, these fields were silently discarded bymerge().
Version 0.8.13
Released on February 17, 2026.
@optique/core
- Fixed usage string collapsing to a single line when
completionwithname: "both"created a nested exclusive term (e.g.,(completion | completions)) inside an outer exclusive.normalizeUsageTerm()now distributes an exclusive that appears as the first term of a branch across the remaining terms, so that[exclusive(A, B), C]is flattened into separate branches[A, C]and[B, C]. This allowsformatUsage()withexpandCommands: trueto properly render each alternative on its own line.
Version 0.8.12
Released on February 14, 2026.
@optique/core
- Fixed the
completioncommand appearing before user-defined commands in the usage line shown on parse errors. When usingcompletionin command mode withor()-combined commands, the error output now lists user commands first and meta-commands (version, completion, help) after them. [#107]
Version 0.8.11
Released on February 13, 2026.
@optique/core
Fixed contradictory suggestion messages for subcommand-only options at the root command level. Previously, when an option that belongs to a subcommand was entered before specifying the subcommand (for example,
mycli --fooflag 123where--fooflagbelongs tomycli foo), the parser could reportUnexpected option or subcommandand still suggest the same option. Suggestions now only include options and commands that are valid at the current parse position. [#98]Fixed
--completionand--completionswithout a shell value in option mode reporting a generic parse error. Previously, inputs likemycli --completioncould fall through to normal argument parsing and showUnexpected option or subcommand, which was misleading. These now produce the dedicated completion error for a missing shell name. [#100]
Version 0.8.10
Released on February 12, 2026.
@optique/core
- Fixed
mycli subcommand --helpdisplaying thebriefanddescriptionpassed torun()instead of those passed tocommand(). Now, when showing help for a specific subcommand, the subcommand's ownbrief,description, andfootertake priority over therun()-level values. Therun()-level values are used as fallback only when the subcommand does not define its own. [#95]
Version 0.8.9
Released on February 12, 2026.
@optique/core
- Fixed
run()showing top-level help instead of an error when--helpis used with an invalid subcommand (e.g.,mycli nonexistent --help). Previously, the help handler did not validate whether the commands extracted before--helpwere actually recognized by the parser, so it silently fell back to displaying root-level help. Now, the commands are validated against the parser, and an appropriate error message with usage information is shown instead. [#97] - Fixed nested subcommand help showing sibling subcommand usage lines. Previously, when using
or(topLevel, command("nested", or(foo, bar))), runningmycli nested foo --helpwould incorrectly display usage lines for bothfooandbarsubcommands. Now, only the selected subcommand's usage is shown. [#96]
Version 0.8.8
Released on January 19, 2026.
@optique/core
- Fixed
command()failing to parse inner parser when buffer is empty after command match. Previously, when usingcommand()with an inner parser that can succeed with zero tokens (likelongestMatch()orobject()with all optional fields), parsing["dev"]would fail with “No matching option, command, or argument found” even though the inner parser should succeed. Now, thecomplete()method first gives the inner parser a chance to run with an empty buffer before completing. [#81]
Version 0.8.7
Released on January 5, 2026.
@optique/core
- Fixed
multiple()parser suggesting already-selected values in shell completion. Previously, when usingmultiple(argument(choice(...)))or similar patterns, values that had already been selected would continue to appear in completion suggestions. Now, thesuggest()method filters out values that have already been parsed, providing a cleaner completion experience. [#73]
Version 0.8.6
Released on January 1, 2026.
@optique/core
- Fixed
object()parser ignoring symbol keys. Previously, when using symbol keys in the parser definition (e.g.,object({ [sym]: option(...) })), the symbol-keyed parsers were silently ignored becauseObject.entries()andfor...inloops do not enumerate symbol properties. Now, the parser correctly handles both string and symbol keys by usingReflect.ownKeys().
Version 0.8.5
Released on December 30, 2025.
@optique/core
Fixed subcommand help display when using
merge()withor(). Previously, when combining parsers usingmerge(..., or(...)), the help output would fail to display subcommand-specific options becausemerge()did not correctly propagate the runtime state of parsers with undefined initial state. Now, the state is correctly resolved, ensuring that subcommand help is generated correctly. [#69]Fixed subcommand help displaying incorrect usage line when using
longestMatch()with nested subcommands. Previously, when using patterns likelongestMatch(merge(globalOptions, or(sub1, sub2)), helpCommand), the usage line inhelp COMMANDoutput would show all subcommand alternatives instead of only the selected subcommand's usage. Now, the usage line correctly displays only the relevant subcommand.
Version 0.8.4
Released on December 30, 2025.
@optique/core
Added default descriptions for built-in
helpcommand argument and completion option arguments. Previously, these arguments showed no help text. Now they display helpful default descriptions in help output.Fixed subcommand help not displaying option descriptions in some edge cases. When using
help COMMANDorCOMMAND --help, the help output now correctly passes the inner parser's initial state for documentation generation when the command is matched but the inner parser hasn't started yet. Previously, it passedunavailablestate which could cause issues with parsers that depend on state for documentation. [#68]
@optique/logtape
- Added default descriptions for
verbosity(),debug(), andlogOutput()parsers. Previously, these parsers showed no help text unless users explicitly provided adescriptionoption. Now they display helpful default descriptions in help output.
Version 0.8.3
Released on December 30, 2025.
@optique/core
- Fixed
merge()breaking option parsing inside subcommands when merged withor(). Previously, when usingmerge(globalOptions, or(sub1, sub2)), options inside the subcommands would fail to parse after the subcommand was selected. For example,sub1 -o foowould fail with “No matching option or argument found” even though-owas a valid option forsub1. [#67]
Version 0.8.2
Released on December 21, 2025.
@optique/core
- Fixed
withDefault()widening literal types in its default value parameter. Previously, passing a literal type as the default value would cause type widening (e.g.,"auto"→string,42→number). Now literal types are correctly preserved. For example,withDefault(option("--format", choice(["auto", "text"])), "auto")now correctly infers"auto" | "text"instead ofstring. [#58]
Version 0.8.1
Released on December 16, 2025.
@optique/core
Fixed shell completion scripts using singular form (
completion,--completion) even whencompletion.nameis set to"plural". Now, when the name is"plural", the generated scripts correctly usecompletionsor--completionsinstead of the singular form. [#53]Fixed shell completion suggestions including positional argument values when completing an option that expects a value. For example, when completing
--remote ""in a parser with both--remoteoption and positional tag arguments, only the remote values are now suggested, not the tag values. [#55]
Version 0.8.0
Released on December 9, 2025.
@optique/core
Added
conditional()combinator for discriminated union patterns where certain options depend on a discriminator option value. This enables context-dependent parsing where different sets of options become valid based on the discriminator selection. [#49]const parser = conditional( option("--reporter", choice(["console", "junit", "html"])), { console: object({}), junit: object({ outputFile: option("--output-file", string()) }), html: object({ outputFile: option("--output-file", string()) }), } ); // Result type: ["console", {}] | ["junit", { outputFile: string }] | ...Key features:
- Explicit discriminator option determines which branch is selected
- Tuple result
[discriminator, branchValue]for clear type narrowing - Optional default branch for when discriminator is not provided
- Clear error messages indicating which options are required for each discriminator value
Added
literaltype toUsageTermfor representing fixed string values in usage descriptions. Unlike metavars (which are placeholders displayed with special formatting), literals are displayed as plain text. This is used internally byconditional()to show actual discriminator values in help text (e.g.,--reporter consoleinstead of--reporter TYPE). [#49]Added
passThrough()parser for building wrapper CLI tools that need to forward unrecognized options to an underlying tool. This parser captures unknown options without validation errors, enabling legitimate wrapper/proxy patterns. [#35]const parser = object({ debug: option("--debug"), extra: passThrough(), }); // mycli --debug --foo=bar --baz=qux // → { debug: true, extra: ["--foo=bar", "--baz=qux"] }Key features:
- Three capture formats:
"equalsOnly"(default, safest),"nextToken"(captures--opt valpairs), and"greedy"(captures all remaining tokens) - Lowest priority (−10) ensures explicit parsers always match first
- Respects
--options terminator in"equalsOnly"and"nextToken"modes - Works seamlessly with
object(), subcommands, and other combinators
- Three capture formats:
Added
passthroughtype toUsageTermfor representing pass-through options in usage descriptions. Displayed as[...]in help text.Fixed
integer()value parser to accept negative integers when usingtype: "number". Previously, the regex pattern only matched non-negative integers (/^\d+$/), causing inputs like-42to be rejected as invalid. The pattern has been updated to/^-?\d+$/to correctly handle negative values. Note thattype: "bigint"already accepted negative integers viaBigInt()conversion, so this change brings consistency between the two types.
@optique/logtape
The @optique/logtape package was introduced in this release, providing integration with the LogTape logging library. This package enables configuring LogTape logging through command-line arguments with various parsing strategies.
Added
logLevel()value parser for parsing log level strings ("trace","debug","info","warning","error","fatal") into LogTape'sLogLeveltype. Parsing is case-insensitive and provides shell completion suggestions.Added
verbosity()parser for accumulating-vflags to determine log level. Each additional-vflag increases verbosity: no flags →"warning",-v→"info",-vv→"debug",-vvv→"trace".Added
debug()parser for simple--debug/-dflag that toggles between normal level ("info") and debug level ("debug").Added
logOutput()parser for log output destination. Accepts-for console output or a file path for file output, following CLI conventions.Added
loggingOptions()preset that combines log level and log output options into a singlegroup(). Uses a discriminated union configuration to enforce mutually exclusive level selection methods ("option","verbosity", or"debug").Added
createLoggingConfig()helper function that converts parsed logging options into a LogTapeConfigobject for use withconfigure().Added
createConsoleSink()function for creating console sinks with configurable stream selection (stdout/stderr) and level-based stream resolution.Added
createSink()async function that creates LogTape sinks fromLogOutputvalues. File output requires the optional@logtape/filepackage.
Version 0.7.18
Released on February 19, 2026.
@optique/core
- Fixed meta commands (
help,version,completion,completions) disappearing from the subcommand list in help output when the parser uses awithDefault(or(...))construct. The root cause was thatgetDocPage()used ado...whileloop, which ran the parser at least once even with an empty argument buffer. BecausewithDefault(or(...))allows the inner parser to succeed without consuming any tokens, thelongestMatchcombinator would record the user's parser as “selected” and subsequently return only that parser's doc fragments—silently dropping the meta command entries. The loop is now awhileloop that skips parsing entirely when the buffer is empty. [#121]
Version 0.7.17
Released on February 18, 2026.
@optique/core
Fixed how
briefanddescriptionare displayed in subcommand help pages. [#118, #119]Each help page now shows
briefat the very top (before the Usage line) anddescriptionbelow the Usage line, consistent with how the root-level help page works.The
run()-levelbriefanddescriptionno longer bleed into a subcommand's help page. Previously, when a subcommand had nodescriptionof its own, the description supplied torun()would appear on the subcommand's help page (e.g.repro file --helpwould show “Description for repro CLI” even though that text describes the top-level program, not thefilesubcommand). Now, only the subcommand's ownbriefanddescription(if any) are shown; run-level docs are used only for the root-level help page.When
command()is wrapped withgroup(), the command'sbrief,description, andfooterare now correctly forwarded to the help page. Previouslygroup()only forwardeddescription, sobriefwas silently dropped and the run-level brief appeared instead.
Fixed contradictory “Did you mean?” suggestion when a subcommand name is provided at the wrong level. Previously, a structure like
command("file", or(add, remove))given the inputadd --helpwould reportExpected command file, but got add.and then illogically suggestDid you mean add?—even thoughaddis only valid insidefile. Suggestions incommand()errors now derive fromextractLeadingCommandNames(), which limits candidates to commands that are actually valid at the current parse position, not commands nested inside other commands. The same scoping is applied when a customnotMatchedcallback receives itssuggestionsargument. [#117]Fixed
group()label leaking into a selected subcommand's own nested command list. Previously, when agroup()-wrapped command (e.g.,alias) itself contained further subcommands (e.g.,delete,set), viewingalias --helpwould show those inner commands under the outer group's label (e.g., “Additional commands:”), which was incorrect. The fix compares current command entries against the initial set of commands that the group originally labeled; the label is now applied only when the visible commands are the group's own top-level commands, not commands from a deeper level. [#116]
Version 0.7.16
Released on February 18, 2026.
@optique/core
Fixed
group()incorrectly applying its label to subcommand flags in help output. Whengroup()wrapscommand()parsers (viaor()), the group label is now only shown for the command list at the top level, not for the inner flags/options after a command is selected. [#114]Fixed
merge()not propagatingbrief,description, andfooterfrom inner parsers in help output. When using themerge(or(...commands), globalOptions)pattern, subcommand help (e.g.,myapp subcommand --help) now correctly displays the command's description. Previously, these fields were silently discarded bymerge().
Version 0.7.15
Released on February 17, 2026.
@optique/core
- Fixed usage string collapsing to a single line when
completionwithname: "both"created a nested exclusive term (e.g.,(completion | completions)) inside an outer exclusive.normalizeUsageTerm()now distributes an exclusive that appears as the first term of a branch across the remaining terms, so that[exclusive(A, B), C]is flattened into separate branches[A, C]and[B, C]. This allowsformatUsage()withexpandCommands: trueto properly render each alternative on its own line.
Version 0.7.14
Released on February 14, 2026.
@optique/core
- Fixed the
completioncommand appearing before user-defined commands in the usage line shown on parse errors. When usingcompletionin command mode withor()-combined commands, the error output now lists user commands first and meta-commands (version, completion, help) after them. [#107]
Version 0.7.13
Released on February 13, 2026.
@optique/core
Fixed contradictory suggestion messages for subcommand-only options at the root command level. Previously, when an option that belongs to a subcommand was entered before specifying the subcommand (for example,
mycli --fooflag 123where--fooflagbelongs tomycli foo), the parser could reportUnexpected option or subcommandand still suggest the same option. Suggestions now only include options and commands that are valid at the current parse position. [#98]Fixed
--completionand--completionswithout a shell value in option mode reporting a generic parse error. Previously, inputs likemycli --completioncould fall through to normal argument parsing and showUnexpected option or subcommand, which was misleading. These now produce the dedicated completion error for a missing shell name. [#100]
Version 0.7.12
Released on February 12, 2026.
@optique/core
- Fixed
mycli subcommand --helpdisplaying thebriefanddescriptionpassed torun()instead of those passed tocommand(). Now, when showing help for a specific subcommand, the subcommand's ownbrief,description, andfootertake priority over therun()-level values. Therun()-level values are used as fallback only when the subcommand does not define its own. [#95]
Version 0.7.11
Released on February 12, 2026.
@optique/core
- Fixed
run()showing top-level help instead of an error when--helpis used with an invalid subcommand (e.g.,mycli nonexistent --help). Previously, the help handler did not validate whether the commands extracted before--helpwere actually recognized by the parser, so it silently fell back to displaying root-level help. Now, the commands are validated against the parser, and an appropriate error message with usage information is shown instead. [#97]
Version 0.7.10
Released on January 19, 2026.
@optique/core
- Fixed
command()failing to parse inner parser when buffer is empty after command match. Previously, when usingcommand()with an inner parser that can succeed with zero tokens (likelongestMatch()orobject()with all optional fields), parsing["dev"]would fail with “No matching option, command, or argument found” even though the inner parser should succeed. Now, thecomplete()method first gives the inner parser a chance to run with an empty buffer before completing. [#81]
Version 0.7.9
Released on January 5, 2026.
@optique/core
- Fixed
multiple()parser suggesting already-selected values in shell completion. Previously, when usingmultiple(argument(choice(...)))or similar patterns, values that had already been selected would continue to appear in completion suggestions. Now, thesuggest()method filters out values that have already been parsed, providing a cleaner completion experience. [#73]
Version 0.7.8
Released on January 1, 2026.
@optique/core
- Fixed
object()parser ignoring symbol keys. Previously, when using symbol keys in the parser definition (e.g.,object({ [sym]: option(...) })), the symbol-keyed parsers were silently ignored becauseObject.entries()andfor...inloops do not enumerate symbol properties. Now, the parser correctly handles both string and symbol keys by usingReflect.ownKeys().
Version 0.7.7
Released on December 30, 2025.
@optique/core
Fixed subcommand help display when using
merge()withor(). Previously, when combining parsers usingmerge(..., or(...)), the help output would fail to display subcommand-specific options becausemerge()did not correctly propagate the runtime state of parsers with undefined initial state. Now, the state is correctly resolved, ensuring that subcommand help is generated correctly. [#69]Fixed subcommand help displaying incorrect usage line when using
longestMatch()with nested subcommands. Previously, when using patterns likelongestMatch(merge(globalOptions, or(sub1, sub2)), helpCommand), the usage line inhelp COMMANDoutput would show all subcommand alternatives instead of only the selected subcommand's usage. Now, the usage line correctly displays only the relevant subcommand.
Version 0.7.6
Released on December 30, 2025.
@optique/core
Added default descriptions for built-in
helpcommand argument and completion option arguments. Previously, these arguments showed no help text. Now they display helpful default descriptions in help output.Fixed subcommand help not displaying option descriptions in some edge cases. When using
help COMMANDorCOMMAND --help, the help output now correctly passes the inner parser's initial state for documentation generation when the command is matched but the inner parser hasn't started yet. Previously, it passedunavailablestate which could cause issues with parsers that depend on state for documentation. [#68]
Version 0.7.5
Released on December 30, 2025.
@optique/core
- Fixed
merge()breaking option parsing inside subcommands when merged withor(). Previously, when usingmerge(globalOptions, or(sub1, sub2)), options inside the subcommands would fail to parse after the subcommand was selected. For example,sub1 -o foowould fail with “No matching option or argument found” even though-owas a valid option forsub1. [#67]
Version 0.7.4
Released on December 21, 2025.
@optique/core
- Fixed
withDefault()widening literal types in its default value parameter. Previously, passing a literal type as the default value would cause type widening (e.g.,"auto"→string,42→number). Now literal types are correctly preserved. For example,withDefault(option("--format", choice(["auto", "text"])), "auto")now correctly infers"auto" | "text"instead ofstring. [#58]
Version 0.7.3
Released on December 16, 2025.
@optique/core
Fixed shell completion scripts using singular form (
completion,--completion) even whencompletion.nameis set to"plural". Now, when the name is"plural", the generated scripts correctly usecompletionsor--completionsinstead of the singular form. [#53]Fixed shell completion suggestions including positional argument values when completing an option that expects a value. For example, when completing
--remote ""in a parser with both--remoteoption and positional tag arguments, only the remote values are now suggested, not the tag values. [#55]
Version 0.7.2
Released on December 2, 2025.
@optique/core
- Fixed
optional()andwithDefault()returning an error instead of the expected value when the input contains only the options terminator"--". For example,parse(withDefault(option("--name", string()), "Bob"), ["--"])now correctly returns"Bob"instead of failing with “Missing option--name.” [#50]
Version 0.7.1
Released on December 2, 2025.
@optique/core
- Fixed
optional()andwithDefault()returning an error instead of the expected value when used as a standalone parser with empty input. For example,parse(withDefault(option("--name", string()), "Bob"), [])now correctly returns"Bob"instead of failing with “Expected an option, but got end of input.” [#48]
Version 0.7.0
Released on November 25, 2025.
@optique/core
Added duplicate option name detection to parser combinators. The
object(),tuple(),merge(), andgroup()combinators now validate at parse time that option names are unique across their child parsers. When duplicate option names are detected, parsing fails with a clear error message indicating which option name is duplicated and in which fields or positions. [#37]- Added
ObjectOptions.allowDuplicatesfield to opt out of validation. - Added
TupleOptionsinterface withallowDuplicatesfield. - Added
MergeOptionsinterface withallowDuplicatesfield. - Added
tuple()overloads accepting optionalTupleOptionsparameter. - Added
extractOptionNames()function to@optique/core/usagemodule.
- Added
Changed parsing behavior:
object(),tuple(),merge(), andgroup()now reject parsers with duplicate option names by default. Previously, duplicate option names would result in ambiguous parsing where the first matching parser consumed the option and subsequent parsers silently received default values. This breaking change prevents unintentional bugs from option name conflicts.To restore previous behavior, set
allowDuplicates: truein options. [#37]The
or()combinator continues to allow duplicate option names across branches since alternatives are mutually exclusive. [#37]Added “Did you mean?” suggestion system for typos in option names and command names. When users make typos in command-line arguments, Optique now automatically suggests similar valid options to help them correct the mistake:
const parser = object({ verbose: option("-v", "--verbose"), version: option("--version"), }); // User types: --verbos (typo) const result = parse(parser, ["--verbos"]); // Error: No matched option for `--verbos`. // Did you mean `--verbose`?The suggestion system uses Levenshtein distance to find similar names, suggesting up to 3 alternatives when the edit distance is at most 3 characters and the distance ratio is at most 0.5. Comparison is case-insensitive by default. Suggestions work automatically for both option names and subcommand names in all error contexts (
option(),flag(),command(),object(),or(), andlongestMatch()parsers). This feature is implemented internally and requires no user configuration. [#38]Added customization support for “Did you mean?” suggestion messages. All parsers that generate suggestion messages now support customizing how suggestions are formatted or disabling them entirely through the
errorsoption:// Custom suggestion format for option/flag parsers const portOption = option("--port", integer(), { errors: { noMatch: (invalidOption, suggestions) => suggestions.length > 0 ? message`Unknown option ${invalidOption}. Try: ${values(suggestions)}` : message`Unknown option ${invalidOption}.` } }); // Custom suggestion format for command parser const addCmd = command("add", object({}), { errors: { notMatched: (expected, actual, suggestions) => suggestions && suggestions.length > 0 ? message`Unknown command ${actual}. Similar: ${values(suggestions)}` : message`Expected ${expected} command.` } }); // Custom suggestion formatter for combinators const config = object({ host: option("--host", string()), port: option("--port", integer()) }, { errors: { suggestions: (suggestions) => suggestions.length > 0 ? message`Available options: ${values(suggestions)}` : [] } });The customization system allows complete control over suggestion formatting while maintaining backward compatibility. When custom error messages are provided, they receive the array of suggestions and can format, filter, or disable them as needed. This enables applications to provide context-specific error messages that match their CLI's style and user expectations. [#38]
- Extended
OptionErrorOptionsinterface withnoMatchfield. - Extended
FlagErrorOptionsinterface withnoMatchfield. - Extended
CommandErrorOptions.notMatchedsignature to include optionalsuggestionsparameter. - Extended
OrErrorOptionsinterface withsuggestionsfield. - Extended
LongestMatchErrorOptionsinterface withsuggestionsfield. - Extended
ObjectErrorOptionsinterface withsuggestionsfield.
- Extended
Improved
formatMessage()line break handling to distinguish between soft breaks (word wrap) and hard breaks (paragraph separation):Single newlines (
\n) are now treated as soft breaks, converted to spaces for natural word wrapping. This allows long error messages to be written across multiple lines in source code while rendering as continuous text.Double or more consecutive newlines (
\n\n+) are treated as hard breaks, creating actual paragraph separations in the output.
This change improves the readability of multi-part error messages, such as those with “Did you mean?” suggestions, by ensuring proper spacing between the base error and suggestions:
Error: Unexpected option or subcommand: comit. Did you mean commit?Previously, single newlines in
text()terms would be silently dropped during word wrapping, causing messages to run together without spacing.Improved error messages for
or(),longestMatch(), andobject()combinators to provide context-aware feedback based on expected input types. Error messages now accurately reflect what's missing (options, commands, or arguments) instead of showing generic “No matching option or command found” for all cases. [#45]// When only arguments are expected const parser1 = or(argument(string()), argument(integer())); // Error: Missing required argument. // When only commands are expected const parser2 = or(command("add", ...), command("remove", ...)); // Error: No matching command found. // When both options and arguments are expected const parser3 = object({ port: option("--port", integer()), file: argument(string()), }); // Error: No matching option or argument found.Added function-form support for
errors.noMatchoption inor(),longestMatch(), andobject()combinators. Error messages can now be dynamically generated based on context, enabling use cases like internationalization:const parser = or( command("add", constant("add")), command("remove", constant("remove")), { errors: { noMatch: ({ hasOptions, hasCommands, hasArguments }) => { if (hasCommands && !hasOptions && !hasArguments) { return message`일치하는 명령을 찾을 수 없습니다.`; // Korean } return message`잘못된 입력입니다.`; } } } );The callback receives a
NoMatchContextobject with Boolean flags (hasOptions,hasCommands,hasArguments) indicating what types of inputs are expected. This allows generating language-specific or context-specific error messages while maintaining backward compatibility with static message form. [#45]- Added
NoMatchContextinterface. - Extended
OrErrorOptions.noMatchto acceptMessage | ((context: NoMatchContext) => Message). - Extended
LongestMatchErrorOptions.noMatchto acceptMessage | ((context: NoMatchContext) => Message). - Extended
ObjectErrorOptions.endOfInputto acceptMessage | ((context: NoMatchContext) => Message). - Added
extractArgumentMetavars()function to@optique/core/usagemodule.
- Added
Added configuration for shell completion naming conventions. The
run()function now supports both singular (completion,--completion) and plural (completions,--completions) naming styles. By default, both forms are accepted ("both"), but this can be restricted to just one style using the newnameoption in the completion configuration:run(parser, { completion: { // Use "completions" and "--completions" only name: "plural", } });This allows CLI tools to match their preferred style guide or exist alongside other commands. The help text examples automatically reflect the configured naming style. [#42]
- Added
nameoption toRunOptions.completionconfiguration ("singular","plural", or"both").
- Added
@optique/valibot
The @optique/valibot package was introduced in this release, providing integration with the Valibot validation library. This package enables using Valibot schemas as value parsers, bringing Valibot's powerful validation capabilities with a significantly smaller bundle size (~10KB vs Zod's ~52KB) to command-line argument parsing. Supports Valibot version 0.42.0 and above. [#40]
- Added
valibot()value parser for validating command-line arguments using Valibot schemas. - Added
ValibotParserOptionsinterface for configuring metavar and custom error messages. - Added automatic metavar inference from Valibot schema types (
STRING,EMAIL,NUMBER,INTEGER,CHOICE, etc.).
@optique/zod
The @optique/zod package was introduced in this release, providing integration with the Zod validation library. This package enables using Zod schemas as value parsers, bringing Zod's powerful validation capabilities to command-line argument parsing. Supports both Zod v3.25.0+ and v4.0.0+. [#39]
- Added
zod()value parser for validating command-line arguments using Zod schemas. - Added
ZodParserOptionsinterface for configuring metavar and custom error messages.
Version 0.6.11
Released on January 5, 2026.
@optique/core
- Fixed
multiple()parser suggesting already-selected values in shell completion. Previously, when usingmultiple(argument(choice(...)))or similar patterns, values that had already been selected would continue to appear in completion suggestions. Now, thesuggest()method filters out values that have already been parsed, providing a cleaner completion experience. [#73]
Version 0.6.10
Released on January 1, 2026.
@optique/core
- Fixed
object()parser ignoring symbol keys. Previously, when using symbol keys in the parser definition (e.g.,object({ [sym]: option(...) })), the symbol-keyed parsers were silently ignored becauseObject.entries()andfor...inloops do not enumerate symbol properties. Now, the parser correctly handles both string and symbol keys by usingReflect.ownKeys().
Version 0.6.9
Released on December 30, 2025.
@optique/core
Fixed subcommand help display when using
merge()withor(). Previously, when combining parsers usingmerge(..., or(...)), the help output would fail to display subcommand-specific options becausemerge()did not correctly propagate the runtime state of parsers with undefined initial state. Now, the state is correctly resolved, ensuring that subcommand help is generated correctly. [#69]Fixed subcommand help displaying incorrect usage line when using
longestMatch()with nested subcommands. Previously, when using patterns likelongestMatch(merge(globalOptions, or(sub1, sub2)), helpCommand), the usage line inhelp COMMANDoutput would show all subcommand alternatives instead of only the selected subcommand's usage. Now, the usage line correctly displays only the relevant subcommand.
Version 0.6.8
Released on December 30, 2025.
@optique/core
Added default descriptions for built-in
helpcommand argument and completion option arguments. Previously, these arguments showed no help text. Now they display helpful default descriptions in help output.Fixed subcommand help not displaying option descriptions in some edge cases. When using
help COMMANDorCOMMAND --help, the help output now correctly passes the inner parser's initial state for documentation generation when the command is matched but the inner parser hasn't started yet. Previously, it passedunavailablestate which could cause issues with parsers that depend on state for documentation. [#68]
Version 0.6.7
Released on December 30, 2025.
@optique/core
- Fixed
merge()breaking option parsing inside subcommands when merged withor(). Previously, when usingmerge(globalOptions, or(sub1, sub2)), options inside the subcommands would fail to parse after the subcommand was selected. For example,sub1 -o foowould fail with “No matching option or argument found” even though-owas a valid option forsub1. [#67]
Version 0.6.6
Released on December 21, 2025.
@optique/core
- Fixed
withDefault()widening literal types in its default value parameter. Previously, passing a literal type as the default value would cause type widening (e.g.,"auto"→string,42→number). Now literal types are correctly preserved. For example,withDefault(option("--format", choice(["auto", "text"])), "auto")now correctly infers"auto" | "text"instead ofstring. [#58]
Version 0.6.5
Released on December 2, 2025.
@optique/core
- Fixed
optional()andwithDefault()returning an error instead of the expected value when the input contains only the options terminator"--". For example,parse(withDefault(option("--name", string()), "Bob"), ["--"])now correctly returns"Bob"instead of failing with “Missing option--name.” [#50]
Version 0.6.4
Rleased on December 2, 2025.
@optique/core
- Fixed
optional()andwithDefault()returning an error instead of the expected value when used as a standalone parser with empty input. For example,parse(withDefault(option("--name", string()), "Bob"), [])now correctly returns"Bob"instead of failing with “Expected an option, but got end of input.” [#48]
Version 0.6.3
Released on November 25, 2025.
@optique/core
- Fixed shell completion scripts using
completionsubcommand even whencompletion.modeis set to"option". Now, when the mode is"option", the generated scripts correctly use--completioninstead ofcompletion. [#41]
Version 0.6.2
Released on October 27, 2025.
@optique/core
- Fixed incorrect CommonJS type export paths in
package.json. The"require"type paths for submodules were incorrectly pointing to"*.cts"files instead of"*.d.cts"files, causing type resolution issues when importing submodules in CommonJS environments. [#36]
@optique/run
- Fixed incorrect CommonJS type export paths in
package.json. The"require"type paths for submodules were incorrectly pointing to"*.cts"files instead of"*.d.cts"files, causing type resolution issues when importing submodules in CommonJS environments. [#36]
Version 0.6.1
Released on October 8, 2025.
@optique/run
- Fixed inconsistent error message coloring in Bun runtime by replacing reliance on default
console.error()andconsole.log()with directprocess.stderr.write()andprocess.stdout.write()calls. Previously, Bun'sconsole.error()would add red coloring by default, which interfered with ANSI formatting in error messages. This change ensures consistent output formatting across all JavaScript runtimes (Node.js, Bun, and Deno).
Version 0.6.0
Released on October 2, 2025.
@optique/core
Added shell completion support for Bash, zsh, fish, PowerShell, and Nushell. Optique now provides built-in completion functionality that integrates seamlessly with the existing parser architecture. This allows CLI applications to offer intelligent suggestions for commands, options, and arguments without requiring additional configuration. [#5, #30, #31, #33]
- Added
Suggestiontype. - Added
Parser.suggest()method. - Added
ValueParser.suggest()method. - Added
suggest()function. - Added
@optique/core/completionmodule. - Added
ShellCompletioninterface. - Added
bash,zsh,fish,pwsh, andnushell completion generators. - Added
RunOptions.completionoption.
- Added
Added
briefandfooteroptions tocommand()parser for improved documentation control. Thebriefoption provides a short description for command listings (e.g.,myapp help), whiledescriptionis used for detailed help (e.g.,myapp help subcommandormyapp subcommand --help). Thefooteroption allows adding examples or additional information at the bottom of command-specific help text.- Added
CommandOptions.brieffield. - Added
CommandOptions.footerfield. - Added
DocFragments.footerfield.
- Added
Added
commandLinemessage term type for formatting command-line examples in help text and error messages. This allows command-line snippets to be displayed with distinct styling (cyan color in terminals) to make examples more visually clear.- Added
commandLine()function for creating command-line message terms. - Updated
MessageTermtype to includecommandLinevariant. - Updated
formatMessage()to support styling command-line examples.
- Added
@optique/run
Added completion integration to the
run()function. Applications can now enable shell completion by setting thecompletionoption, which automatically handles completion script generation and runtime completion requests. [#5]- Added
RunOptions.completionoption.
- Added
Version 0.5.3
Released on October 27, 2025.
@optique/core
- Fixed incorrect CommonJS type export paths in
package.json. The"require"type paths for submodules were incorrectly pointing to"*.cts"files instead of"*.d.cts"files, causing type resolution issues when importing submodules in CommonJS environments. [#36]
@optique/run
- Fixed incorrect CommonJS type export paths in
package.json. The"require"type paths for submodules were incorrectly pointing to"*.cts"files instead of"*.d.cts"files, causing type resolution issues when importing submodules in CommonJS environments. [#36]
Version 0.5.2
Released on October 8, 2025.
@optique/run
- Fixed inconsistent error message coloring in Bun runtime by replacing reliance on default
console.error()andconsole.log()with directprocess.stderr.write()andprocess.stdout.write()calls. Previously, Bun'sconsole.error()would add red coloring by default, which interfered with ANSI formatting in error messages. This change ensures consistent output formatting across all JavaScript runtimes (Node.js, Bun, and Deno).
Version 0.5.1
Released on September 26, 2025.
@optique/core
- Fixed an issue where empty group sections were displayed in help output for nested subcommands. When using
group()with nested commands, help text would show empty group headers when the grouped items were no longer available in the current command context. TheformatDocPage()function now skips sections with no entries. [#29]
Version 0.5.0
Released on September 23, 2025.
@optique/core
Refactored parser modules for better code organization by splitting the large
@optique/core/parsermodule into focused, specialized modules. This improves maintainability, reduces module size, and provides clearer separation of concerns. All changes maintain full backward compatibility through re-exports.Primitive parsers: Moved primitive parsers (
constant(),option(),flag(),argument(),command()) and their related types to a dedicated@optique/core/primitivesmodule. These core building blocks are now logically separated from parser combinators.Modifying combinators: Moved parser modifier functions (
optional(),withDefault(),map(),multiple()) and their related types to a dedicated@optique/core/modifiersmodule. These higher-order functions that transform and enhance parsers are now grouped together.Construct combinators: Moved parser combinators (
object(),tuple(),or(),merge(),concat(),longestMatch(),group()) and their related types to a dedicated@optique/core/constructsmodule. These combinator functions that compose and combine parsers are now organized in a separate module.
For backward compatibility, all functions continue to be re-exported from
@optique/core/parser, so existing code will work unchanged. However, the recommended approach going forward is to import directly from the specialized modules:// Recommended: import directly from specialized modules import { option, flag, argument } from "@optique/core/primitives"; import { optional, withDefault, multiple } from "@optique/core/modifiers"; import { object, or, merge } from "@optique/core/constructs"; // Still supported: import everything from parser module (backward compatibility) import { option, flag, optional, withDefault, object, or } from "@optique/core/parser";This refactoring only affects internal code organization and does not impact the public API or runtime behavior of any parsers.
Added automatic error handling for
withDefault()default value callbacks. When a default value callback throws an error, it is now automatically caught and converted to a parser-level error. This allows users to handle validation errors (like missing environment variables) directly at the parser level instead of after callingrun(). [#18, #21]Added
WithDefaultErrorclass for structured error messages inwithDefault()callbacks. This error type accepts aMessageobject instead of just a string, enabling rich formatting with colors and structured content in error messages. Regular errors are still supported and automatically converted to plain text messages. [#18, #21]// Regular error (automatically converted to text) withDefault(option("--url", url()), () => { throw new Error("Environment variable not set"); }); // Structured error with rich formatting withDefault(option("--config", string()), () => { throw new WithDefaultError( message`Environment variable ${envVar("CONFIG_PATH")} is not set` ); });Added
envVarmessage term type for environment variable references. Environment variables are displayed with bold and underline formatting to distinguish them from other message components like option names and metavariables. TheenvVar()helper function creates environment variable terms for use in structured messages.import { envVar, message } from "@optique/core/message"; const configError = message`Environment variable ${envVar("API_URL")} is not set.`; // Displays: Environment variable API_URL is not set. (bold + underlined)Added custom help display messages for
withDefault()parsers. ThewithDefault()modifier now accepts an optional third parameter to customize how default values are displayed in help text. This allows showing descriptive text instead of actual default values, which is particularly useful for environment variables, computed defaults, or sensitive information. [#19]import { message, envVar } from "@optique/core/message"; import { withDefault } from "@optique/core/parser"; // Show custom help text instead of actual values const parser = object({ apiUrl: withDefault( option("--api-url", url()), new URL("https://api.example.com"), { message: message`Default API endpoint` } ), token: withDefault( option("--token", string()), () => process.env.API_TOKEN || "", { message: message`${envVar("API_TOKEN")}` } ) }); // Help output shows: --token STRING [API_TOKEN] // Instead of the actual token valueEnhanced
formatMessage()withresetSuffixsupport for better ANSI color handling. Thecolorsoption can now be an object with aresetSuffixproperty that maintains parent styling context after ANSI reset sequences. This fixes issues where dim styling was interrupted by inner ANSI codes in default value display. [#19]formatMessage(msg, { colors: { resetSuffix: "\x1b[2m" }, // Maintains dim after resets quotes: false });Updated
DocEntry.defaultfield type fromstringtoMessagefor rich formatting support in documentation. This change enables structured default value display with colors, environment variables, and other message components while maintaining full backward compatibility. [#19]Added comprehensive error message customization system that allows users to customize error messages for all parser types and combinators. This addresses the need for better user-facing error messages in CLI applications by enabling context-specific error text with support for both static messages and dynamic error generation functions. [#27]
Parser error options: All primitive parsers (
option(),flag(),argument(),command()) now accept anerrorsoption to customize error messages:const parser = option("--port", integer(), { errors: { missing: message`Port number is required for server operation.`, invalidValue: (error) => message`Invalid port: ${error}` } });Combinator error options: Parser combinators (
or(),longestMatch(),object(),multiple()) support error customization for various failure scenarios:const parser = or( flag("--verbose"), flag("--quiet"), { errors: { noMatch: message`Please specify either --verbose or --quiet.` } } );Value parser error customization: All value parsers now support customizable error messages through an
errorsoption in their configuration. This enables application-specific error messages that better guide users:// Core value parsers const portOption = option("--port", integer({ min: 1024, max: 65535, errors: { invalidInteger: message`Port must be a whole number.`, belowMinimum: (value, min) => message`Port ${text(value.toString())} too low. Use ${text(min.toString())} or higher.`, aboveMaximum: (value, max) => message`Port ${text(value.toString())} too high. Maximum is ${text(max.toString())}.` } })); const formatOption = option("--format", choice(["json", "yaml", "xml"], { errors: { invalidChoice: (input, choices) => message`Format ${input} not supported. Available: ${values(choices)}.` } }));Function-based error messages: Error options can be functions that receive context parameters to generate dynamic error messages:
const parser = command("deploy", argument(string()), { errors: { notMatched: (expected, actual) => message`Expected "${expected}" command, but got "${actual ?? "nothing"}".` } });Type-safe error interfaces: Each parser type has its own error options interface (
OptionErrorOptions,FlagErrorOptions,ArgumentErrorOptions,CommandErrorOptions,ObjectOptions,MultipleOptions,OrOptions,LongestMatchOptions) providing IntelliSense support and type safety.Improved default messages: Default error messages have been improved to be more user-friendly, changing generic messages like “No parser matched” to specific ones like “No matching option or command found.”
Backward compatibility: All error customization is optional and maintains full backward compatibility. Existing code continues to work unchanged while new APIs are available when needed.
@optique/run
Added comprehensive error message customization for the
path()value parser. Path validation now supports custom error messages for all validation scenarios, enabling more user-friendly error messages in file and directory operations:import { path } from "@optique/run/valueparser"; import { message, values } from "@optique/core/message"; // Configuration file with custom validation errors const configFile = option("--config", path({ mustExist: true, type: "file", extensions: [".json", ".yaml", ".yml"], errors: { pathNotFound: (input) => message`Configuration file ${input} not found.`, notAFile: message`Configuration must be a file, not a directory.`, invalidExtension: (input, extensions, actualExt) => message`Config file ${input} has wrong extension ${actualExt}. Expected: ${values(extensions)}.`, } }));The
PathOptionsinterface now includes anerrorsfield with support for customizingpathNotFound,notAFile,notADirectory,invalidExtension, andparentNotFounderror messages. All error customization options support both staticMessageobjects and dynamic functions that receive validation context parameters. [#27]
@optique/temporal
Added comprehensive error message customization for all temporal value parsers. This enables application-specific error messages for date, time, and timezone validation scenarios:
import { instant, duration, timeZone } from "@optique/temporal"; import { message } from "@optique/core/message"; // Timestamp parser with user-friendly errors const startTime = option("--start", instant({ errors: { invalidFormat: (input) => message`Start time ${input} is invalid. Use ISO 8601 format like 2023-12-25T10:30:00Z.`, } })); // Duration parser with contextual errors const timeout = option("--timeout", duration({ errors: { invalidFormat: message`Timeout must be in ISO 8601 duration format (e.g., PT30S, PT5M, PT1H).`, } })); // Timezone parser with helpful suggestions const timezone = option("--timezone", timeZone({ errors: { invalidFormat: (input) => message`Timezone ${input} is not valid. Use IANA identifiers like America/New_York or UTC.`, } }));All temporal parsers (
instant(),duration(),zonedDateTime(),plainDate(),plainTime(),plainDateTime(),plainYearMonth(),plainMonthDay(),timeZone()) now support anerrorsoption withinvalidFormaterror message customization. This maintains consistency with the error customization system across all Optique packages. [#27]
Version 0.4.4
Released on September 18, 2025.
@optique/core
- Fixed subcommand help display bug in the
run()function when using thehelpoption. Previously,cli my-cmd --helpwould incorrectly show root-level help instead of subcommand-specific help when parsers were combined withor(). Now--helpcorrectly shows help for the specific subcommand being used. [#26]
Version 0.4.3
Released on September 16, 2025.
@optique/temporal
- Fixed timezone identifier validation failure in Deno 2.5.0. The
timeZone()parser now works correctly with all supported timezones by removing explicit zero values for hour, minute, and second fields when creating validationZonedDateTimeobjects. This addresses a regression in Deno 2.5.0's Temporal API implementation that rejected zero values for time fields.
Version 0.4.2
Released on September 10, 2025.
@optique/run
- Fixed dependency resolution bug where @optique/core dependency was not properly versioned, causing installations to use outdated stable versions instead of matching development versions. This resolves type errors and message formatting issues when using dev versions. [#22]
Version 0.4.1
Released on September 8, 2025.
@optique/run
- Fixed inconsistent error message coloring across JavaScript runtimes by replacing
console.error()andconsole.log()with directprocess.stderr.write()andprocess.stdout.write()calls. Previously, runtimes like Bun would displayconsole.error()output in red, causing ANSI reset codes in formatted option names to interrupt the error coloring. This change ensures consistent output formatting across all JavaScript runtimes. [#20]
Version 0.4.0
Released on September 6, 2025.
@optique/core
Improved type inference for
tuple()parser by usingconsttype parameter. The generic type parameter now preserves tuple literal types more accurately, providing better type safety and inference when working with tuple parsers.Extended
merge()combinator to support up to 10 parsers (previously limited to 5), allowing more complex CLI configurations with larger numbers of parsers to be combined into a single object structure.Added optional label parameter to
merge()combinator for better help text organization. Similar toobject(),merge()now accepts an optional first string parameter to label the merged group in documentation:// Without label (existing usage) merge(apiParser, dbParser, serverParser) // With label (new feature) merge("Configuration", apiParser, dbParser, serverParser)This addresses the inconsistency where developers had to choose between clean code structure using
merge()or organized help output using labeledobject()calls. The label appears as a section header in help text, making it easier to group related options from multiple parsers. [#12]Added
group()combinator for organizing any parser under a labeled section in help text. This wrapper function applies a group label to any parser for documentation purposes without affecting parsing behavior:// Group mutually exclusive options const outputFormat = group( "Output Format", or( map(flag("--json"), () => "json"), map(flag("--yaml"), () => "yaml"), map(flag("--xml"), () => "xml"), ), );Unlike
merge()andobject()which have built-in label support,group()can be used with any parser type (or(),flag(),multiple(), etc.) that doesn't natively support labeling. This enables clean code organization while maintaining well-structured help text. [#12]Improved type safety for
merge()combinator by enforcing stricter parameter constraints. The function now rejects parsers that return non-object values (arrays, primitives, etc.) at compile time, producing clear “No overload matches this call” errors instead of allowing invalid combinations that would fail at runtime:// These now produce compile-time errors (previously allowed): merge( object({ port: option("--port", integer()) }), // ✅ Returns object multiple(argument(string())), // ❌ Returns array ); merge( object({ verbose: flag("--verbose") }), // ✅ Returns object flag("--debug"), // ❌ Returns boolean );This prevents a class of runtime errors where
merge()would receive incompatible parser types. Existing code usingmerge()with only object-producing parsers (the intended usage) continues to work unchanged.Improved help and version handling in
run()function with better edge case support and more consistent behavior. The implementation was refactored from a complex conditional parser generator into modular helper functions, improving maintainability and test coverage:- Enhanced last-option-wins pattern:
--version --helpshows help,--help --versionshows version - Added support for options terminator (
--) to prevent help/version flags after--from being interpreted as options - Improved handling of multiple identical flags (e.g.,
--version --version)
The public API remains unchanged - existing
run()usage continues to work identically while benefiting from more robust edge case handling. [#13]- Enhanced last-option-wins pattern:
Added
showDefaultoption to display default values in help text. When enabled, options and arguments created withwithDefault()will display their default values in the help output:const parser = object({ port: withDefault(option("--port", integer()), 3000), format: withDefault(option("--format", string()), "json"), }); // Basic usage shows: --port [3000], --format [json] formatDocPage("myapp", doc, { showDefault: true }); // Custom formatting formatDocPage("myapp", doc, { showDefault: { prefix: " (default: ", suffix: ")" } }); // Shows: --port (default: 3000), --format (default: json)The feature is opt-in and backward compatible. Default values are automatically dimmed when colors are enabled. A new
ShowDefaultOptionsinterface provides type-safe customization of the display format. [#14]Added
brief,description, andfooteroptions to theRunOptionsinterface in therun()function. These fields allow users to provide rich documentation that appears in help text without modifying parser definitions:import { run } from "@optique/core/facade"; import { message } from "@optique/core/message"; run(parser, "myapp", args, { brief: message`A powerful CLI tool for data processing`, description: message`This tool provides comprehensive data processing capabilities with support for multiple formats and transformations.`, footer: message`For more information, visit https://example.com`, help: { mode: "option" }, });The documentation fields appear in both help output (when
--helpis used) and error output (whenaboveError: "help"is configured). These options augment theDocPagegenerated bygetDocPage(), with user-provided values taking precedence over parser-generated content. [#15]
@optique/run
Added
showDefaultoption to theRunOptionsinterface. This option works identically to the same option in@optique/core/facade, allowing users to display default values in help text when using the process-integratedrun()function:import { run } from "@optique/run"; const result = run(parser, { showDefault: true, // Shows default values in help colors: true, // With dim styling });This ensures feature parity between the low-level facade API and the high-level run package. [#14]
Added
brief,description, andfooteroptions to theRunOptionsinterface. These fields provide the same rich documentation capabilities as the corresponding options in@optique/core/facade, but with the convenience of automatic process integration:import { run } from "@optique/run"; import { message } from "@optique/core/message"; const result = run(parser, { brief: message`A powerful CLI tool for data processing`, description: message`This tool provides comprehensive data processing capabilities with support for multiple formats and transformations.`, footer: message`For more information, visit https://example.com`, help: "option", // Enables --help option version: "1.0.0", // Enables --version option });This maintains feature parity between the low-level facade API and the high-level run package, allowing developers to create rich CLI documentation regardless of which API they use. [#15]
@optique/temporal
The @optique/temporal package was introduced in this release, providing parsers for JavaScript Temporal types. This package depends on the @js-temporal/polyfill package for environments that do not yet support Temporal natively.
- Added
TimeZonetype. - Added
instant()value parser. - Added
InstantOptionsinterface. - Added
duration()value parser. - Added
DurationOptionsinterface. - added
zonedDateTime()value parser. - Added
ZonedDateTimeOptionsinterface. - Added
plainDate()value parser. - Added
PlainDateOptionsinterface. - Added
plainTime()value parser. - Added
PlainTimeOptionsinterface. - Added
plainDateTime()value parser. - Added
PlainDateTimeOptionsinterface. - Added
plainYearMonth()value parser. - Added
PlainYearMonthOptionsinterface. - Added
plainMonthDay()value parser. - Added
PlainMonthDayOptionsinterface. - Added
timeZone()value parser. - Added
TimeZoneOptionsinterface.
Version 0.3.2
Released on September 10, 2025.
@optique/run
- Fixed dependency resolution bug where @optique/core dependency was not properly versioned, causing installations to use outdated stable versions instead of matching development versions. This resolves type errors and message formatting issues when using dev versions. [#22]
Version 0.3.1
Released on September 8, 2025.
@optique/run
- Fixed inconsistent error message coloring across JavaScript runtimes by replacing
console.error()andconsole.log()with directprocess.stderr.write()andprocess.stdout.write()calls. Previously, runtimes like Bun would displayconsole.error()output in red, causing ANSI reset codes in formatted option names to interrupt the error coloring. This change ensures consistent output formatting across all JavaScript runtimes. [#20]
Version 0.3.0
Released on August 29, 2025.
@optique/core
Added
flag()parser for creating required Boolean flags that must be explicitly provided. Unlikeoption()which defaults tofalsewhen not present,flag()fails parsing when not provided, making it useful for dependent options and conditional parsing scenarios.Extended
or()combinator to support up to 10 parsers (previously limited to 5), enabling more complex command-line interfaces with larger numbers of mutually exclusive subcommands or options.Enhanced
withDefault()to support union types when the default value is a different type from the parser result. The result type is nowT | TDefaultinstead of requiring the default to match the parser type. This enables patterns like conditional CLI structures with dependent options where different default structures are needed based on flag states.Modified
object()parser to use greedy parsing behavior. The parser now attempts to consume all matching fields in a single parse call, rather than returning after the first successful field match. This enables dependent options patterns where multiple related options need to be parsed together (e.g.,--flag --dependent-option). While this change maintains backward compatibility for most use cases, it may affect performance and parsing order in complex scenarios.Enhanced
merge()combinator type constraints to accept any parser that produces object-like values, not justobject()parsers. This enables combiningwithDefault(),map(), and other parsers that generate objects withmerge(). The combinator also now properly supports parsers with different state management strategies using specialized state handling to preserve the expected state format for each parser type.Modified
Parser.getDocFragments()method signature to useDocState<TState>discriminated union instead of direct state values. The new signature isgetDocFragments(state: DocState<TState>, ...args). This change improves type safety by explicitly modeling when parser state is available versus unavailable, fixing crashes in help generation when usingmerge()withor()combinations. This only affects advanced users who directly callgetDocFragments()or implement custom parsers.- Added
DocStatetype. - Changed the type of
Parser.getDocFragments()'s first parameter fromTStatetoDocState<TState>.
- Added
Added
longestMatch()combinator that selects the parser which consumes the most input tokens. Unlikeor()which returns the first successful match,longestMatch()tries all provided parsers and chooses the one with the longest match. This enables context-aware parsing where more specific patterns take precedence over general ones, making it ideal for implementing sophisticated help systems wherecommand --helpshows command-specific help rather than global help. The combinator supports function overloads for 2–5 parsers plus a variadic version, with full type inference for discriminated union results.Modified
run()function in@optique/core/facadeto uselongestMatch()instead ofor()for help parsing. This enables context-aware help behavior wheresubcommand --helpshows help specific to that command instead of global help. For example,myapp list --helpnow displays help for thelistcommand rather than the global application help. This change affects all help modes ("command","option", and"both"). Applications using therun()function will automatically benefit from improved help UX without code changes.Refactored
run()function help API to group related options together for better type safety. The new API prevents invalid combinations and provides a cleaner interface:run(parser, args, { help: { mode: "both", // "command" | "option" | "both" onShow: process.exit, // Optional callback } });Added
versionfunctionality to therun()function, supporting both--versionoption andversioncommand modes:run(parser, args, { version: { mode: "option", // "command" | "option" | "both" value: "1.0.0", // Version string to display onShow: process.exit, // Optional callback } });The
versionconfiguration follows the same pattern ashelp, with three modes:"option": Only--versionflag is available"command": Onlyversionsubcommand is available"both": Both--versionandversionsubcommand work
@optique/run
The
run()function now provides context-aware help behavior, automatically inheriting the improvements from@optique/core/facade. Applications using@optique/runwill see the same enhanced help system wheresubcommand --helpshows command-specific help instead of global help.Simplified help API by removing the
"none"option. Since disabling help can be achieved by simply omitting thehelpoption, the explicit"none"value is no longer needed:// Old API run(parser, { help: "none" }); // Explicitly disable help run(parser, { help: "both" }); // Enable help // New API run(parser, {}); // Help disabled (omit the option) run(parser, { help: "both" }); // Enable help: "command" | "option" | "both"The help option now only accepts
"command" | "option" | "both". To disable help functionality, simply omit thehelpoption entirely.Added
versionfunctionality with a flexible API that supports both simple string and detailed object configurations:// Simple version configuration (uses default "option" mode) run(parser, { version: "1.0.0" }); // Advanced version configuration run(parser, { version: { value: "1.0.0", mode: "both" // "command" | "option" | "both" } });The version functionality automatically calls
process.exit(0)when version information is requested, making it suitable for CLI applications that need to display version and exit.Added structured message output functions for CLI applications with automatic terminal detection and consistent formatting:
- Added
print()function for general output to stdout. - Added
printError()function for error output to stderr with automaticError:prefix and optional process exit. - Added
createPrinter()function for custom output scenarios with predefined formatting options. - Added
PrintOptionsandPrintErrorOptionsinterfaces for type-safe configuration. - Added
Printertype for custom printer functions.
All output functions automatically detect terminal capabilities (colors, width) and format structured messages consistently across different environments.
- Added
Version 0.2.1
Released on September 9, 2025.
@optique/run
- Fixed dependency resolution bug where @optique/core dependency was not properly versioned, causing installations to use outdated stable versions instead of matching development versions. This resolves type errors and message formatting issues when using dev versions. [#22]
Version 0.2.0
Released on August 22, 2025.
@optique/core
Added
concat()function for concatenating multipletuple()parsers into a single flattened tuple, similar to howmerge()works forobject()parsers. [#1]Fixed an infinite loop issue in the main parsing loop that could occur when parsers succeeded but didn't consume input.
Fixed bundled short options (e.g.,
-vdffor-v -d -f) not being parsed correctly in some cases.
Version 0.1.2
Released on September 9, 2025.
@optique/run
- Fixed dependency resolution bug where @optique/core dependency was not properly versioned, causing installations to use outdated stable versions instead of matching development versions. This resolves type errors and message formatting issues when using dev versions. [#22]
Version 0.1.1
Released on August 21, 2025.
@optique/core
- Fixed a bug where
object()parsers containing only Boolean flags would fail when no arguments were provided, instead of defaulting the flags tofalse. [#6]
Version 0.1.0
Released on August 21, 2025. Initial release.