1/**
2 * Defines the shared parameter model for CLI arguments and flags.
3 *
4 * A `Param<Kind, A>` describes how to consume parsed command-line input and
5 * return a typed value. The `Kind` decides whether the parameter reads
6 * positional arguments or named flags. `Argument` and `Flag` build on this
7 * module to share parsing structure, primitive constructors, help metadata,
8 * aliases, defaults, prompts, configuration fallbacks, validation, schema
9 * decoding, fallback parameters, and traversal helpers.
10 *
11 * @stability unstable
12 * @since 4.0.0
13 */
14import * as Config from "../Config.ts"
15import * as Effect from "../Effect.ts"
16import { dual, identity, type LazyArg } from "../Function.ts"
17import * as Option from "../Option.ts"
18import { type Pipeable, pipeArguments } from "../Pipeable.ts"
19import * as Predicate from "../Predicate.ts"
20import type * as Redacted_ from "../Redacted.ts"
21import * as Result from "../Result.ts"
22import * as Schema from "../Schema.ts"
23import type { Covariant } from "../Types.ts"
24import * as CliError from "./CliError.ts"
25import type { Environment } from "./Command.ts"
26import * as Primitive from "./Primitive.ts"
27import * as Prompt from "./Prompt.ts"
28
29const TypeId = "~effect/cli/Param"
30
31/**
32 * Polymorphic CLI parameter shared by `Argument` and `Flag`.
33 *
34 * **Details**
35 *
36 * A parameter knows whether it consumes positional arguments or flags and
37 * parses a `ParsedArgs` value into its typed result.
38 *
39 * @stability unstable
40 * @category models
41 * @since 4.0.0
42 */
43export interface Param<Kind extends ParamKind, out A> extends Param.Variance<A> {
44 readonly _tag: "Single" | "Map" | "Transform" | "Optional" | "Variadic"
45 readonly kind: Kind
46 readonly parse: Parse<A>
47}
48
49/**
50 * Discriminator for whether a `Param` parses positional arguments or
51 * command-line flags.
52 *
53 * @stability unstable
54 * @category models
55 * @since 4.0.0
56 */
57export type ParamKind = "argument" | "flag"
58
59/**
60 * Defines the kind discriminator for positional argument parameters.
61 *
62 * **When to use**
63 *
64 * Use to build low-level `Param` constructors or type positions for positional
65 * argument parameters.
66 *
67 * @see {@link flagKind} for the named flag parameter discriminator
68 * @see {@link ParamKind} for the full parameter kind union
69 *
70 * @stability unstable
71 * @category constants
72 * @since 4.0.0
73 */
74export const argumentKind: "argument" = "argument" as const
75
76/**
77 * Defines the kind discriminator for flag parameters.
78 *
79 * **When to use**
80 *
81 * Use to build low-level `Param` constructors or type positions for named flag
82 * parameters.
83 *
84 * @see {@link argumentKind} for the positional argument parameter discriminator
85 *
86 * @stability unstable
87 * @category constants
88 * @since 4.0.0
89 */
90export const flagKind: "flag" = "flag" as const
91
92/**
93 * Represents any parameter.
94 *
95 * @stability unstable
96 * @category models
97 * @since 4.0.0
98 */
99export type Any = Param<ParamKind, unknown>
100
101/**
102 * Represents any positional argument parameter.
103 *
104 * @stability unstable
105 * @category models
106 * @since 4.0.0
107 */
108export type AnyArgument = Param<typeof argumentKind, unknown>
109
110/**
111 * Represents any flag parameter.
112 *
113 * @stability unstable
114 * @category models
115 * @since 4.0.0
116 */
117export type AnyFlag = Param<typeof flagKind, unknown>
118
119/**
120 * Function type used by parameters to parse currently available flags and
121 * positional arguments.
122 *
123 * **Details**
124 *
125 * It returns the remaining positional arguments together with the parsed value,
126 * or fails with a `CliError` while requiring the CLI parsing environment.
127 *
128 * @stability unstable
129 * @category models
130 * @since 4.0.0
131 */
132export type Parse<A> = (args: ParsedArgs) => Effect.Effect<
133 readonly [leftover: ReadonlyArray<string>, value: A],
134 CliError.CliError,
135 Environment
136>
137
138/**
139 * Namespace containing type-level utilities attached to the `Param` interface.
140 *
141 * @stability unstable
142 * @since 4.0.0
143 */
144export declare namespace Param {
145 /**
146 * Variance and pipeability marker carried by every `Param` value.
147 *
148 * @stability unstable
149 * @category models
150 * @since 4.0.0
151 */
152 export interface Variance<out A> extends Pipeable {
153 readonly [TypeId]: {
154 readonly _A: Covariant<A>
155 }
156 }
157}
158
159/**
160 * Map of flag names to their provided string values.
161 * Multiple occurrences of a flag produce multiple values.
162 *
163 * @stability unstable
164 * @category models
165 * @since 4.0.0
166 */
167export type Flags = Record<string, ReadonlyArray<string>>
168
169/**
170 * Input context passed to `Param.parse` implementations.
171 * - `flags`: already-collected flag values by canonical flag name
172 * - `arguments`: remaining positional arguments to be consumed
173 *
174 * @stability unstable
175 * @category models
176 * @since 4.0.0
177 */
178export interface ParsedArgs {
179 readonly flags: Flags
180 readonly arguments: ReadonlyArray<string>
181}
182
183/**
184 * Represents a fallback prompt that can either be provided directly or
185 * computed effectfully when the parameter is missing.
186 *
187 * @stability unstable
188 * @category models
189 * @since 4.0.0
190 */
191export type FallbackPrompt<A> =
192 | Prompt.Prompt<A>
193 | Effect.Effect<Prompt.Prompt<A>, CliError.CliError, Environment>
194
195/**
196 * Leaf parameter that reads one named argument or flag with a primitive parser.
197 *
198 * **Details**
199 *
200 * Single parameters carry the user-facing name, aliases, description, primitive
201 * type, and optional metavar/type name used in help output.
202 *
203 * @stability unstable
204 * @category models
205 * @since 4.0.0
206 */
207export interface Single<Kind extends ParamKind, out A> extends Param<Kind, A> {
208 readonly _tag: "Single"
209 readonly kind: Kind
210 readonly name: string
211 readonly description: Option.Option<string>
212 readonly aliases: ReadonlyArray<string>
213 readonly primitiveType: Primitive.Primitive<A>
214 readonly typeName?: string | undefined
215 readonly hidden: boolean
216}
217
218/**
219 * Parameter node that maps the successfully parsed value of another parameter
220 * with a pure function.
221 *
222 * @stability unstable
223 * @category models
224 * @since 4.0.0
225 */
226export interface Map<Kind extends ParamKind, in out A, out B> extends Param<Kind, B> {
227 readonly _tag: "Map"
228 readonly kind: Kind
229 readonly param: Param<Kind, A>
230 readonly f: (value: A) => B
231}
232
233/**
234 * Parameter node that rewrites another parameter's parser, allowing effectful
235 * validation, fallback behavior, or error translation while preserving the same
236 * parameter kind.
237 *
238 * @stability unstable
239 * @category models
240 * @since 4.0.0
241 */
242export interface Transform<Kind extends ParamKind, in out A, out B> extends Param<Kind, B> {
243 readonly _tag: "Transform"
244 readonly kind: Kind
245 readonly param: Param<Kind, A>
246 readonly alternatives: ReadonlyArray<LazyArg<Param<Kind, unknown>>>
247 readonly f: (
248 parse: Parse<A>,
249 alternatives: ReadonlyArray<LazyArg<Parse<unknown>>>
250 ) => Parse<B>
251}
252
253/**
254 * Parameter node that turns a missing argument or flag into `Option.none()` and
255 * a present parsed value into `Option.some(value)`.
256 *
257 * @stability unstable
258 * @category models
259 * @since 4.0.0
260 */
261export interface Optional<Kind extends ParamKind, A> extends Param<Kind, Option.Option<A>> {
262 readonly _tag: "Optional"
263 readonly kind: Kind
264 readonly param: Param<Kind, A>
265}
266
267/**
268 * Parameter node that parses another parameter zero or more times and returns
269 * all parsed values as an array, respecting optional minimum and maximum
270 * occurrence bounds.
271 *
272 * @stability unstable
273 * @category models
274 * @since 4.0.0
275 */
276export interface Variadic<Kind extends ParamKind, A> extends Param<Kind, ReadonlyArray<A>> {
277 readonly _tag: "Variadic"
278 readonly kind: Kind
279 readonly param: Param<Kind, A>
280 readonly min: Option.Option<number>
281 readonly max: Option.Option<number>
282}
283
284const Proto = {
285 [TypeId]: {
286 _A: identity
287 },
288 pipe() {
289 return pipeArguments(this, arguments)
290 }
291}
292
293/**
294 * Type guard to check if a value is a Param.
295 *
296 * **Example** (Checking for params)
297 *
298 * ```ts import.meta.vitest
299 * import { Param } from "effect/cli"
300 *
301 * const maybeParam = Param.String(Param.flagKind, "name")
302 *
303 * Param.isParam(maybeParam) // => true
304 * ```
305 *
306 * @stability unstable
307 * @category guards
308 * @since 4.0.0
309 */
310export const isParam = (u: unknown): u is Param<any, ParamKind> => Predicate.hasProperty(u, TypeId)
311
312/**
313 * Type guard to check if a param is a Single param (not composed).
314 *
315 * **Example** (Checking for single params)
316 *
317 * ```ts import.meta.vitest
318 * import { Param } from "effect/cli"
319 *
320 * const nameParam = Param.String(Param.flagKind, "name")
321 * const optionalParam = Param.optional(nameParam)
322 *
323 * Param.isSingle(nameParam) // => true
324 * Param.isSingle(optionalParam) // => false
325 * ```
326 *
327 * @stability unstable
328 * @category guards
329 * @since 4.0.0
330 */
331export const isSingle = <const Kind extends ParamKind, A>(
332 param: Param<Kind, A>
333): param is Single<Kind, A> => Predicate.isTagged(param, "Single")
334
335/**
336 * Type guard to check if a Single param is a flag (not an argument).
337 *
338 * @internal
339 */
340export const isFlagParam = <A>(
341 single: Single<ParamKind, A>
342): single is Single<typeof flagKind, A> => single.kind === "flag"
343
344/**
345 * Constructs a leaf `Single` parameter from its kind, name, primitive parser,
346 * and optional help metadata.
347 *
348 * **Details**
349 *
350 * The returned parser reads either one positional argument or the named flag,
351 * depending on `kind`.
352 *
353 * @stability unstable
354 * @category constructors
355 * @since 4.0.0
356 */
357export const makeSingle = <const Kind extends ParamKind, A>(params: {
358 readonly kind: Kind
359 readonly name: string
360 readonly primitiveType: Primitive.Primitive<A>
361 readonly typeName?: string | undefined
362 readonly description?: Option.Option<string> | undefined
363 readonly aliases?: ReadonlyArray<string> | undefined
364 readonly hidden?: boolean | undefined
365}): Single<Kind, A> => {
366 const parse: Parse<A> = (args) =>
367 params.kind === argumentKind
368 ? parsePositional(params.name, params.primitiveType, args)
369 : parseFlag(params.name, params.primitiveType, args)
370 return Object.setPrototypeOf({
371 _tag: "Single",
372 ...params,
373 description: params.description ?? Option.none(),
374 aliases: params.aliases ?? [],
375 hidden: params.hidden ?? false,
376 parse
377 }, Proto)
378}
379
380/**
381 * Creates a string parameter.
382 *
383 * **Example** (Creating string parameters)
384 *
385 * ```ts import.meta.vitest
386 * import { Param } from "effect/cli"
387 *
388 * // Create a string flag
389 * const nameFlag = Param.String(Param.flagKind, "name")
390 *
391 * // Create a string argument
392 * const fileArg = Param.String(Param.argumentKind, "file")
393 *
394 * // Usage in CLI: --name "John Doe" or as positional argument
395 * const kinds = [nameFlag.kind, fileArg.kind] // => ["flag", "argument"]
396 * ```
397 *
398 * @stability unstable
399 * @category constructors
400 * @since 4.0.0
401 */
402export const String = <const Kind extends ParamKind>(
403 kind: Kind,
404 name: string
405): Param<Kind, string> =>
406 makeSingle({
407 name,
408 primitiveType: Primitive.String,
409 kind
410 })
411
412/**
413 * Creates a boolean parameter.
414 *
415 * **Example** (Creating boolean parameters)
416 *
417 * ```ts import.meta.vitest
418 * import { Param } from "effect/cli"
419 *
420 * // Create a boolean flag
421 * const verboseFlag = Param.Boolean(Param.flagKind, "verbose")
422 *
423 * // Create a boolean argument
424 * const enableArg = Param.Boolean(Param.argumentKind, "enable")
425 *
426 * // Usage in CLI: --verbose (true) or --no-verbose (false).
427 * // The flag is required unless made optional or given a fallback.
428 * // Boolean positional arguments accept true/false.
429 * const kinds = [verboseFlag.kind, enableArg.kind] // => ["flag", "argument"]
430 * ```
431 *
432 * @stability unstable
433 * @category constructors
434 * @since 4.0.0
435 */
436export const Boolean = <const Kind extends ParamKind>(
437 kind: Kind,
438 name: string
439): Param<Kind, boolean> =>
440 makeSingle({
441 name,
442 primitiveType: Primitive.Boolean,
443 kind
444 })
445
446/**
447 * Creates an integer parameter.
448 *
449 * **Example** (Creating integer parameters)
450 *
451 * ```ts import.meta.vitest
452 * import { Param } from "effect/cli"
453 *
454 * // Create an integer flag
455 * const portFlag = Param.Int(Param.flagKind, "port")
456 *
457 * // Create an integer argument
458 * const countArg = Param.Int(Param.argumentKind, "count")
459 *
460 * // Usage in CLI: --port 8080 or as positional argument: 42
461 * const kinds = [portFlag.kind, countArg.kind] // => ["flag", "argument"]
462 * ```
463 *
464 * @stability unstable
465 * @category constructors
466 * @since 4.0.0
467 */
468export const Int = <const Kind extends ParamKind>(
469 kind: Kind,
470 name: string
471): Param<Kind, number> =>
472 makeSingle({
473 name,
474 primitiveType: Primitive.Int,
475 kind
476 })
477
478/**
479 * Creates a finite number parameter.
480 *
481 * **Example** (Creating finite number parameters)
482 *
483 * ```ts import.meta.vitest
484 * import { Param } from "effect/cli"
485 *
486 * const rateFlag = Param.Finite(Param.flagKind, "rate")
487 *
488 * const thresholdArg = Param.Finite(Param.argumentKind, "threshold")
489 *
490 * const kinds = [rateFlag.kind, thresholdArg.kind] // => ["flag", "argument"]
491 * ```
492 *
493 * @stability unstable
494 * @category constructors
495 * @since 4.0.0
496 */
497export const Finite = <const Kind extends ParamKind>(
498 kind: Kind,
499 name: string
500): Param<Kind, number> =>
501 makeSingle({
502 name,
503 primitiveType: Primitive.Finite,
504 kind
505 })
506
507/**
508 * Creates a date parameter that parses ISO date strings.
509 *
510 * **Example** (Creating date parameters)
511 *
512 * ```ts import.meta.vitest
513 * import { Param } from "effect/cli"
514 *
515 * // Create a date flag
516 * const startFlag = Param.Date(Param.flagKind, "start-date")
517 *
518 * // Create a date argument
519 * const dueDateArg = Param.Date(Param.argumentKind, "due-date")
520 *
521 * // Usage in CLI: --start-date "2023-12-25" or as positional: "2023-01-01"
522 * // Parses to JavaScript Date object
523 * const kinds = [startFlag.kind, dueDateArg.kind] // => ["flag", "argument"]
524 * ```
525 *
526 * @stability unstable
527 * @category constructors
528 * @since 4.0.0
529 */
530export const Date = <const Kind extends ParamKind>(
531 kind: Kind,
532 name: string
533): Param<Kind, globalThis.Date> =>
534 makeSingle({
535 name,
536 primitiveType: Primitive.Date,
537 kind
538 })
539
540/**
541 * Constructs command-line params that represent a choice between several
542 * inputs. The input will be mapped to it's associated value during parsing.
543 *
544 * **Example** (Creating valued choices)
545 *
546 * ```ts import.meta.vitest
547 * import { Param } from "effect/cli"
548 *
549 * type Animal = Dog | Cat
550 *
551 * interface Dog {
552 * readonly _tag: "Dog"
553 * }
554 *
555 * interface Cat {
556 * readonly _tag: "Cat"
557 * }
558 *
559 * const animal = Param.ChoiceWithValue(Param.flagKind, "animal", [
560 * ["dog", { _tag: "Dog" }],
561 * ["cat", { _tag: "Cat" }]
562 * ])
563 * animal.kind // => "flag"
564 * ```
565 *
566 * @stability unstable
567 * @category constructors
568 * @since 4.0.0
569 */
570export const ChoiceWithValue = <
571 const Kind extends ParamKind,
572 const Choices extends ReadonlyArray<readonly [string, any]>
573>(kind: Kind, name: string, choices: Choices): Param<Kind, Choices[number][1]> =>
574 makeSingle({
575 name,
576 primitiveType: Primitive.Choice(choices),
577 kind
578 })
579
580/**
581 * Constructs command-line params that represent a choice between several
582 * string inputs.
583 *
584 * **Example** (Creating string choices)
585 *
586 * ```ts import.meta.vitest
587 * import { Param } from "effect/cli"
588 *
589 * const logLevel = Param.Literals(Param.flagKind, "log-level", [
590 * "debug",
591 * "info",
592 * "warn",
593 * "error"
594 * ])
595 * logLevel.kind // => "flag"
596 * ```
597 *
598 * @stability unstable
599 * @category constructors
600 * @since 4.0.0
601 */
602export const Literals = <
603 const Kind extends ParamKind,
604 const Choices extends ReadonlyArray<string>
605>(kind: Kind, name: string, choices: Choices): Param<Kind, Choices[number]> => {
606 const mappedChoices = choices.map((value) => [value, value] as const)
607 return ChoiceWithValue(kind, name, mappedChoices)
608}
609
610/**
611 * Creates a path parameter that accepts file or directory paths.
612 *
613 * **Example** (Creating path parameters)
614 *
615 * ```ts import.meta.vitest
616 * import { Param } from "effect/cli"
617 *
618 * // Basic path parameter
619 * const outputPath = Param.Path(Param.flagKind, "output")
620 *
621 * // Path that must exist
622 * const inputPath = Param.Path(Param.flagKind, "input", { mustExist: true })
623 *
624 * // File-only path
625 * const configFile = Param.Path(Param.flagKind, "config", {
626 * pathType: "file",
627 * mustExist: true,
628 * typeName: "config-file"
629 * })
630 * const kinds = [outputPath.kind, inputPath.kind, configFile.kind] // => ["flag", "flag", "flag"]
631 * ```
632 *
633 * @stability unstable
634 * @category constructors
635 * @since 4.0.0
636 */
637export const Path = <Kind extends ParamKind>(
638 kind: Kind,
639 name: string,
640 options?: {
641 readonly pathType?: Primitive.PathType | undefined
642 readonly mustExist?: boolean | undefined
643 readonly typeName?: string | undefined
644 }
645): Param<Kind, string> =>
646 makeSingle({
647 name,
648 kind,
649 primitiveType: Primitive.Path(options?.pathType ?? "either", options?.mustExist),
650 typeName: options?.typeName
651 })
652
653/**
654 * Creates a directory path parameter.
655 *
656 * **Details**
657 *
658 * This is a convenience function that creates a path parameter with the
659 * `pathType` set to `"directory"` and a default type name of `"directory"`.
660 *
661 * **Example** (Creating directory parameters)
662 *
663 * ```ts import.meta.vitest
664 * import { Param } from "effect/cli"
665 *
666 * // Basic directory parameter
667 * const outputDir = Param.Directory(Param.flagKind, "output-dir")
668 *
669 * // Directory that must exist
670 * const sourceDir = Param.Directory(Param.flagKind, "source", { mustExist: true })
671 *
672 * // Usage: --output-dir /path/to/dir --source /existing/dir
673 * const kinds = [outputDir.kind, sourceDir.kind] // => ["flag", "flag"]
674 * ```
675 *
676 * @stability unstable
677 * @category constructors
678 * @since 4.0.0
679 */
680export const Directory = <Kind extends ParamKind>(
681 kind: Kind,
682 name: string,
683 options?: {
684 readonly mustExist?: boolean | undefined
685 }
686): Param<Kind, string> =>
687 Path(kind, name, {
688 pathType: "directory",
689 typeName: "directory",
690 mustExist: options?.mustExist
691 })
692
693/**
694 * Creates a file path parameter.
695 *
696 * **Details**
697 *
698 * This is a convenience function that creates a path parameter with a
699 * `pathType` set to `"file"` and a default type name of `"file"`.
700 *
701 * **Example** (Creating file parameters)
702 *
703 * ```ts import.meta.vitest
704 * import { Param } from "effect/cli"
705 *
706 * // Basic file parameter
707 * const outputFile = Param.File(Param.flagKind, "output")
708 *
709 * // File that must exist
710 * const inputFile = Param.File(Param.flagKind, "input", { mustExist: true })
711 *
712 * // Usage: --output result.txt --input existing-file.txt
713 * const kinds = [outputFile.kind, inputFile.kind] // => ["flag", "flag"]
714 * ```
715 *
716 * @stability unstable
717 * @category constructors
718 * @since 4.0.0
719 */
720export const File = <Kind extends ParamKind>(
721 kind: Kind,
722 name: string,
723 options?: {
724 readonly mustExist?: boolean | undefined
725 }
726): Param<Kind, string> =>
727 Path(kind, name, {
728 pathType: "file",
729 typeName: "file",
730 mustExist: options?.mustExist
731 })
732
733/**
734 * Creates a redacted parameter for sensitive data like passwords.
735 * The value is masked in help output and logging.
736 *
737 * **Example** (Creating redacted parameters)
738 *
739 * ```ts import.meta.vitest
740 * import { Param } from "effect/cli"
741 *
742 * // Create a password parameter
743 * const password = Param.Redacted(Param.flagKind, "password")
744 *
745 * // Create an API key argument
746 * const apiKey = Param.Redacted(Param.argumentKind, "api-key")
747 *
748 * // Usage: --password (value will be hidden in help/logs)
749 * const kinds = [password.kind, apiKey.kind] // => ["flag", "argument"]
750 * ```
751 *
752 * @stability unstable
753 * @category constructors
754 * @since 4.0.0
755 */
756export const Redacted = <Kind extends ParamKind>(
757 kind: Kind,
758 name: string
759): Param<Kind, Redacted_.Redacted<string>> =>
760 makeSingle({
761 name,
762 primitiveType: Primitive.Redacted,
763 kind
764 })
765
766/**
767 * Creates a parameter that reads and returns file content as a string.
768 *
769 * **Example** (Reading file text)
770 *
771 * ```ts import.meta.vitest
772 * import { Param } from "effect/cli"
773 *
774 * // Read a config file as string
775 * const configContent = Param.FileText(Param.flagKind, "config")
776 *
777 * // Read a template file as argument
778 * const templateContent = Param.FileText(Param.argumentKind, "template")
779 *
780 * // Usage: --config config.txt (reads file content into string)
781 * const kinds = [configContent.kind, templateContent.kind] // => ["flag", "argument"]
782 * ```
783 *
784 * @stability unstable
785 * @category constructors
786 * @since 4.0.0
787 */
788export const FileText = <Kind extends ParamKind>(kind: Kind, name: string): Param<Kind, string> =>
789 makeSingle({
790 name,
791 primitiveType: Primitive.FileText,
792 kind
793 })
794
795/**
796 * Creates a param that reads and parses the content of the specified file.
797 *
798 * **Details**
799 *
800 * The parser that is utilized will depend on the specified `format`, or the
801 * extension of the file passed on the command-line if no `format` is specified.
802 *
803 * **Example** (Parsing file contents)
804 *
805 * ```ts import.meta.vitest
806 * import { Param } from "effect/cli"
807 *
808 * // Will use the extension of the file passed on the command line to determine
809 * // the parser to use
810 * const config = Param.FileParse(Param.flagKind, "config")
811 *
812 * // Will use the JSON parser
813 * const jsonConfig = Param.FileParse(Param.flagKind, "json-config", {
814 * format: "json"
815 * })
816 * const kinds = [config.kind, jsonConfig.kind] // => ["flag", "flag"]
817 * ```
818 *
819 * @stability unstable
820 * @category constructors
821 * @since 4.0.0
822 */
823export const FileParse = <Kind extends ParamKind>(
824 kind: Kind,
825 name: string,
826 options?: Primitive.FileParseOptions | undefined
827): Param<Kind, unknown> =>
828 makeSingle({
829 name,
830 primitiveType: Primitive.FileParse(options),
831 kind
832 })
833
834/**
835 * Creates a parameter that reads and validates file content using a schema.
836 *
837 * **Example** (Validating file contents)
838 *
839 * ```ts import.meta.vitest
840 * import { Schema } from "effect"
841 * import { Param } from "effect/cli"
842 * // Parse JSON config file
843 * const configSchema = Schema.Struct({
844 * port: Schema.Number,
845 * host: Schema.String
846 * })
847 *
848 * const config = Param.FileSchema(Param.flagKind, "config", configSchema, {
849 * format: "json"
850 * })
851 *
852 * // Parse YAML file
853 * const yamlConfig = Param.FileSchema(Param.flagKind, "config", configSchema, {
854 * format: "yaml"
855 * })
856 *
857 * // Usage: --config config.json (reads and validates file content)
858 * const kinds = [config.kind, yamlConfig.kind] // => ["flag", "flag"]
859 * ```
860 *
861 * @stability unstable
862 * @category constructors
863 * @since 4.0.0
864 */
865export const FileSchema = <Kind extends ParamKind, A>(
866 kind: Kind,
867 name: string,
868 schema: Schema.ConstraintDecoder<A, Environment>,
869 options?: Primitive.FileSchemaOptions | undefined
870): Param<Kind, A> =>
871 makeSingle({
872 name,
873 primitiveType: Primitive.FileSchema(schema, options),
874 kind
875 })
876
877/**
878 * Creates a param that parses key=value pairs.
879 *
880 * **When to use**
881 *
882 * Use when you need command-line options or arguments that collect `key=value`
883 * configuration entries.
884 *
885 * **Details**
886 *
887 * Requires at least one key=value pair. The parsed pairs are merged into a
888 * single record object.
889 *
890 * **Example** (Parsing key-value pairs)
891 *
892 * ```ts import.meta.vitest
893 * import { Param } from "effect/cli"
894 *
895 * const env = Param.KeyValuePair(Param.flagKind, "env")
896 * // --env FOO=bar --env BAZ=qux will parse to { FOO: "bar", BAZ: "qux" }
897 *
898 * const props = Param.KeyValuePair(Param.flagKind, "property")
899 * // --property name=value --property debug=true
900 * const kinds = [env.kind, props.kind] // => ["flag", "flag"]
901 * ```
902 *
903 * @stability unstable
904 * @category constructors
905 * @since 4.0.0
906 */
907export const KeyValuePair = <Kind extends ParamKind>(
908 kind: Kind,
909 name: string
910): Param<Kind, Record<string, string>> =>
911 map(
912 variadic(
913 makeSingle({
914 name,
915 primitiveType: Primitive.KeyValuePair,
916 kind
917 }),
918 { min: 1 }
919 ),
920 (objects) => Object.fromEntries(objects.flatMap(Object.entries))
921 )
922
923/**
924 * A parameter that always fails to parse.
925 *
926 * **Example** (Creating sentinel parameters)
927 *
928 * ```ts import.meta.vitest
929 * import { Param } from "effect/cli"
930 *
931 * const disabledDebugParam = Param.Never(Param.flagKind)
932 *
933 * const makeDebugParam = (enableDebug: boolean) =>
934 * enableDebug ? Param.String(Param.flagKind, "debug") : disabledDebugParam
935 *
936 * makeDebugParam(true) === disabledDebugParam // => false
937 * makeDebugParam(false) === disabledDebugParam // => true
938 * ```
939 *
940 * @stability unstable
941 * @category constructors
942 * @since 4.0.0
943 */
944export const Never = <Kind extends ParamKind>(kind: Kind): Param<Kind, never> =>
945 makeSingle({
946 name: "__never__",
947 primitiveType: Primitive.Never,
948 kind
949 })
950
951const FLAG_DASH_REGEXP = /^-+/
952
953/**
954 * Adds an alias to an option.
955 *
956 * **When to use**
957 *
958 * Use when you need a CLI parameter to accept an alternate name, such as "-f"
959 * for "--force".
960 *
961 * **Details**
962 *
963 * This works on any param structure by recursively finding the underlying
964 * `Single` node and applying the alias there.
965 *
966 * **Example** (Adding parameter aliases)
967 *
968 * ```ts import.meta.vitest
969 * import { Param } from "effect/cli"
970 *
971 * const force = Param.Boolean(Param.flagKind, "force").pipe(
972 * Param.withAlias("-f"),
973 * Param.withAlias("-F")
974 * )
975 *
976 * // Also works on composed params:
977 * const count = Param.Int(Param.flagKind, "count").pipe(
978 * Param.optional,
979 * Param.withAlias("-c") // finds the underlying Single and adds alias
980 * )
981 * const kinds = [force.kind, count.kind] // => ["flag", "flag"]
982 * ```
983 *
984 * @stability unstable
985 * @category combinators
986 * @since 4.0.0
987 */
988export const withAlias: {
989 <Kind extends ParamKind, A>(alias: string): (self: Param<Kind, A>) => Param<Kind, A>
990 <Kind extends ParamKind, A>(self: Param<Kind, A>, alias: string): Param<Kind, A>
991} = dual(2, <Kind extends ParamKind, A>(self: Param<Kind, A>, alias: string) => {
992 return transformSingle(self, <X>(single: Single<Kind, X>) =>
993 makeSingle({
994 ...single,
995 aliases: [...single.aliases, alias.replace(FLAG_DASH_REGEXP, "")]
996 }))
997})
998
999/**
1000 * Adds a description to an option for help text.
1001 *
1002 * **Details**
1003 *
1004 * Descriptions provide users with information about what the option does
1005 * when they view help documentation.
1006 *
1007 * **Example** (Adding help descriptions)
1008 *
1009 * ```ts import.meta.vitest
1010 * import { Param } from "effect/cli"
1011 *
1012 * const verbose = Param.Boolean(Param.flagKind, "verbose").pipe(
1013 * Param.withAlias("-v"),
1014 * Param.withDescription("Enable verbose output")
1015 * )
1016 * verbose.kind // => "flag"
1017 * ```
1018 *
1019 * @stability unstable
1020 * @category combinators
1021 * @since 4.0.0
1022 */
1023export const withDescription: {
1024 <Kind extends ParamKind, A>(description: string): (self: Param<Kind, A>) => Param<Kind, A>
1025 <Kind extends ParamKind, A>(self: Param<Kind, A>, description: string): Param<Kind, A>
1026} = dual(2, <Kind extends ParamKind, A>(self: Param<Kind, A>, description: string) => {
1027 return transformSingle(self, <X>(single: Single<Kind, X>) =>
1028 makeSingle({
1029 ...single,
1030 description: Option.some(description)
1031 }))
1032})
1033
1034/**
1035 * Hides a parameter from generated help output and completions while keeping
1036 * it parseable on the command line.
1037 *
1038 * **When to use**
1039 *
1040 * Use when experimental, internal, or deprecated flags should be accepted but
1041 * not advertised.
1042 *
1043 * **Example** (Hiding a flag from help)
1044 *
1045 * ```ts import.meta.vitest
1046 * import { Param } from "effect/cli"
1047 *
1048 * const experimental = Param.Boolean(Param.flagKind, "experimental-foo").pipe(
1049 * Param.withHidden
1050 * )
1051 * experimental.kind // => "flag"
1052 * ```
1053 *
1054 * @stability unstable
1055 * @category metadata
1056 * @since 4.0.0
1057 */
1058export const withHidden = <Kind extends ParamKind, A>(self: Param<Kind, A>): Param<Kind, A> =>
1059 transformSingle(self, <X>(single: Single<Kind, X>) =>
1060 makeSingle({
1061 ...single,
1062 hidden: true
1063 }))
1064
1065/**
1066 * Transforms the parsed value of an option using a mapping function.
1067 *
1068 * **Example** (Mapping parsed values)
1069 *
1070 * ```ts import.meta.vitest
1071 * import { Param } from "effect/cli"
1072 *
1073 * const port = Param.Int(Param.flagKind, "port").pipe(
1074 * Param.map((n) => ({ port: n, url: `http://localhost:${n}` }))
1075 * )
1076 * port.kind // => "flag"
1077 * ```
1078 *
1079 * @stability unstable
1080 * @category combinators
1081 * @since 4.0.0
1082 */
1083export const map: {
1084 <A, B>(f: (a: A) => B): <Kind extends ParamKind>(self: Param<Kind, A>) => Param<Kind, B>
1085 <Kind extends ParamKind, A, B>(self: Param<Kind, A>, f: (a: A) => B): Param<Kind, B>
1086} = dual(2, <Kind extends ParamKind, A, B>(self: Param<Kind, A>, f: (a: A) => B) => {
1087 const parse: Parse<B> = (args: ParsedArgs) =>
1088 Effect.map(
1089 self.parse(args),
1090 ([operands, value]) => [operands, f(value)] as const
1091 )
1092 return Object.assign(Object.create(Proto), {
1093 _tag: "Map",
1094 kind: self.kind,
1095 param: self,
1096 f,
1097 parse
1098 })
1099})
1100
1101const transform = <Kind extends ParamKind, A, B>(
1102 self: Param<Kind, A>,
1103 f: (
1104 parse: Parse<A>,
1105 alternatives: ReadonlyArray<LazyArg<Parse<unknown>>>
1106 ) => Parse<B>,
1107 alternatives: ReadonlyArray<LazyArg<Param<Kind, unknown>>> = []
1108): Transform<Kind, A, B> => {
1109 const alternativeParsers = alternatives.map((alternative) => () => alternative().parse)
1110 return Object.assign(Object.create(Proto), {
1111 _tag: "Transform",
1112 kind: self.kind,
1113 param: self,
1114 alternatives,
1115 f,
1116 parse: f(self.parse, alternativeParsers)
1117 })
1118}
1119
1120/**
1121 * Transforms the parsed value of an option using an effectful mapping function.
1122 *
1123 * **Example** (Mapping parsed values effectfully)
1124 *
1125 * ```ts import.meta.vitest
1126 * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect"
1127 * import { CliError, Param } from "effect/cli"
1128 * import { ChildProcessSpawner } from "effect/process"
1129 *
1130 * const CliTestLayer = Layer.mergeAll(
1131 * FileSystem.layerNoop({}),
1132 * Path.layer,
1133 * Stdio.layerTest({}),
1134 * Layer.succeed(Terminal.Terminal, Terminal.make({
1135 * columns: Effect.succeed(80),
1136 * rows: Effect.succeed(24),
1137 * readInput: Effect.die("unused"),
1138 * readLine: Effect.die("unused"),
1139 * display: () => Effect.void
1140 * })),
1141 * Layer.succeed(
1142 * ChildProcessSpawner.ChildProcessSpawner,
1143 * ChildProcessSpawner.make(() => Effect.die("unused"))
1144 * )
1145 * )
1146 *
1147 * const validatedEmail = Param.String(Param.flagKind, "email").pipe(
1148 * Param.mapEffect((email) =>
1149 * email.includes("@")
1150 * ? Effect.succeed(email)
1151 * : Effect.fail(
1152 * new CliError.InvalidValue({
1153 * option: "email",
1154 * value: email,
1155 * expected: "valid email format",
1156 * kind: "flag"
1157 * })
1158 * )
1159 * )
1160 * )
1161 *
1162 * const [, value] = await Effect.runPromise(
1163 * validatedEmail.parse({
1164 * arguments: [],
1165 * flags: { email: ["alice@example.com"] }
1166 * }).pipe(Effect.provide(CliTestLayer))
1167 * )
1168 * value // => "alice@example.com"
1169 * ```
1170 *
1171 * @stability unstable
1172 * @category combinators
1173 * @since 4.0.0
1174 */
1175export const mapEffect: {
1176 <A, B>(
1177 f: (a: A) => Effect.Effect<B, CliError.CliError, Environment>
1178 ): <Kind extends ParamKind>(self: Param<Kind, A>) => Param<Kind, B>
1179 <Kind extends ParamKind, A, B>(
1180 self: Param<Kind, A>,
1181 f: (a: A) => Effect.Effect<B, CliError.CliError, Environment>
1182 ): Param<Kind, B>
1183} = dual(2, <Kind extends ParamKind, A, B>(
1184 self: Param<Kind, A>,
1185 f: (a: A) => Effect.Effect<B, CliError.CliError, Environment>
1186) =>
1187 transform(
1188 self,
1189 (parse: Parse<A>) => (args: ParsedArgs) =>
1190 Effect.flatMap(parse(args), ([leftover, a]) =>
1191 f(a).pipe(
1192 Effect.map((b) => [leftover, b] as const)
1193 ))
1194 ))
1195
1196/**
1197 * Transforms the parsed value of an option using a function that may throw,
1198 * converting any thrown errors into failure messages.
1199 *
1200 * **Example** (Mapping thrown errors)
1201 *
1202 * ```ts import.meta.vitest
1203 * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect"
1204 * import { Param } from "effect/cli"
1205 * import { ChildProcessSpawner } from "effect/process"
1206 *
1207 * const CliTestLayer = Layer.mergeAll(
1208 * FileSystem.layerNoop({}),
1209 * Path.layer,
1210 * Stdio.layerTest({}),
1211 * Layer.succeed(Terminal.Terminal, Terminal.make({
1212 * columns: Effect.succeed(80),
1213 * rows: Effect.succeed(24),
1214 * readInput: Effect.die("unused"),
1215 * readLine: Effect.die("unused"),
1216 * display: () => Effect.void
1217 * })),
1218 * Layer.succeed(
1219 * ChildProcessSpawner.ChildProcessSpawner,
1220 * ChildProcessSpawner.make(() => Effect.die("unused"))
1221 * )
1222 * )
1223 *
1224 * const parsedJson = Param.String(Param.flagKind, "config").pipe(
1225 * Param.mapTryCatch(
1226 * (str) => JSON.parse(str),
1227 * (error) =>
1228 * `Invalid JSON: ${error instanceof Error ? error.message : String(error)}`
1229 * )
1230 * )
1231 *
1232 * const [, value] = await Effect.runPromise(
1233 * parsedJson.parse({
1234 * arguments: [],
1235 * flags: { config: ['{"enabled":true}'] }
1236 * }).pipe(Effect.provide(CliTestLayer))
1237 * )
1238 * value // => { enabled: true }
1239 * ```
1240 *
1241 * @stability unstable
1242 * @category combinators
1243 * @since 4.0.0
1244 */
1245export const mapTryCatch: {
1246 <A, B>(
1247 f: (a: A) => B,
1248 onError: (error: unknown) => string
1249 ): <Kind extends ParamKind>(self: Param<Kind, A>) => Param<Kind, B>
1250 <Kind extends ParamKind, A, B>(
1251 self: Param<Kind, A>,
1252 f: (a: A) => B,
1253 onError: (error: unknown) => string
1254 ): Param<Kind, B>
1255} = dual(3, <Kind extends ParamKind, A, B>(
1256 self: Param<Kind, A>,
1257 f: (a: A) => B,
1258 onError: (error: unknown) => string
1259) => {
1260 const single = getUnderlyingSingleOrThrow(self)
1261
1262 return transform(
1263 self,
1264 (parse: Parse<A>) => (args: ParsedArgs) =>
1265 Effect.flatMap(parse(args), ([leftover, a]) =>
1266 Effect.try({
1267 try: () => f(a),
1268 catch: (error) => onError(error)
1269 }).pipe(
1270 Effect.mapError(
1271 (error) =>
1272 new CliError.InvalidValue({
1273 option: single.name,
1274 value: globalThis.String(a),
1275 expected: error,
1276 kind: single.kind
1277 })
1278 ),
1279 Effect.map((b) => [leftover, b] as const)
1280 ))
1281 )
1282})
1283
1284/**
1285 * Makes a flag or positional argument optional.
1286 *
1287 * **Details**
1288 *
1289 * When the parameter is absent, parsing succeeds with `Option.none()` instead
1290 * of failing with a missing option or missing argument error. When present, the
1291 * parsed value is wrapped in `Option.some()`.
1292 *
1293 * **Example** (Making parameters optional)
1294 *
1295 * ```ts import.meta.vitest
1296 * import { Param } from "effect/cli"
1297 *
1298 * // Create an optional port option
1299 * // - When not provided: returns Option.none()
1300 * // - When provided: returns Option.some(parsedValue)
1301 * const port = Param.optional(Param.Int(Param.flagKind, "port"))
1302 * port.kind // => "flag"
1303 * ```
1304 *
1305 * @stability unstable
1306 * @category combinators
1307 * @since 4.0.0
1308 */
1309export const optional = <Kind extends ParamKind, A>(
1310 param: Param<Kind, A>
1311): Param<Kind, Option.Option<A>> => {
1312 const parse: Parse<Option.Option<A>> = (args) =>
1313 param.parse(args).pipe(
1314 Effect.map(([leftover, value]) => [leftover, Option.some(value)] as const),
1315 // Catch both MissingOption (for flags) and MissingArgument (for positional arguments)
1316 Effect.catchTags({
1317 MissingOption: () => Effect.succeed([args.arguments, Option.none()] as const),
1318 MissingArgument: () => Effect.succeed([args.arguments, Option.none()] as const)
1319 })
1320 )
1321 return Object.assign(Object.create(Proto), {
1322 _tag: "Optional",
1323 kind: param.kind,
1324 param,
1325 parse
1326 })
1327}
1328
1329/**
1330 * Makes a flag or positional argument optional by supplying a fallback value.
1331 *
1332 * **Details**
1333 *
1334 * The fallback may be a pure value or an effect. It is used only when the
1335 * parameter is absent; provided values are parsed normally.
1336 *
1337 * **Example** (Providing default values)
1338 *
1339 * ```ts import.meta.vitest
1340 * import { Param } from "effect/cli"
1341 *
1342 * // Using the pipe operator to make an option optional
1343 * const port = Param.Int(Param.flagKind, "port").pipe(
1344 * Param.withDefault(8080)
1345 * )
1346 *
1347 * // Can also be used with other combinators
1348 * const verbose = Param.Boolean(Param.flagKind, "verbose").pipe(
1349 * Param.withAlias("-v"),
1350 * Param.withDescription("Enable verbose output"),
1351 * Param.withDefault(false)
1352 * )
1353 * const kinds = [port.kind, verbose.kind] // => ["flag", "flag"]
1354 * ```
1355 *
1356 * @stability unstable
1357 * @category combinators
1358 * @since 4.0.0
1359 */
1360export const withDefault: {
1361 <const B>(
1362 defaultValue: B | Effect.Effect<B, CliError.CliError, Environment>
1363 ): <Kind extends ParamKind, A>(self: Param<Kind, A>) => Param<Kind, A | B>
1364 <Kind extends ParamKind, A, const B>(
1365 self: Param<Kind, A>,
1366 defaultValue: B | Effect.Effect<B, CliError.CliError, Environment>
1367 ): Param<Kind, A | B>
1368} = dual(2, <Kind extends ParamKind, A, const B>(
1369 self: Param<Kind, A>,
1370 defaultValue: B | Effect.Effect<B, CliError.CliError, Environment>
1371): Param<Kind, A | B> => {
1372 if (!Effect.isEffect(defaultValue)) {
1373 return map(optional(self), Option.getOrElse(() => defaultValue))
1374 }
1375 return mapEffect(
1376 optional(self as Param<Kind, A | B>),
1377 Option.match({
1378 onNone: () => defaultValue,
1379 onSome: Effect.succeed
1380 })
1381 )
1382})
1383
1384/**
1385 * Adds a fallback config that is loaded when a required parameter is missing.
1386 *
1387 * **When to use**
1388 *
1389 * Use when you need config to provide a fallback source for required flags or
1390 * arguments that are absent from CLI input.
1391 *
1392 * **Details**
1393 *
1394 * Provided CLI values win. Config is loaded only after a missing option or
1395 * missing argument error.
1396 *
1397 * **Gotchas**
1398 *
1399 * Missing config preserves the original missing-parameter error. Config parse
1400 * failure becomes `CliError.InvalidValue`.
1401 *
1402 * @see {@link withDefault} for a pure default value
1403 * @see {@link withFallbackPrompt} for prompting interactively when input is missing
1404 *
1405 * @stability unstable
1406 * @category combinators
1407 * @since 4.0.0
1408 */
1409export const withFallbackConfig: {
1410 <B>(config: Config.Config<B>): <Kind extends ParamKind, A>(self: Param<Kind, A>) => Param<Kind, A | B>
1411 <Kind extends ParamKind, A, B>(self: Param<Kind, A>, config: Config.Config<B>): Param<Kind, A | B>
1412} = dual(2, <Kind extends ParamKind, A, B>(
1413 self: Param<Kind, A>,
1414 config: Config.Config<B>
1415): Param<Kind, A | B> => {
1416 const toInvalidValue = (
1417 error: CliError.MissingOption | CliError.MissingArgument,
1418 configError: Config.ConfigError
1419 ): CliError.InvalidValue =>
1420 new CliError.InvalidValue({
1421 option: error._tag === "MissingOption" ? error.option : error.argument,
1422 value: "config",
1423 expected: configError.message,
1424 kind: error._tag === "MissingOption" ? "flag" : "argument"
1425 })
1426 const runConfig = (error: CliError.MissingOption | CliError.MissingArgument, args: ParsedArgs) =>
1427 Config.option(config).pipe(
1428 Effect.mapError((configError) => toInvalidValue(error, configError)),
1429 Effect.flatMap(Option.match({
1430 onNone: () => Effect.fail(error),
1431 onSome: (value) => Effect.succeed([args.arguments, value as A | B] as const)
1432 }))
1433 )
1434 return transform(
1435 self,
1436 (parse) => (args) =>
1437 parse(args).pipe(
1438 Effect.catchTag(["MissingOption", "MissingArgument"], (error) => runConfig(error, args))
1439 )
1440 )
1441})
1442
1443/**
1444 * Adds a fallback prompt that is shown when a required parameter is missing.
1445 *
1446 * **When to use**
1447 *
1448 * Use when a CLI should ask interactively for a missing required flag or
1449 * argument.
1450 *
1451 * **Details**
1452 *
1453 * `FallbackPrompt` accepts either a `Prompt` or an effect that builds one.
1454 * Effectful prompt creation is lazy and runs only when the fallback is needed.
1455 *
1456 * **Gotchas**
1457 *
1458 * This only handles missing options and missing arguments. Invalid values do not
1459 * prompt, and prompt cancellation re-fails with the original missing error.
1460 *
1461 * @see {@link FallbackPrompt} for accepted fallback prompt forms
1462 * @see {@link withFallbackConfig} for loading a fallback from config
1463 * @see {@link withDefault} for a pure default value
1464 *
1465 * @stability unstable
1466 * @category combinators
1467 * @since 4.0.0
1468 */
1469export const withFallbackPrompt: {
1470 <B>(prompt: FallbackPrompt<B>): <Kind extends ParamKind, A>(self: Param<Kind, A>) => Param<Kind, A | B>
1471 <Kind extends ParamKind, A, B>(self: Param<Kind, A>, prompt: FallbackPrompt<B>): Param<Kind, A | B>
1472} = dual(2, <Kind extends ParamKind, A, B>(
1473 self: Param<Kind, A>,
1474 prompt: FallbackPrompt<B>
1475): Param<Kind, A | B> => {
1476 const runPrompt = (error: CliError.MissingOption | CliError.MissingArgument, args: ParsedArgs) =>
1477 Effect.flatMap(Prompt.isPrompt(prompt) ? Effect.succeed(prompt) : prompt, Prompt.run).pipe(
1478 Effect.map((value) => [args.arguments, value as A | B] as const),
1479 Effect.catchTag("QuitError", () => Effect.fail(error))
1480 )
1481 return transform(
1482 self,
1483 (parse) => (args) =>
1484 parse(args).pipe(
1485 Effect.catchTag(["MissingOption", "MissingArgument"], (error) => runPrompt(error, args))
1486 )
1487 )
1488})
1489
1490/**
1491 * Represent options which can be used to configure variadic parameters.
1492 *
1493 * @stability unstable
1494 * @category options
1495 * @since 4.0.0
1496 */
1497export type VariadicParamOptions = {
1498 /**
1499 * The minimum number of times the parameter can be specified.
1500 */
1501 readonly min?: number | undefined
1502 /**
1503 * The maximum number of times the parameter can be specified.
1504 */
1505 readonly max?: number | undefined
1506}
1507
1508/**
1509 * Creates a variadic parameter that can be specified multiple times.
1510 *
1511 * **Details**
1512 *
1513 * This is the base combinator for creating parameters that accept multiple values.
1514 * The `min` and `max` parameters are optional. When they are not provided, the
1515 * parameter can be specified any number of times, from 0 to infinity.
1516 *
1517 * **Example** (Accepting multiple values)
1518 *
1519 * ```ts import.meta.vitest
1520 * import { Param } from "effect/cli"
1521 *
1522 * // Basic variadic parameter (0 to infinity)
1523 * const tags = Param.variadic(Param.String(Param.flagKind, "tag"))
1524 *
1525 * // Variadic with minimum count
1526 * const inputs = Param.variadic(
1527 * Param.String(Param.flagKind, "input"),
1528 * { min: 1 } // at least 1 required
1529 * )
1530 *
1531 * // Variadic with both min and max
1532 * const limited = Param.variadic(Param.String(Param.flagKind, "item"), {
1533 * min: 2, // at least 2 times
1534 * max: 2 // at most 2 times
1535 * })
1536 * const kinds = [tags.kind, inputs.kind, limited.kind] // => ["flag", "flag", "flag"]
1537 * ```
1538 *
1539 * @stability unstable
1540 * @category combinators
1541 * @since 4.0.0
1542 */
1543export const variadic = <Kind extends ParamKind, A>(
1544 self: Param<Kind, A>,
1545 options?: VariadicParamOptions | undefined
1546): Param<Kind, ReadonlyArray<A>> => {
1547 const single = getUnderlyingSingleOrThrow(self)
1548 const parse: Parse<ReadonlyArray<A>> = (args) => {
1549 if (single.kind === "argument") {
1550 return parsePositionalVariadic(self, single, args, options)
1551 } else {
1552 return parseOptionVariadic(self, single, args, options)
1553 }
1554 }
1555 return Object.assign(Object.create(Proto), {
1556 _tag: "Variadic",
1557 kind: self.kind,
1558 param: self,
1559 min: Option.fromUndefinedOr(options?.min),
1560 max: Option.fromUndefinedOr(options?.max),
1561 parse
1562 })
1563}
1564
1565/**
1566 * Wraps an option to allow it to be specified multiple times within a range.
1567 *
1568 * **Details**
1569 *
1570 * This combinator transforms an option to accept between `min` and `max`
1571 * occurrences on the command line, returning an array of all provided values.
1572 *
1573 * **Example** (Bounding repeated values)
1574 *
1575 * ```ts import.meta.vitest
1576 * import { Param } from "effect/cli"
1577 *
1578 * // Allow 1-3 file inputs
1579 * const files = Param.String(Param.flagKind, "file").pipe(
1580 * Param.between(1, 3),
1581 * Param.withAlias("-f")
1582 * )
1583 *
1584 * // Parse: --file a.txt --file b.txt
1585 * // Result: ["a.txt", "b.txt"]
1586 *
1587 * // Allow 0 or more tags
1588 * const tags = Param.String(Param.flagKind, "tag").pipe(
1589 * Param.between(0, Number.MAX_SAFE_INTEGER)
1590 * )
1591 *
1592 * // Parse: --tag dev --tag staging --tag v1.0
1593 * // Result: ["dev", "staging", "v1.0"]
1594 * const kinds = [files.kind, tags.kind] // => ["flag", "flag"]
1595 * ```
1596 *
1597 * @stability unstable
1598 * @category combinators
1599 * @since 4.0.0
1600 */
1601export const between: {
1602 <A>(min: number, max: number): <Kind extends ParamKind>(self: Param<Kind, A>) => Param<Kind, ReadonlyArray<A>>
1603 <Kind extends ParamKind, A>(self: Param<Kind, A>, min: number, max: number): Param<Kind, ReadonlyArray<A>>
1604} = dual(3, <Kind extends ParamKind, A>(self: Param<Kind, A>, min: number, max: number) => {
1605 if (min < 0) {
1606 throw new Error("between: min must be non-negative")
1607 }
1608 if (max < min) {
1609 throw new Error("between: max must be greater than or equal to min")
1610 }
1611
1612 return variadic(self, { min, max })
1613})
1614
1615/**
1616 * Wraps an option to allow it to be specified at most `max` times.
1617 *
1618 * **Details**
1619 *
1620 * This combinator transforms an option to accept between 0 and `max`
1621 * occurrences on the command line, returning an array of all provided values.
1622 *
1623 * **Example** (Limiting repeated values)
1624 *
1625 * ```ts import.meta.vitest
1626 * import { Param } from "effect/cli"
1627 *
1628 * // Allow at most 3 warning suppressions
1629 * const suppressions = Param.String(Param.flagKind, "suppress").pipe(
1630 * Param.atMost(3)
1631 * )
1632 *
1633 * // Parse: --suppress warning1 --suppress warning2
1634 * // Result: ["warning1", "warning2"]
1635 * suppressions.kind // => "flag"
1636 * ```
1637 *
1638 * @stability unstable
1639 * @category combinators
1640 * @since 4.0.0
1641 */
1642export const atMost: {
1643 <A>(max: number): <Kind extends ParamKind>(self: Param<Kind, A>) => Param<Kind, ReadonlyArray<A>>
1644 <Kind extends ParamKind, A>(self: Param<Kind, A>, max: number): Param<Kind, ReadonlyArray<A>>
1645} = dual(2, <Kind extends ParamKind, A>(self: Param<Kind, A>, max: number) => {
1646 if (max < 0) {
1647 throw new Error("atMost: max must be non-negative")
1648 }
1649 return variadic(self, { max })
1650})
1651
1652/**
1653 * Wraps an option to require it to be specified at least `min` times.
1654 *
1655 * **Details**
1656 *
1657 * This combinator transforms an option to accept at least `min`
1658 * occurrences on the command line, returning an array of all provided values.
1659 *
1660 * **Example** (Requiring repeated values)
1661 *
1662 * ```ts import.meta.vitest
1663 * import { Param } from "effect/cli"
1664 *
1665 * // Require at least 2 input files
1666 * const inputs = Param.String(Param.flagKind, "input").pipe(
1667 * Param.atLeast(2),
1668 * Param.withAlias("-i")
1669 * )
1670 *
1671 * // Parse: --input file1.txt --input file2.txt --input file3.txt
1672 * // Result: ["file1.txt", "file2.txt", "file3.txt"]
1673 * inputs.kind // => "flag"
1674 * ```
1675 *
1676 * @stability unstable
1677 * @category combinators
1678 * @since 4.0.0
1679 */
1680export const atLeast: {
1681 <A>(min: number): <Kind extends ParamKind>(self: Param<Kind, A>) => Param<Kind, ReadonlyArray<A>>
1682 <Kind extends ParamKind, A>(self: Param<Kind, A>, min: number): Param<Kind, ReadonlyArray<A>>
1683} = dual(2, <Kind extends ParamKind, A>(self: Param<Kind, A>, min: number) => {
1684 if (min < 0) {
1685 throw new Error("atLeast: min must be non-negative")
1686 }
1687 return variadic(self, { min })
1688})
1689
1690/**
1691 * Filters and transforms parsed values, failing with a custom error message
1692 * if the filter function returns `Option.none()`.
1693 *
1694 * **When to use**
1695 *
1696 * Use when you need validation and transformation in a single parameter
1697 * combinator.
1698 *
1699 * **Example** (Filtering and transforming values)
1700 *
1701 * ```ts import.meta.vitest
1702 * import { Option } from "effect"
1703 * import { Param } from "effect/cli"
1704 * const positiveInt = Param.Int(Param.flagKind, "count").pipe(
1705 * Param.filterMap(
1706 * (n) => n > 0 ? Option.some(n) : Option.none(),
1707 * (n) => `Expected positive integer, got ${n}`
1708 * )
1709 * )
1710 * positiveInt.kind // => "flag"
1711 * ```
1712 *
1713 * @stability unstable
1714 * @category combinators
1715 * @since 4.0.0
1716 */
1717export const filterMap: {
1718 <A, B>(
1719 filter: (a: A) => Option.Option<B>,
1720 onNone: (a: A) => string
1721 ): <Kind extends ParamKind>(self: Param<Kind, A>) => Param<Kind, B>
1722 <Kind extends ParamKind, A, B>(
1723 self: Param<Kind, A>,
1724 filter: (a: A) => Option.Option<B>,
1725 onNone: (a: A) => string
1726 ): Param<Kind, B>
1727} = dual(3, <Kind extends ParamKind, A, B>(
1728 self: Param<Kind, A>,
1729 filter: (a: A) => Option.Option<B>,
1730 onNone: (a: A) => string
1731) =>
1732 mapEffect(
1733 self,
1734 Effect.fnUntraced(function*(a) {
1735 const result = filter(a)
1736 if (Option.isSome(result)) {
1737 return result.value
1738 }
1739 const single = getUnderlyingSingleOrThrow(self)
1740 return yield* new CliError.InvalidValue({
1741 option: single.name,
1742 value: globalThis.String(a),
1743 expected: onNone(a),
1744 kind: single.kind
1745 })
1746 })
1747 ))
1748
1749/**
1750 * Filters parsed values, failing with a custom error message if the predicate returns false.
1751 *
1752 * **Example** (Filtering parsed values)
1753 *
1754 * ```ts import.meta.vitest
1755 * import { Param } from "effect/cli"
1756 *
1757 * const evenNumber = Param.Int(Param.flagKind, "num").pipe(
1758 * Param.filter(
1759 * (n) => n % 2 === 0,
1760 * (n) => `Expected even number, got ${n}`
1761 * )
1762 * )
1763 * evenNumber.kind // => "flag"
1764 * ```
1765 *
1766 * @stability unstable
1767 * @category combinators
1768 * @since 4.0.0
1769 */
1770export const filter: {
1771 <A>(
1772 predicate: Predicate.Predicate<A>,
1773 onFalse: (a: A) => string
1774 ): <Kind extends ParamKind>(self: Param<Kind, A>) => Param<Kind, A>
1775 <Kind extends ParamKind, A>(
1776 self: Param<Kind, A>,
1777 predicate: Predicate.Predicate<A>,
1778 onFalse: (a: A) => string
1779 ): Param<Kind, A>
1780} = dual(3, <Kind extends ParamKind, A>(
1781 self: Param<Kind, A>,
1782 predicate: Predicate.Predicate<A>,
1783 onFalse: (a: A) => string
1784) => filterMap(self, Option.liftPredicate(predicate), onFalse))
1785
1786/**
1787 * Sets a custom metavar (placeholder name) for the param in help documentation.
1788 *
1789 * **Details**
1790 *
1791 * The metavar is displayed in usage text to indicate what value the user should provide.
1792 * For example, `--output FILE` shows `FILE` as the metavar.
1793 *
1794 * **Example** (Setting metavars)
1795 *
1796 * ```ts import.meta.vitest
1797 * import { Param } from "effect/cli"
1798 *
1799 * const port = Param.Int(Param.flagKind, "port").pipe(
1800 * Param.withMetavar("PORT"),
1801 * Param.filter(
1802 * (p) => p >= 1 && p <= 65535,
1803 * () => "Port must be between 1 and 65535"
1804 * )
1805 * )
1806 * port.kind // => "flag"
1807 * ```
1808 *
1809 * @stability unstable
1810 * @category metadata
1811 * @since 4.0.0
1812 */
1813export const withMetavar: {
1814 <K extends ParamKind>(metavar: string): <A>(self: Param<K, A>) => Param<K, A>
1815 <K extends ParamKind, A>(self: Param<K, A>, metavar: string): Param<K, A>
1816} = dual(2, <K extends ParamKind, A>(
1817 self: Param<K, A>,
1818 metavar: string
1819) =>
1820 transformSingle(self, (single) =>
1821 makeSingle({
1822 ...single,
1823 typeName: metavar
1824 })))
1825
1826/**
1827 * Validates parsed values against a Schema, providing detailed error messages.
1828 *
1829 * **Example** (Validating with schemas)
1830 *
1831 * ```ts import.meta.vitest
1832 * import { Schema } from "effect"
1833 * import { Param } from "effect/cli"
1834 * const isEmail = Schema.isPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)
1835 *
1836 * const Email = Schema.String.pipe(
1837 * Schema.check(isEmail)
1838 * )
1839 *
1840 * const email = Param.String(Param.flagKind, "email").pipe(
1841 * Param.withSchema(Email)
1842 * )
1843 * email.kind // => "flag"
1844 * ```
1845 *
1846 * @stability unstable
1847 * @category combinators
1848 * @since 4.0.0
1849 */
1850export const withSchema: {
1851 <A, B>(
1852 schema: Schema.ConstraintCodec<B, A, Environment, unknown>
1853 ): <Kind extends ParamKind>(
1854 self: Param<Kind, A>
1855 ) => Param<Kind, B>
1856 <Kind extends ParamKind, A, B>(
1857 self: Param<Kind, A>,
1858 schema: Schema.ConstraintCodec<B, A, Environment, unknown>
1859 ): Param<Kind, B>
1860} = dual(2, <Kind extends ParamKind, A, B>(
1861 self: Param<Kind, A>,
1862 schema: Schema.ConstraintCodec<B, A, Environment, unknown>
1863) => {
1864 const decodeParam = Schema.decodeUnknownEffect(schema)
1865 return mapEffect(self, (value) =>
1866 Effect.mapError(decodeParam(value), (error) => {
1867 const single = getUnderlyingSingleOrThrow(self)
1868 return new CliError.InvalidValue({
1869 option: single.name,
1870 value: globalThis.String(value),
1871 expected: `Schema validation failed: ${error.message}`,
1872 kind: single.kind
1873 })
1874 }))
1875})
1876
1877/**
1878 * Provides a fallback param to use if this param fails to parse.
1879 *
1880 * **Example** (Falling back to another parameter)
1881 *
1882 * ```ts import.meta.vitest
1883 * import { Param } from "effect/cli"
1884 *
1885 * const config = Param.File(Param.flagKind, "config").pipe(
1886 * Param.orElse(() => Param.String(Param.flagKind, "config-url"))
1887 * )
1888 * config.kind // => "flag"
1889 * ```
1890 *
1891 * @stability unstable
1892 * @category combinators
1893 * @since 4.0.0
1894 */
1895export const orElse: {
1896 <B, Kind extends ParamKind>(orElse: LazyArg<Param<Kind, B>>): <A>(self: Param<Kind, A>) => Param<Kind, A | B>
1897 <Kind extends ParamKind, A, B>(
1898 self: Param<Kind, A>,
1899 orElse: LazyArg<Param<Kind, B>>
1900 ): Param<Kind, A | B>
1901} = dual(2, <Kind extends ParamKind, A, B>(
1902 self: Param<Kind, A>,
1903 orElse: LazyArg<Param<Kind, B>>
1904) =>
1905 transform(
1906 self,
1907 (parse: Parse<A>, alternatives): Parse<A | B> => (args: ParsedArgs) =>
1908 Effect.catch(parse(args), () => (alternatives[0]!() as Parse<B>)(args)),
1909 [orElse]
1910 ))
1911
1912/**
1913 * Provides a fallback param and returns a `Result` indicating which param
1914 * succeeded.
1915 *
1916 * **Details**
1917 *
1918 * The original param's value is returned as `Result.succeed`, while the
1919 * fallback param's value is returned as `Result.fail`.
1920 *
1921 * **Example** (Returning fallback results)
1922 *
1923 * ```ts import.meta.vitest
1924 * import { Param } from "effect/cli"
1925 *
1926 * const configSource = Param.File(Param.flagKind, "config").pipe(
1927 * Param.orElseResult(() => Param.String(Param.flagKind, "config-url"))
1928 * )
1929 * // Returns Result<string, string>
1930 * configSource.kind // => "flag"
1931 * ```
1932 *
1933 * @stability unstable
1934 * @category combinators
1935 * @since 4.0.0
1936 */
1937export const orElseResult: {
1938 <Kind extends ParamKind, B>(
1939 orElse: LazyArg<Param<Kind, B>>
1940 ): <A>(self: Param<Kind, A>) => Param<Kind, Result.Result<A, B>>
1941 <Kind extends ParamKind, A, B>(
1942 self: Param<Kind, A>,
1943 orElse: LazyArg<Param<Kind, B>>
1944 ): Param<Kind, Result.Result<A, B>>
1945} = dual(2, <Kind extends ParamKind, A, B>(
1946 self: Param<Kind, A>,
1947 orElse: LazyArg<Param<Kind, B>>
1948) => {
1949 return transform(
1950 self,
1951 (parse: Parse<A>, alternatives): Parse<Result.Result<A, B>> => (args: ParsedArgs) =>
1952 Effect.catch(
1953 Effect.map(parse(args), ([leftover, value]) => [leftover, Result.succeed(value)] as const),
1954 () =>
1955 Effect.map(
1956 (alternatives[0]!() as Parse<B>)(args),
1957 ([leftover, value]) => [leftover, Result.fail(value)] as const
1958 )
1959 ),
1960 [orElse]
1961 )
1962})
1963
1964// =============================================================================
1965// Parsing Utilities
1966// =============================================================================
1967
1968const parsePositional: <A>(
1969 name: string,
1970 primitiveType: Primitive.Primitive<A>,
1971 args: ParsedArgs
1972) => Effect.Effect<
1973 readonly [leftover: ReadonlyArray<string>, value: A],
1974 CliError.CliError,
1975 Environment
1976> = Effect.fnUntraced(function*(name, primitiveType, args) {
1977 if (args.arguments.length === 0) {
1978 return yield* new CliError.MissingArgument({ argument: name })
1979 }
1980
1981 const arg = args.arguments[0]
1982 const value = yield* Effect.mapError(
1983 primitiveType.parse(arg),
1984 (error) =>
1985 new CliError.InvalidValue({
1986 option: name,
1987 value: arg,
1988 expected: error,
1989 kind: "argument"
1990 })
1991 )
1992
1993 return [args.arguments.slice(1), value] as const
1994})
1995
1996const parseFlag: <A>(
1997 name: string,
1998 primitiveType: Primitive.Primitive<A>,
1999 args: ParsedArgs
2000) => Effect.Effect<
2001 readonly [remainingOperands: ReadonlyArray<string>, value: A],
2002 CliError.CliError,
2003 Environment
2004> = Effect.fnUntraced(function*(name, primitiveType, args) {
2005 const providedValues = args.flags[name]
2006
2007 if (providedValues === undefined || providedValues.length === 0) {
2008 return yield* new CliError.MissingOption({ option: name })
2009 }
2010
2011 // Parse the first value (later we can handle multiple)
2012 const arg = providedValues[0]
2013 const value = yield* Effect.mapError(
2014 primitiveType.parse(arg),
2015 (error) =>
2016 new CliError.InvalidValue({
2017 option: name,
2018 value: arg,
2019 expected: error,
2020 kind: "flag"
2021 })
2022 )
2023
2024 return [args.arguments, value] as const
2025})
2026
2027const parsePositionalVariadic: <Kind extends ParamKind, A>(
2028 self: Param<Kind, A>,
2029 single: Single<Kind, A>,
2030 args: ParsedArgs,
2031 options?: VariadicParamOptions | undefined
2032) => Effect.Effect<
2033 readonly [remainingOperands: ReadonlyArray<string>, value: ReadonlyArray<A>],
2034 CliError.CliError,
2035 Environment
2036> = Effect.fnUntraced(function*<A, Kind extends ParamKind>(
2037 self: Param<Kind, A>,
2038 single: Single<Kind, A>,
2039 args: ParsedArgs,
2040 options?: VariadicParamOptions | undefined
2041) {
2042 const results: Array<A> = []
2043 const minValue = options?.min ?? 0
2044 const maxValue = options?.max ?? Number.POSITIVE_INFINITY
2045
2046 let count = 0
2047 let currentArgs = args.arguments
2048 while (currentArgs.length > 0 && count < maxValue) {
2049 const [remainingArgs, value] = yield* self.parse({
2050 flags: args.flags,
2051 arguments: currentArgs
2052 })
2053 results.push(value)
2054 currentArgs = remainingArgs
2055 count++
2056 }
2057
2058 if (count < minValue) {
2059 return yield* count === 0
2060 ? new CliError.MissingArgument({ argument: single.name })
2061 : new CliError.InvalidValue({
2062 option: single.name,
2063 value: `${count} values`,
2064 expected: `at least ${minValue} value${minValue === 1 ? "" : "s"}`,
2065 kind: single.kind
2066 })
2067 }
2068
2069 return [currentArgs, results] as const
2070})
2071
2072const parseOptionVariadic: <Kind extends ParamKind, A>(
2073 self: Param<Kind, A>,
2074 single: Single<Kind, A>,
2075 args: ParsedArgs,
2076 options?: VariadicParamOptions | undefined
2077) => Effect.Effect<
2078 readonly [remainingOperands: ReadonlyArray<string>, value: ReadonlyArray<A>],
2079 CliError.CliError,
2080 Environment
2081> = Effect.fnUntraced(function*<A, Kind extends ParamKind>(
2082 self: Param<Kind, A>,
2083 single: Single<Kind, A>,
2084 args: ParsedArgs,
2085 options?: VariadicParamOptions | undefined
2086) {
2087 const results: Array<A> = []
2088 const names = [single.name, ...single.aliases]
2089 const values = names.flatMap((name) => args.flags[name] ?? [])
2090 const count = values.length
2091
2092 // Validate count constraints
2093 if (Predicate.isNotUndefined(options?.min) && count < options.min) {
2094 return yield* count === 0
2095 ? new CliError.MissingOption({ option: single.name })
2096 : new CliError.InvalidValue({
2097 option: single.name,
2098 value: `${count} occurrences`,
2099 expected: `at least ${options.min} value${options.min === 1 ? "" : "s"}`,
2100 kind: single.kind
2101 })
2102 }
2103
2104 if (Predicate.isNotUndefined(options?.max) && count > options.max) {
2105 return yield* new CliError.InvalidValue({
2106 option: single.name,
2107 value: `${count} occurrences`,
2108 expected: `at most ${options.max} value${options.max === 1 ? "" : "s"}`,
2109 kind: single.kind
2110 })
2111 }
2112
2113 // Parse each value individually
2114 for (const value of values) {
2115 const [, parsedValue] = yield* self.parse({
2116 flags: { [single.name]: [value] },
2117 arguments: []
2118 })
2119 results.push(parsedValue)
2120 }
2121
2122 return [args.arguments, results] as const
2123})
2124
2125type AnyParam<Kind extends ParamKind, A> =
2126 | Single<Kind, A>
2127 | Map<Kind, any, A>
2128 | Transform<Kind, any, A>
2129 | Optional<Kind, A>
2130 | Variadic<Kind, A>
2131
2132/**
2133 * Type-safe param matcher that handles the unsafe casting internally.
2134 * This provides a clean API for pattern matching on param types while
2135 * maintaining type safety at the call site.
2136 */
2137const matchParam = <Kind extends ParamKind, A, R>(
2138 param: Param<Kind, A>,
2139 patterns: {
2140 Single: (single: Single<Kind, A>) => R
2141 Map: <X>(mapped: Map<Kind, X, A>) => R
2142 Transform: <X>(mapped: Transform<Kind, X, A>) => R
2143 Optional: <X>(optional: Optional<Kind, X>) => R
2144 Variadic: <X>(variadic: Variadic<Kind, X>) => R
2145 }
2146): R => {
2147 const p = param as AnyParam<Kind, A>
2148 switch (p._tag) {
2149 case "Single":
2150 return patterns.Single(p)
2151 case "Map":
2152 return patterns.Map(p)
2153 case "Transform":
2154 return patterns.Transform(p)
2155 case "Optional":
2156 return patterns.Optional(p)
2157 case "Variadic":
2158 return patterns.Variadic(p)
2159 }
2160}
2161
2162/**
2163 * Recursively transforms a param by applying a function to any `Single` nodes.
2164 * This is used internally by combinators like `withAlias` to traverse the param tree.
2165 */
2166const transformSingle = <Kind extends ParamKind, A>(
2167 param: Param<Kind, A>,
2168 f: <X>(single: Single<Kind, X>) => Single<Kind, X>
2169): Param<Kind, A> => {
2170 return matchParam(param, {
2171 Single: (single) => f(single),
2172 Map: (mapped) => map(transformSingle(mapped.param, f), mapped.f),
2173 Transform: (mapped) =>
2174 transform(
2175 transformSingle(mapped.param, f),
2176 mapped.f,
2177 mapped.alternatives.map((alternative) => () => transformSingle(alternative(), f))
2178 ),
2179 Optional: (p) => optional(transformSingle(p.param, f)) as Param<Kind, A>,
2180 Variadic: (p) =>
2181 variadic(transformSingle(p.param, f), {
2182 min: Option.getOrUndefined(p.min),
2183 max: Option.getOrUndefined(p.max)
2184 }) as Param<Kind, A>
2185 })
2186}
2187
2188/**
2189 * Extracts all Single params from a potentially nested param structure.
2190 * This handles all param combinators including Map, Transform, Optional, and Variadic.
2191 *
2192 * @internal
2193 */
2194export const extractSingleParams = <Kind extends ParamKind, A>(
2195 param: Param<Kind, A>
2196): Array<Single<Kind, unknown>> => {
2197 return matchParam(param, {
2198 Single: (single) => [single as Single<Kind, unknown>],
2199 Map: (mapped) => extractSingleParams(mapped.param),
2200 Transform: (mapped) => [
2201 ...extractSingleParams(mapped.param),
2202 ...mapped.alternatives.flatMap((alternative) => extractSingleParams(alternative()))
2203 ],
2204 Optional: (optional) => extractSingleParams(optional.param),
2205 Variadic: (variadic) => extractSingleParams(variadic.param)
2206 })
2207}
2208
2209/**
2210 * Gets the underlying Single param from a potentially nested param structure.
2211 * Throws an error if there are no singles or multiple singles found.
2212 *
2213 * @internal
2214 */
2215export const getUnderlyingSingleOrThrow = <Kind extends ParamKind, A>(
2216 param: Param<Kind, A>
2217): Single<Kind, A> => {
2218 const singles = extractSingleParams(param)
2219
2220 if (singles.length === 0) {
2221 throw new Error("No Single param found in param structure")
2222 }
2223
2224 if (singles.length > 1) {
2225 throw new Error(
2226 `Multiple Single params found: ${singles.map((s) => s.name).join(", ")}`
2227 )
2228 }
2229
2230 return singles[0] as Single<Kind, A>
2231}
2232
2233/**
2234 * Gets param metadata by traversing the structure.
2235 *
2236 * @internal
2237 */
2238export const getParamMetadata = <Kind extends ParamKind, A>(
2239 param: Param<Kind, A>
2240): {
2241 readonly isOptional: boolean
2242 readonly isVariadic: boolean
2243 readonly variadicMin: Option.Option<number>
2244 readonly variadicMax: Option.Option<number>
2245} => {
2246 return matchParam(param, {
2247 Single: () => ({
2248 isOptional: false,
2249 isVariadic: false,
2250 variadicMin: Option.none(),
2251 variadicMax: Option.none()
2252 }),
2253 Map: (mapped) => getParamMetadata(mapped.param),
2254 Transform: (mapped) => getParamMetadata(mapped.param),
2255 Optional: (optional) => ({
2256 ...getParamMetadata(optional.param),
2257 isOptional: true
2258 }),
2259 Variadic: (variadic) => ({
2260 ...getParamMetadata(variadic.param),
2261 isVariadic: true,
2262 variadicMin: variadic.min,
2263 variadicMax: variadic.max
2264 })
2265 })
2266}