1/**
2 * Main building block for defining and running Effect-based command-line
3 * applications.
4 *
5 * A `Command` combines a name, typed flags and positional arguments, optional
6 * subcommands, help metadata, and an effectful handler. The module includes
7 * builders for command trees and the runners that parse command-line input,
8 * handle built-in help and version behavior, render help through `CliOutput`,
9 * and execute the selected handler.
10 *
11 * @stability unstable
12 * @since 4.0.0
13 */
14import type { NonEmptyArray, NonEmptyReadonlyArray } from "../Array.ts"
15import * as Console from "../Console.ts"
16import * as Context from "../Context.ts"
17import * as Effect from "../Effect.ts"
18import type * as FileSystem from "../FileSystem.ts"
19import { dual } from "../Function.ts"
20import type * as Layer from "../Layer.ts"
21import * as Option from "../Option.ts"
22import type * as Path from "../Path.ts"
23import * as Predicate from "../Predicate.ts"
24import type { ChildProcessSpawner } from "../process/ChildProcessSpawner.ts"
25import * as References from "../References.ts"
26import * as Result from "../Result.ts"
27import * as Runtime from "../Runtime.ts"
28import * as Stdio from "../Stdio.ts"
29import * as Terminal from "../Terminal.ts"
30import type { Contravariant, Covariant, NoInfer, Simplify } from "../Types.ts"
31import * as CliConfig from "./CliConfig.ts"
32import * as CliError from "./CliError.ts"
33import * as CliOutput from "./CliOutput.ts"
34import * as GlobalFlag from "./GlobalFlag.ts"
35import { checkForDuplicateFlags, makeCommand, makeParser, toImpl, TypeId } from "./internal/command.ts"
36import { mergeConfig, parseConfig } from "./internal/config.ts"
37import { getGlobalFlagsForCommandPath, getGlobalFlagsForCommandTree, getHelpForCommandPath } from "./internal/help.ts"
38import * as Lexer from "./internal/lexer.ts"
39import * as Parser from "./internal/parser.ts"
40import * as Wizard from "./internal/wizard.ts"
41import * as Param from "./Param.ts"
42import * as Prompt from "./Prompt.ts"
43
44/* ========================================================================== */
45/* Public Types */
46/* ========================================================================== */
47
48/**
49 * Represents a CLI command with its configuration, handler, and metadata.
50 *
51 * **Details**
52 *
53 * Commands are the core building blocks of CLI applications. They define:
54 *
55 * - The command name and description
56 * - Configuration including flags and arguments
57 * - Handler function for execution
58 * - Optional subcommands for hierarchical structures
59 *
60 * **Example** (Defining CLI commands)
61 *
62 * ```ts import.meta.vitest
63 * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect"
64 * import { Argument, Command, Flag } from "effect/cli"
65 * import { ChildProcessSpawner } from "effect/process"
66 *
67 * const CliTestLayer = Layer.mergeAll(
68 * FileSystem.layerNoop({}),
69 * Path.layer,
70 * Stdio.layerTest({}),
71 * Layer.succeed(Terminal.Terminal, Terminal.make({
72 * columns: Effect.succeed(80),
73 * rows: Effect.succeed(24),
74 * readInput: Effect.die("unused"),
75 * readLine: Effect.die("unused"),
76 * display: () => Effect.void
77 * })),
78 * Layer.succeed(
79 * ChildProcessSpawner.ChildProcessSpawner,
80 * ChildProcessSpawner.make(() => Effect.die("unused"))
81 * )
82 * )
83 *
84 * // Simple command with no configuration
85 * const version: Command.Command<"version", {}, {}, never, never> = Command.make(
86 * "version"
87 * )
88 *
89 * // Command with flags and arguments
90 * const deploy: Command.Command<
91 * "deploy",
92 * {
93 * readonly env: string
94 * readonly force: boolean
95 * readonly files: ReadonlyArray<string>
96 * },
97 * {},
98 * never,
99 * never
100 * > = Command.make("deploy", {
101 * env: Flag.String("env"),
102 * force: Flag.Boolean("force").pipe(Flag.withDefault(false)),
103 * files: Argument.String("files").pipe(Argument.variadic())
104 * })
105 *
106 * // Command with handler
107 * const output: Array<string> = []
108 * const greet = Command.make("greet", {
109 * name: Flag.String("name")
110 * }, (config) => Effect.sync(() => output.push(`Hello, ${config.name}!`)).pipe(Effect.asVoid))
111 *
112 * await Effect.runPromise(
113 * Command.runWith(greet, { version: "1.0.0" })(["--name", "Alice"]).pipe(Effect.provide(CliTestLayer))
114 * )
115 * output // => ["Hello, Alice!"]
116 * ```
117 *
118 * @stability unstable
119 * @category models
120 * @since 4.0.0
121 */
122export interface Command<in out Name extends string, in Input, out ContextInput = {}, out E = never, out R = never>
123 extends
124 Effect.Effect<
125 ContextInput,
126 never,
127 CommandContext<Name>
128 >
129{
130 readonly [TypeId]: Command.Variance<Input, E, R>
131
132 /**
133 * The name of the command.
134 */
135 readonly name: Name
136
137 /**
138 * An optional description of the command.
139 */
140 readonly description: string | undefined
141
142 /**
143 * An optional short description used when listing subcommands.
144 */
145 readonly shortDescription: string | undefined
146
147 /**
148 * An optional alias that can be used as a shorter command name.
149 */
150 readonly alias: string | undefined
151
152 /**
153 * Optional usage examples for the command.
154 */
155 readonly examples: ReadonlyArray<Command.Example>
156
157 /**
158 * The subcommands available under this command.
159 */
160 readonly subcommands: ReadonlyArray<{
161 readonly group: string | undefined
162 readonly commands: NonEmptyReadonlyArray<Command.Any>
163 }>
164
165 /**
166 * Custom annotations associated with this command.
167 */
168 readonly annotations: Context.Context<never>
169
170 /**
171 * Whether this command is omitted from parent help output, shell
172 * completions, and unknown-subcommand suggestions. Unlisted commands still
173 * parse and execute normally when invoked by exact name.
174 */
175 readonly unlisted: boolean
176}
177
178/**
179 * Companion namespace containing type-level helpers and configuration shapes
180 * used by `Command`.
181 *
182 * @stability unstable
183 * @since 4.0.0
184 */
185export declare namespace Command {
186 /**
187 * Type-level variance marker for `Command`.
188 *
189 * **Details**
190 *
191 * The parsed input type is contravariant, while the command error and service
192 * requirement types are covariant.
193 *
194 * @stability unstable
195 * @category models
196 * @since 4.0.0
197 */
198 export interface Variance<in Input, out E, out R> {
199 readonly Input: Contravariant<Input>
200 readonly E: Covariant<E>
201 readonly R: Covariant<R>
202 }
203
204 /**
205 * Represents a concrete usage example for a command.
206 *
207 * @stability unstable
208 * @category models
209 * @since 4.0.0
210 */
211 export interface Example {
212 readonly command: string
213 readonly description?: string | undefined
214 }
215
216 /**
217 * Configuration object for defining command flags, arguments, and nested structures.
218 *
219 * **Details**
220 *
221 * `Command.Config` can contain individual flags and arguments using `Param`
222 * types, nested configuration objects for organization, and arrays of
223 * parameters for repeated elements.
224 *
225 * **Example** (Configuring command input)
226 *
227 * ```ts import.meta.vitest
228 * import { Argument, Flag } from "effect/cli"
229 * import type { Command as CliCommand } from "effect/cli"
230 *
231 * // Simple flat configuration
232 * const simpleConfig = {
233 * name: Flag.String("name"),
234 * age: Flag.Int("age"),
235 * file: Argument.String("file")
236 * } satisfies CliCommand.Command.Config
237 *
238 * // Nested configuration for organization
239 * const nestedConfig = {
240 * user: {
241 * name: Flag.String("name"),
242 * email: Flag.String("email")
243 * },
244 * server: {
245 * host: Flag.String("host"),
246 * port: Flag.Int("port")
247 * }
248 * } satisfies CliCommand.Command.Config
249 *
250 * [simpleConfig.name.kind, nestedConfig.server.port.kind] // => ["flag", "flag"]
251 * ```
252 *
253 * @stability unstable
254 * @category models
255 * @since 4.0.0
256 */
257 export interface Config {
258 readonly [key: string]:
259 | Param.Param<Param.ParamKind, any>
260 | ReadonlyArray<Param.Param<Param.ParamKind, any> | Config>
261 | Config
262 }
263
264 /**
265 * Configuration shape accepted by `Command.withSharedFlags`.
266 *
267 * **Details**
268 *
269 * Only flags are allowed here; arguments are intentionally excluded.
270 *
271 * @stability unstable
272 * @category models
273 * @since 4.0.0
274 */
275 export interface FlagConfig {
276 readonly [key: string]:
277 | Param.Param<typeof Param.flagKind, any>
278 | ReadonlyArray<Param.Param<typeof Param.flagKind, any> | FlagConfig>
279 | FlagConfig
280 }
281
282 /**
283 * Utilities for working with command configurations.
284 *
285 * @stability unstable
286 * @since 4.0.0
287 */
288 export namespace Config {
289 /**
290 * Infers the TypeScript type from a Command.Config structure.
291 *
292 * **Details**
293 *
294 * This type utility extracts the final configuration type that handlers will receive,
295 * preserving the nested structure while converting Param types to their values.
296 *
297 * **Example** (Inferring command input)
298 *
299 * ```ts import.meta.vitest
300 * import { Flag } from "effect/cli"
301 * import type { Command as CliCommand } from "effect/cli"
302 *
303 * const config = {
304 * name: Flag.String("name"),
305 * server: {
306 * host: Flag.String("host"),
307 * port: Flag.Int("port")
308 * }
309 * } as const
310 *
311 * type Result = CliCommand.Command.Config.Infer<typeof config>
312 * // {
313 * // readonly name: string
314 * // readonly server: {
315 * // readonly host: string
316 * // readonly port: number
317 * // }
318 * // }
319 *
320 * const inferred: Result = {
321 * name: "Alice",
322 * server: { host: "localhost", port: 8080 }
323 * }
324 * inferred // => { name: "Alice", server: { host: "localhost", port: 8080 } }
325 * ```
326 *
327 * @stability unstable
328 * @category models
329 * @since 4.0.0
330 */
331 export type Infer<A extends Config> = Simplify<
332 { readonly [Key in keyof A]: InferValue<A[Key]> }
333 >
334
335 /**
336 * Helper type utility for recursively inferring types from Config values.
337 *
338 * @stability unstable
339 * @category models
340 * @since 4.0.0
341 */
342 export type InferValue<A> = A extends ReadonlyArray<any> ? { readonly [Key in keyof A]: InferValue<A[Key]> }
343 : A extends Param.Param<infer _Kind, infer _Value> ? _Value
344 : A extends Config ? Infer<A>
345 : never
346 }
347
348 /**
349 * Represents any Command regardless of its type parameters.
350 *
351 * @stability unstable
352 * @category models
353 * @since 4.0.0
354 */
355 export interface Any extends Effect.Effect<any, never, any> {
356 readonly [TypeId]: any
357 readonly name: string
358 readonly description: string | undefined
359 readonly shortDescription: string | undefined
360 readonly alias: string | undefined
361 readonly examples: ReadonlyArray<Command.Example>
362 readonly subcommands: ReadonlyArray<{
363 readonly group: string | undefined
364 readonly commands: NonEmptyReadonlyArray<Command.Any>
365 }>
366 readonly annotations: Context.Context<never>
367 readonly unlisted: boolean
368 }
369
370 /**
371 * A grouped set of subcommands used by `Command.withSubcommands`.
372 *
373 * @stability unstable
374 * @category models
375 * @since 4.0.0
376 */
377 export interface SubcommandGroup<Commands extends ReadonlyArray<Any> = ReadonlyArray<Any>> {
378 readonly group: string
379 readonly commands: Commands
380 }
381
382 /**
383 * Entry type accepted by `Command.withSubcommands`.
384 *
385 * @stability unstable
386 * @category models
387 * @since 4.0.0
388 */
389 export type SubcommandEntry = Any | SubcommandGroup<ReadonlyArray<Any>>
390}
391
392/**
393 * Services required by CLI parsing and execution.
394 *
395 * **Details**
396 *
397 * This includes file-system and path services for arguments, terminal and
398 * stdio services for running commands, and child-process spawning for
399 * process-related CLI features.
400 *
401 * @stability unstable
402 * @category utility types
403 * @since 4.0.0
404 */
405export type Environment = FileSystem.FileSystem | Path.Path | Terminal.Terminal | ChildProcessSpawner | Stdio.Stdio
406
407/**
408 * A utility type to extract the error type from a `Command`.
409 *
410 * @stability unstable
411 * @category utility types
412 * @since 4.0.0
413 */
414export type Error<C> = C extends Command<
415 infer _Name,
416 infer _Input,
417 infer _ContextInput,
418 infer _Error,
419 infer _Requirements
420> ? _Error :
421 never
422
423/**
424 * A utility type to extract the required services type from a `Command`.
425 *
426 * @stability unstable
427 * @category utility types
428 * @since 4.0.0
429 */
430export type Services<C> = C extends Command<
431 infer _Name,
432 infer _Input,
433 infer _ContextInput,
434 infer _Error,
435 infer _Requirements
436> ? _Requirements :
437 never
438
439/**
440 * Service context for a specific command, enabling subcommands to access their parent's parsed configuration.
441 *
442 * **Details**
443 *
444 * When a subcommand handler needs access to flags or arguments from a parent command,
445 * it can yield the parent command directly to retrieve its config. This is powered by
446 * Effect's service system - each command automatically creates a service that provides
447 * its parsed input to child commands.
448 *
449 * **Example** (Accessing parent command context)
450 *
451 * ```ts import.meta.vitest
452 * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect"
453 * import { Command, Flag } from "effect/cli"
454 * import { ChildProcessSpawner } from "effect/process"
455 *
456 * const CliTestLayer = Layer.mergeAll(
457 * FileSystem.layerNoop({}),
458 * Path.layer,
459 * Stdio.layerTest({}),
460 * Layer.succeed(Terminal.Terminal, Terminal.make({
461 * columns: Effect.succeed(80),
462 * rows: Effect.succeed(24),
463 * readInput: Effect.die("unused"),
464 * readLine: Effect.die("unused"),
465 * display: () => Effect.void
466 * })),
467 * Layer.succeed(
468 * ChildProcessSpawner.ChildProcessSpawner,
469 * ChildProcessSpawner.make(() => Effect.die("unused"))
470 * )
471 * )
472 *
473 * const parent = Command.make("app").pipe(
474 * Command.withSharedFlags({
475 * verbose: Flag.Boolean("verbose").pipe(Flag.withDefault(false)),
476 * config: Flag.String("config")
477 * })
478 * )
479 *
480 * const output: Array<string> = []
481 * const child = Command.make("deploy", {
482 * target: Flag.String("target")
483 * }, (config) =>
484 * Effect.gen(function*() {
485 * // Access parent's config by yielding the parent command
486 * const parentConfig = yield* parent
487 * yield* Effect.sync(() => output.push(`Verbose: ${parentConfig.verbose}`))
488 * yield* Effect.sync(() => output.push(`Config: ${parentConfig.config}`))
489 * yield* Effect.sync(() => output.push(`Target: ${config.target}`))
490 * }))
491 *
492 * const app = parent.pipe(Command.withSubcommands([child]))
493 *
494 * await Effect.runPromise(
495 * Command.runWith(app, { version: "1.0.0" })([
496 * "--verbose",
497 * "--config",
498 * "prod.json",
499 * "deploy",
500 * "--target",
501 * "staging"
502 * ]).pipe(Effect.provide(CliTestLayer))
503 * )
504 * output // => ["Verbose: true", "Config: prod.json", "Target: staging"]
505 * ```
506 *
507 * @stability unstable
508 * @category models
509 * @since 4.0.0
510 */
511export interface CommandContext<Name extends string> {
512 readonly _: unique symbol
513 readonly name: Name
514}
515
516/**
517 * Represents the parsed tokens from command-line input before validation.
518 *
519 * @stability unstable
520 * @category models
521 * @since 4.0.0
522 */
523export interface ParsedTokens {
524 readonly flags: Record<string, ReadonlyArray<string>>
525 readonly arguments: ReadonlyArray<string>
526 readonly errors?: ReadonlyArray<CliError.NonShowHelpErrors>
527 readonly subcommand: Option.Option<{
528 readonly name: string
529 readonly parsedInput: ParsedTokens
530 }>
531}
532
533/**
534 * Returns `true` if the provided value is a `Command`.
535 *
536 * **Gotchas**
537 *
538 * This checks for the `Command` type-id property; it does not validate the full
539 * command shape.
540 *
541 * @stability unstable
542 * @category guards
543 * @since 4.0.0
544 */
545export const isCommand = (u: unknown): u is Command.Any => Predicate.hasProperty(u, TypeId)
546
547/* ========================================================================== */
548/* Constructors */
549/* ========================================================================== */
550
551/**
552 * Creates a `Command` from a name, an optional configuration, and an optional
553 * handler.
554 *
555 * **Details**
556 *
557 * Use `withDescription` and related metadata combinators to add help text. The
558 * overloads support simple commands, configured commands, and commands with
559 * effectful handlers.
560 *
561 * **Example** (Creating commands)
562 *
563 * ```ts import.meta.vitest
564 * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect"
565 * import { Argument, Command, Flag } from "effect/cli"
566 * import { ChildProcessSpawner } from "effect/process"
567 *
568 * const CliTestLayer = Layer.mergeAll(
569 * FileSystem.layerNoop({}),
570 * Path.layer,
571 * Stdio.layerTest({}),
572 * Layer.succeed(Terminal.Terminal, Terminal.make({
573 * columns: Effect.succeed(80),
574 * rows: Effect.succeed(24),
575 * readInput: Effect.die("unused"),
576 * readLine: Effect.die("unused"),
577 * display: () => Effect.void
578 * })),
579 * Layer.succeed(
580 * ChildProcessSpawner.ChildProcessSpawner,
581 * ChildProcessSpawner.make(() => Effect.die("unused"))
582 * )
583 * )
584 *
585 * // Simple command with no configuration
586 * const version = Command.make("version")
587 *
588 * // Command with simple flags
589 * const greet = Command.make("greet", {
590 * name: Flag.String("name"),
591 * count: Flag.Int("count").pipe(Flag.withDefault(1))
592 * })
593 *
594 * // Command with nested configuration
595 * const deploy = Command.make("deploy", {
596 * environment: Flag.String("env").pipe(
597 * Flag.withDescription("Target environment")
598 * ),
599 * server: {
600 * host: Flag.String("host").pipe(Flag.withDefault("localhost")),
601 * port: Flag.Int("port").pipe(Flag.withDefault(3000))
602 * },
603 * files: Argument.String("files").pipe(Argument.variadic),
604 * force: Flag.Boolean("force").pipe(
605 * Flag.withDescription("Force deployment"),
606 * Flag.withDefault(false)
607 * )
608 * })
609 *
610 * // Command with handler
611 * const output: Array<string> = []
612 * const deployWithHandler = Command.make("deploy", {
613 * environment: Flag.String("env"),
614 * force: Flag.Boolean("force").pipe(Flag.withDefault(false))
615 * }, (config) =>
616 * Effect.gen(function*() {
617 * yield* Effect.sync(() => output.push(`Starting deployment to ${config.environment}`))
618 *
619 * if (!config.force && config.environment === "production") {
620 * return yield* Effect.fail("Production deployments require --force flag")
621 * }
622 *
623 * yield* Effect.sync(() => output.push("Deployment completed successfully"))
624 * }))
625 *
626 * await Effect.runPromise(
627 * Command.runWith(deployWithHandler, { version: "1.0.0" })([
628 * "--env",
629 * "staging",
630 * "--force"
631 * ]).pipe(Effect.provide(CliTestLayer))
632 * )
633 * output // => ["Starting deployment to staging", "Deployment completed successfully"]
634 * ```
635 *
636 * @stability unstable
637 * @category constructors
638 * @since 4.0.0
639 */
640export const make: {
641 <Name extends string>(name: Name): Command<Name, {}, {}, never, never>
642
643 <Name extends string, const Config extends Command.Config>(
644 name: Name,
645 config: Config
646 ): Command<Name, Command.Config.Infer<Config>, {}, never, never>
647
648 <Name extends string, const Config extends Command.Config, R, E>(
649 name: Name,
650 config: Config,
651 handler: (config: Command.Config.Infer<Config>) => Effect.Effect<void, E, R>
652 ): Command<Name, Command.Config.Infer<Config>, {}, E, Exclude<R, BuiltInSettingContext>>
653} = ((
654 name: string,
655 config?: Command.Config,
656 handler?: (config: unknown) => Effect.Effect<void, unknown, unknown>
657) => {
658 const parsedConfig = parseConfig(config ?? {})
659 return makeCommand({
660 name,
661 config: parsedConfig,
662 ...(Predicate.isNotUndefined(handler) ? { handle: handler } : {})
663 })
664}) as any
665
666/* ========================================================================== */
667/* Combinators */
668/* ========================================================================== */
669
670/**
671 * Adds or replaces the handler for a command.
672 *
673 * **Example** (Adding command handlers)
674 *
675 * ```ts import.meta.vitest
676 * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect"
677 * import { Command, Flag } from "effect/cli"
678 * import { ChildProcessSpawner } from "effect/process"
679 *
680 * const CliTestLayer = Layer.mergeAll(
681 * FileSystem.layerNoop({}),
682 * Path.layer,
683 * Stdio.layerTest({}),
684 * Layer.succeed(Terminal.Terminal, Terminal.make({
685 * columns: Effect.succeed(80),
686 * rows: Effect.succeed(24),
687 * readInput: Effect.die("unused"),
688 * readLine: Effect.die("unused"),
689 * display: () => Effect.void
690 * })),
691 * Layer.succeed(
692 * ChildProcessSpawner.ChildProcessSpawner,
693 * ChildProcessSpawner.make(() => Effect.die("unused"))
694 * )
695 * )
696 *
697 * // Command without initial handler
698 * const greet = Command.make("greet", {
699 * name: Flag.String("name")
700 * })
701 *
702 * // Add handler later
703 * const output: Array<string> = []
704 * const greetWithHandler = greet.pipe(
705 * Command.withHandler((config: { readonly name: string }) =>
706 * Effect.sync(() => output.push(`Hello, ${config.name}!`)).pipe(Effect.asVoid)
707 * )
708 * )
709 *
710 * await Effect.runPromise(
711 * Command.runWith(greetWithHandler, { version: "1.0.0" })(["--name", "Alice"]).pipe(
712 * Effect.provide(CliTestLayer)
713 * )
714 * )
715 * output // => ["Hello, Alice!"]
716 * ```
717 *
718 * @stability unstable
719 * @category combinators
720 * @since 4.0.0
721 */
722export const withHandler: {
723 <A, R, E>(
724 handler: (value: A) => Effect.Effect<void, E, R>
725 ): <Name extends string, XR, XE, ContextInput>(
726 self: Command<Name, A, ContextInput, XE, XR>
727 ) => Command<Name, A, ContextInput, E, Exclude<R, BuiltInSettingContext>>
728 <Name extends string, A, XR, XE, R, E, ContextInput>(
729 self: Command<Name, A, ContextInput, XE, XR>,
730 handler: (value: A) => Effect.Effect<void, E, R>
731 ): Command<Name, A, ContextInput, E, Exclude<R, BuiltInSettingContext>>
732} = dual(2, <Name extends string, A, XR, XE, R, E, ContextInput>(
733 self: Command<Name, A, ContextInput, XE, XR>,
734 handler: (value: A) => Effect.Effect<void, E, R>
735): Command<Name, A, ContextInput, E, Exclude<R, BuiltInSettingContext>> =>
736 makeCommand({ ...toImpl(self), handle: handler } as any))
737
738interface SubcommandGroupInternal {
739 readonly group: string | undefined
740 readonly commands: NonEmptyReadonlyArray<Command.Any>
741}
742
743const normalizeSubcommandEntries = (
744 entries: ReadonlyArray<Command.SubcommandEntry>
745): {
746 readonly flat: ReadonlyArray<Command.Any>
747 readonly groups: ReadonlyArray<SubcommandGroupInternal>
748} => {
749 const flat: Array<Command.Any> = []
750 const grouped = new Map<string | undefined, NonEmptyArray<Command.Any>>()
751
752 const addToGroup = (group: string | undefined, command: Command.Any): void => {
753 flat.push(command)
754 const existing = grouped.get(group)
755 if (existing) {
756 existing.push(command)
757 } else {
758 grouped.set(group, [command])
759 }
760 }
761
762 for (const entry of entries) {
763 if (isCommand(entry)) {
764 addToGroup(undefined, entry)
765 continue
766 }
767 for (const command of entry.commands) {
768 addToGroup(entry.group, command)
769 }
770 }
771
772 const groups: Array<SubcommandGroupInternal> = []
773 const ungroupedCommands = grouped.get(undefined)
774
775 if (ungroupedCommands && ungroupedCommands.length > 0) {
776 groups.push({ group: undefined, commands: ungroupedCommands })
777 }
778
779 for (const [group, commands] of grouped) {
780 if (group === undefined) {
781 continue
782 }
783 groups.push({ group, commands })
784 }
785
786 return { flat, groups }
787}
788
789/**
790 * Adds subcommands to a command, creating a hierarchical command structure.
791 *
792 * **Details**
793 *
794 * Subcommands can access their parent's parsed configuration by yielding the parent
795 * command within their handler. This enables shared parent flags that affect
796 * all subcommands.
797 *
798 * **Example** (Adding subcommands)
799 *
800 * ```ts import.meta.vitest
801 * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect"
802 * import { Command, Flag } from "effect/cli"
803 * import { ChildProcessSpawner } from "effect/process"
804 *
805 * const CliTestLayer = Layer.mergeAll(
806 * FileSystem.layerNoop({}),
807 * Path.layer,
808 * Stdio.layerTest({}),
809 * Layer.succeed(Terminal.Terminal, Terminal.make({
810 * columns: Effect.succeed(80),
811 * rows: Effect.succeed(24),
812 * readInput: Effect.die("unused"),
813 * readLine: Effect.die("unused"),
814 * display: () => Effect.void
815 * })),
816 * Layer.succeed(
817 * ChildProcessSpawner.ChildProcessSpawner,
818 * ChildProcessSpawner.make(() => Effect.die("unused"))
819 * )
820 * )
821 *
822 * // Parent command with shared flags
823 * const git = Command.make("git").pipe(
824 * Command.withSharedFlags({
825 * verbose: Flag.Boolean("verbose").pipe(Flag.withDefault(false))
826 * })
827 * )
828 *
829 * // Subcommand that accesses parent config
830 * const output: Array<string> = []
831 * const clone = Command.make("clone", {
832 * repository: Flag.String("repo")
833 * }, (config) =>
834 * Effect.gen(function*() {
835 * const parent = yield* git // Access parent's parsed config
836 * if (parent.verbose) {
837 * yield* Effect.sync(() => output.push("Verbose mode enabled"))
838 * }
839 * yield* Effect.sync(() => output.push(`Cloning ${config.repository}`))
840 * }))
841 *
842 * const app = git.pipe(Command.withSubcommands([clone]))
843 *
844 * await Effect.runPromise(
845 * Command.runWith(app, { version: "1.0.0" })([
846 * "--verbose",
847 * "clone",
848 * "--repo",
849 * "github.com/foo/bar"
850 * ]).pipe(Effect.provide(CliTestLayer))
851 * )
852 * output // => ["Verbose mode enabled", "Cloning github.com/foo/bar"]
853 * ```
854 *
855 * @stability unstable
856 * @category combinators
857 * @since 4.0.0
858 */
859export const withSubcommands: {
860 <const Subcommands extends ReadonlyArray<Command.SubcommandEntry>>(
861 subcommands: Subcommands
862 ): <Name extends string, Input, E, R, ContextInput>(
863 self: Command<Name, Input, ContextInput, E, R>
864 ) => Command<
865 Name,
866 Simplify<Input | ContextInput>,
867 ContextInput,
868 E | ExtractSubcommandErrors<Subcommands>,
869 R | Exclude<ExtractSubcommandContext<Subcommands>, CommandContext<Name>>
870 >
871 <
872 Name extends string,
873 Input,
874 E,
875 R,
876 ContextInput,
877 const Subcommands extends ReadonlyArray<Command.SubcommandEntry>
878 >(
879 self: Command<Name, Input, ContextInput, E, R>,
880 subcommands: Subcommands
881 ): Command<
882 Name,
883 Simplify<Input | ContextInput>,
884 ContextInput,
885 E | ExtractSubcommandErrors<Subcommands>,
886 R | Exclude<ExtractSubcommandContext<Subcommands>, CommandContext<Name>>
887 >
888} = dual(2, <
889 Name extends string,
890 Input,
891 E,
892 R,
893 ContextInput,
894 const Subcommands extends ReadonlyArray<Command.SubcommandEntry>
895>(
896 self: Command<Name, Input, ContextInput, E, R>,
897 subcommands: Subcommands
898): Command<
899 Name,
900 Simplify<Input | ContextInput>,
901 ContextInput,
902 E | ExtractSubcommandErrors<Subcommands>,
903 R | Exclude<ExtractSubcommandContext<Subcommands>, CommandContext<Name>>
904> => {
905 const normalized = normalizeSubcommandEntries(subcommands)
906 checkForDuplicateFlags(self, normalized.flat)
907
908 const impl = toImpl(self)
909 const byName = new Map(normalized.flat.map((s) => [s.name, toImpl(s as any)] as const))
910
911 type NextInput = Simplify<Input | ContextInput>
912 const SubcommandStateSymbol = Symbol("effect/cli/SubcommandState")
913 type SubcommandState = { readonly name: string; readonly result: unknown }
914 type InternalInput = NextInput & { readonly [SubcommandStateSymbol]?: SubcommandState }
915
916 const parse = Effect.fnUntraced(function*(raw: ParsedTokens) {
917 if (Option.isNone(raw.subcommand)) {
918 return (yield* impl.parse(raw)) as NextInput
919 }
920
921 const sub = byName.get(raw.subcommand.value.name)
922 if (!sub) {
923 return (yield* impl.parse(raw)) as NextInput
924 }
925
926 const context = yield* impl.parseContext(raw)
927 const result = yield* sub.parse(raw.subcommand.value.parsedInput)
928 return { ...context, [SubcommandStateSymbol]: { name: sub.name, result } } as NextInput
929 })
930
931 const handle = Effect.fnUntraced(function*(input: NextInput, path: ReadonlyArray<string>) {
932 const internal = input as InternalInput
933 const selectedSubcommand = internal[SubcommandStateSymbol]
934
935 if (selectedSubcommand) {
936 const child = byName.get(selectedSubcommand.name)
937 if (!child) {
938 return yield* new CliError.ShowHelp({ commandPath: path, errors: [] })
939 }
940 return yield* child
941 .handle(selectedSubcommand.result, [...path, child.name])
942 .pipe(Effect.provideService(impl.service, input as unknown as ContextInput))
943 }
944 return yield* impl.handle(input as Input, path)
945 })
946
947 return makeCommand({
948 name: impl.name,
949 config: impl.config,
950 contextConfig: impl.contextConfig,
951 description: impl.description,
952 shortDescription: impl.shortDescription,
953 alias: impl.alias,
954 unlisted: impl.unlisted,
955 annotations: impl.annotations,
956 globalFlags: impl.globalFlags,
957 examples: impl.examples,
958 service: impl.service,
959 subcommands: normalized.groups,
960 parse,
961 parseContext: impl.parseContext,
962 handle
963 }) as any
964})
965
966/**
967 * Adds flags that are inherited by subcommands.
968 *
969 * **Details**
970 *
971 * Shared flags are available to this command's handler and to descendant
972 * handlers via `yield* parentCommand`. Shared flags are accepted both before
973 * and after a selected subcommand name (npm-style).
974 *
975 * @stability unstable
976 * @category combinators
977 * @since 4.0.0
978 */
979export const withSharedFlags: {
980 <const SharedFlags extends Command.FlagConfig>(
981 sharedFlags: SharedFlags
982 ): <Name extends string, Input, E, R, ContextInput>(
983 self: Command<Name, Input, ContextInput, E, R>
984 ) => Command<
985 Name,
986 Simplify<Input & Command.Config.Infer<SharedFlags>>,
987 Simplify<ContextInput & Command.Config.Infer<SharedFlags>>,
988 E,
989 R
990 >
991 <Name extends string, Input, E, R, ContextInput, const SharedFlags extends Command.FlagConfig>(
992 self: Command<Name, Input, ContextInput, E, R>,
993 sharedFlags: SharedFlags
994 ): Command<
995 Name,
996 Simplify<Input & Command.Config.Infer<SharedFlags>>,
997 Simplify<ContextInput & Command.Config.Infer<SharedFlags>>,
998 E,
999 R
1000 >
1001} = dual(
1002 2,
1003 <Name extends string, Input, E, R, ContextInput, const SharedFlags extends Command.FlagConfig>(
1004 self: Command<Name, Input, ContextInput, E, R>,
1005 sharedFlags: SharedFlags
1006 ): Command<
1007 Name,
1008 Simplify<Input & Command.Config.Infer<SharedFlags>>,
1009 Simplify<ContextInput & Command.Config.Infer<SharedFlags>>,
1010 E,
1011 R
1012 > => {
1013 const impl = toImpl(self)
1014 const sharedConfig = parseConfig(sharedFlags)
1015 const mergedConfig = mergeConfig(impl.config, sharedConfig)
1016 const mergedContextConfig = mergeConfig(impl.contextConfig, sharedConfig)
1017
1018 if (impl.subcommands.length > 0) {
1019 const flatSubcommands = impl.subcommands.flatMap((group) => group.commands)
1020 checkForDuplicateFlags(self, flatSubcommands, { contextConfig: mergedContextConfig })
1021 }
1022
1023 type SharedInput = Command.Config.Infer<SharedFlags>
1024 type NextInput = Simplify<Input & SharedInput>
1025 type NextContextInput = Simplify<ContextInput & SharedInput>
1026
1027 const parseShared = makeParser(sharedConfig, { allowLeftovers: true }) as (
1028 input: ParsedTokens
1029 ) => Effect.Effect<SharedInput, CliError.CliError, Environment>
1030
1031 const parse = Effect.fnUntraced(function*(raw: ParsedTokens) {
1032 const base = yield* impl.parse(raw)
1033 const shared = yield* parseShared(raw)
1034 return { ...(base as object), ...(shared as object) } as NextInput
1035 })
1036
1037 const parseContext = Effect.fnUntraced(function*(raw: ParsedTokens) {
1038 const base = yield* impl.parseContext(raw)
1039 const shared = yield* parseShared(raw)
1040 return { ...(base as object), ...(shared as object) } as NextContextInput
1041 })
1042
1043 const handle = (
1044 input: NextInput,
1045 commandPath: ReadonlyArray<string>
1046 ) => impl.handle(input as Input, commandPath)
1047
1048 return makeCommand({
1049 name: impl.name,
1050 config: mergedConfig,
1051 contextConfig: mergedContextConfig,
1052 description: impl.description,
1053 shortDescription: impl.shortDescription,
1054 alias: impl.alias,
1055 unlisted: impl.unlisted,
1056 annotations: impl.annotations,
1057 globalFlags: impl.globalFlags,
1058 examples: impl.examples,
1059 service: impl.service as Context.Key<CommandContext<Name>, NextContextInput>,
1060 subcommands: impl.subcommands,
1061 parse,
1062 parseContext,
1063 handle
1064 }) as any
1065 }
1066)
1067
1068/**
1069 * Adds global flags to a command scope.
1070 *
1071 * **Details**
1072 *
1073 * Declared global flags apply to the command and all of its descendants.
1074 *
1075 * @stability unstable
1076 * @category combinators
1077 * @since 4.0.0
1078 */
1079export const withGlobalFlags: {
1080 <const GlobalFlags extends ReadonlyArray<GlobalFlag.GlobalFlag<any>>>(
1081 globalFlags: GlobalFlags
1082 ): <Name extends string, Input, E, R, ContextInput>(
1083 self: Command<Name, Input, ContextInput, E, R>
1084 ) => Command<Name, Input, ContextInput, E, Exclude<R, ExtractGlobalFlagContext<GlobalFlags>>>
1085 <
1086 Name extends string,
1087 Input,
1088 E,
1089 R,
1090 ContextInput,
1091 const GlobalFlags extends ReadonlyArray<GlobalFlag.GlobalFlag<any>>
1092 >(
1093 self: Command<Name, Input, ContextInput, E, R>,
1094 globalFlags: GlobalFlags
1095 ): Command<Name, Input, ContextInput, E, Exclude<R, ExtractGlobalFlagContext<GlobalFlags>>>
1096} = dual(
1097 2,
1098 <
1099 Name extends string,
1100 Input,
1101 E,
1102 R,
1103 ContextInput,
1104 const GlobalFlags extends ReadonlyArray<GlobalFlag.GlobalFlag<any>>
1105 >(
1106 self: Command<Name, Input, ContextInput, E, R>,
1107 globalFlags: GlobalFlags
1108 ): Command<Name, Input, ContextInput, E, Exclude<R, ExtractGlobalFlagContext<GlobalFlags>>> => {
1109 const impl = toImpl(self)
1110 const next = Array.from(new Set([...impl.globalFlags, ...globalFlags]))
1111 return makeCommand({ ...impl, globalFlags: next }) as any
1112 }
1113)
1114
1115// Type extractors for subcommand arrays - T[number] gives union of all elements
1116type ExtractGlobalFlagContext<T extends ReadonlyArray<GlobalFlag.GlobalFlag<any>>> = T[number] extends infer F
1117 ? F extends GlobalFlag.Setting<infer Id, infer _A> ? GlobalFlag.Setting.Identifier<Id>
1118 : never
1119 : never
1120type BuiltInSettingContext = ExtractGlobalFlagContext<typeof GlobalFlag.BuiltIns>
1121type ExtractSubcommand<T> = T extends Command<infer _Name, infer _Input, infer _CI, infer _E, infer _R> ? T
1122 : T extends Command.SubcommandGroup<infer Commands> ? Commands[number]
1123 : never
1124type ExtractSubcommandErrors<T extends ReadonlyArray<Command.SubcommandEntry>> = Error<ExtractSubcommand<T[number]>>
1125type ExtractSubcommandContext<T extends ReadonlyArray<Command.SubcommandEntry>> = Services<ExtractSubcommand<T[number]>>
1126
1127/**
1128 * Sets the description for a command.
1129 *
1130 * **Details**
1131 *
1132 * Descriptions provide users with information about what the command does
1133 * when they view help documentation.
1134 *
1135 * **Example** (Setting descriptions)
1136 *
1137 * ```ts import.meta.vitest
1138 * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect"
1139 * import { Command, Flag } from "effect/cli"
1140 * import { ChildProcessSpawner } from "effect/process"
1141 *
1142 * const CliTestLayer = Layer.mergeAll(
1143 * FileSystem.layerNoop({}),
1144 * Path.layer,
1145 * Stdio.layerTest({}),
1146 * Layer.succeed(Terminal.Terminal, Terminal.make({
1147 * columns: Effect.succeed(80),
1148 * rows: Effect.succeed(24),
1149 * readInput: Effect.die("unused"),
1150 * readLine: Effect.die("unused"),
1151 * display: () => Effect.void
1152 * })),
1153 * Layer.succeed(
1154 * ChildProcessSpawner.ChildProcessSpawner,
1155 * ChildProcessSpawner.make(() => Effect.die("unused"))
1156 * )
1157 * )
1158 *
1159 * const output: Array<string> = []
1160 * const deploy = Command.make("deploy", {
1161 * environment: Flag.String("env")
1162 * }, (config) =>
1163 * Effect.gen(function*() {
1164 * yield* Effect.sync(() => output.push(`Deploying to ${config.environment}`))
1165 * })).pipe(
1166 * Command.withDescription("Deploy the application to a specified environment")
1167 * )
1168 *
1169 * await Effect.runPromise(
1170 * Command.runWith(deploy, { version: "1.0.0" })(["--env", "staging"]).pipe(Effect.provide(CliTestLayer))
1171 * )
1172 * output // => ["Deploying to staging"]
1173 * ```
1174 *
1175 * @stability unstable
1176 * @category combinators
1177 * @since 4.0.0
1178 */
1179export const withDescription: {
1180 (description: string): <const Name extends string, Input, E, R, ContextInput>(
1181 self: Command<Name, Input, ContextInput, E, R>
1182 ) => Command<Name, Input, ContextInput, E, R>
1183 <const Name extends string, Input, E, R, ContextInput>(
1184 self: Command<Name, Input, ContextInput, E, R>,
1185 description: string
1186 ): Command<Name, Input, ContextInput, E, R>
1187} = dual(2, <const Name extends string, Input, E, R, ContextInput>(
1188 self: Command<Name, Input, ContextInput, E, R>,
1189 description: string
1190) => makeCommand({ ...toImpl(self), description }))
1191
1192/**
1193 * Sets a short description for a command.
1194 *
1195 * **Details**
1196 *
1197 * Short descriptions are used when listing subcommands in help output and
1198 * shell completions. If no short description is provided, the full
1199 * `description` is used as a fallback.
1200 *
1201 * @stability unstable
1202 * @category combinators
1203 * @since 4.0.0
1204 */
1205export const withShortDescription: {
1206 (shortDescription: string): <const Name extends string, Input, E, R, ContextInput>(
1207 self: Command<Name, Input, ContextInput, E, R>
1208 ) => Command<Name, Input, ContextInput, E, R>
1209 <const Name extends string, Input, E, R, ContextInput>(
1210 self: Command<Name, Input, ContextInput, E, R>,
1211 shortDescription: string
1212 ): Command<Name, Input, ContextInput, E, R>
1213} = dual(2, <const Name extends string, Input, E, R, ContextInput>(
1214 self: Command<Name, Input, ContextInput, E, R>,
1215 shortDescription: string
1216) => makeCommand({ ...toImpl(self), shortDescription }))
1217
1218/**
1219 * Sets an alias for a command.
1220 *
1221 * **Details**
1222 *
1223 * Aliases are accepted as alternate subcommand names during parsing and are
1224 * shown in help output as `name, alias`.
1225 *
1226 * @stability unstable
1227 * @category combinators
1228 * @since 4.0.0
1229 */
1230export const withAlias: {
1231 (alias: string): <const Name extends string, Input, E, R, ContextInput>(
1232 self: Command<Name, Input, ContextInput, E, R>
1233 ) => Command<Name, Input, ContextInput, E, R>
1234 <const Name extends string, Input, E, R, ContextInput>(
1235 self: Command<Name, Input, ContextInput, E, R>,
1236 alias: string
1237 ): Command<Name, Input, ContextInput, E, R>
1238} = dual(2, <const Name extends string, Input, E, R, ContextInput>(
1239 self: Command<Name, Input, ContextInput, E, R>,
1240 alias: string
1241) => makeCommand({ ...toImpl(self), alias }))
1242
1243/**
1244 * Omits a subcommand from parent help output, shell completions, and
1245 * "did you mean?" suggestions while keeping it fully invocable by exact name.
1246 *
1247 * **When to use**
1248 *
1249 * Use when experimental or internal subcommands should be accepted but not advertised on
1250 * the public CLI surface.
1251 *
1252 * **Example** (Unlisting a subcommand)
1253 *
1254 * ```ts import.meta.vitest
1255 * import { Command } from "effect/cli"
1256 *
1257 * // `experimental` still runs when invoked as `mycli experimental`,
1258 * // but it does not appear under SUBCOMMANDS in `mycli --help`.
1259 * const experimental = Command.make("experimental").pipe(
1260 * Command.unlisted
1261 * )
1262 *
1263 * const root = Command.make("mycli").pipe(
1264 * Command.withSubcommands([experimental])
1265 * )
1266 *
1267 * root.subcommands[0].commands[0].unlisted // => true
1268 * ```
1269 *
1270 * @stability unstable
1271 * @category combinators
1272 * @since 4.0.0
1273 */
1274export const unlisted = <const Name extends string, Input, E, R, ContextInput>(
1275 self: Command<Name, Input, ContextInput, E, R>
1276): Command<Name, Input, ContextInput, E, R> =>
1277 makeCommand({ ...toImpl(self), unlisted: true }) as Command<Name, Input, ContextInput, E, R>
1278
1279/**
1280 * Adds a custom annotation to a command.
1281 *
1282 * **When to use**
1283 *
1284 * Use to attach one command-scoped metadata value under a `Context.Key`,
1285 * especially for consumers such as custom help formatters.
1286 *
1287 * **Details**
1288 *
1289 * Annotations are stored on the command's annotation context and flow into
1290 * generated help document annotations.
1291 *
1292 * **Gotchas**
1293 *
1294 * Adding the same `Context.Key` again replaces the earlier value.
1295 *
1296 * @see {@link annotateMerge} for merging an existing annotation context
1297 *
1298 * @stability unstable
1299 * @category combinators
1300 * @since 4.0.0
1301 */
1302export const annotate: {
1303 <I, S>(
1304 service: Context.Key<I, S>,
1305 value: NoInfer<S>
1306 ): <Name extends string, Input, E, R, ContextInput>(
1307 self: Command<Name, Input, ContextInput, E, R>
1308 ) => Command<Name, Input, ContextInput, E, R>
1309 <Name extends string, Input, E, R, ContextInput, I, S>(
1310 self: Command<Name, Input, ContextInput, E, R>,
1311 service: Context.Key<I, S>,
1312 value: NoInfer<S>
1313 ): Command<Name, Input, ContextInput, E, R>
1314} = dual(3, <Name extends string, Input, E, R, ContextInput, I, S>(
1315 self: Command<Name, Input, ContextInput, E, R>,
1316 service: Context.Key<I, S>,
1317 value: NoInfer<S>
1318) => {
1319 const impl = toImpl(self)
1320 return makeCommand({ ...impl, annotations: Context.add(impl.annotations, service, value) })
1321})
1322
1323/**
1324 * Merges a Context of annotations into a command.
1325 *
1326 * **When to use**
1327 *
1328 * Use when you need to attach an already-built `Context.Context` of command
1329 * annotations.
1330 *
1331 * **Details**
1332 *
1333 * Merged annotations are stored on the command and exposed through generated
1334 * help document annotations.
1335 *
1336 * **Gotchas**
1337 *
1338 * If both contexts contain the same `Context.Key`, the incoming annotations
1339 * context wins.
1340 *
1341 * @see {@link annotate} for adding a single annotation without constructing a `Context`
1342 *
1343 * @stability unstable
1344 * @category combinators
1345 * @since 4.0.0
1346 */
1347export const annotateMerge: {
1348 <I>(
1349 annotations: Context.Context<I>
1350 ): <Name extends string, Input, E, R, ContextInput>(
1351 self: Command<Name, Input, ContextInput, E, R>
1352 ) => Command<Name, Input, ContextInput, E, R>
1353 <Name extends string, Input, E, R, ContextInput, I>(
1354 self: Command<Name, Input, ContextInput, E, R>,
1355 annotations: Context.Context<I>
1356 ): Command<Name, Input, ContextInput, E, R>
1357} = dual(2, <Name extends string, Input, E, R, ContextInput, I>(
1358 self: Command<Name, Input, ContextInput, E, R>,
1359 annotations: Context.Context<I>
1360) => {
1361 const impl = toImpl(self)
1362 return makeCommand({ ...impl, annotations: Context.merge(impl.annotations, annotations) })
1363})
1364
1365/**
1366 * Sets usage examples for a command.
1367 *
1368 * **Details**
1369 *
1370 * Examples are exposed in structured `HelpDoc` data and rendered by the
1371 * default formatter in an `EXAMPLES` section.
1372 *
1373 * **Example** (Adding usage examples)
1374 *
1375 * ```ts import.meta.vitest
1376 * import { Command } from "effect/cli"
1377 *
1378 * const login = Command.make("login").pipe(
1379 * Command.withExamples([
1380 * { command: "myapp login", description: "Log in with browser OAuth" },
1381 * { command: "myapp login --token sbp_abc123", description: "Log in with a token" }
1382 * ])
1383 * )
1384 *
1385 * login.examples.map((example) => example.command) // => ["myapp login", "myapp login --token sbp_abc123"]
1386 * ```
1387 *
1388 * @stability unstable
1389 * @category combinators
1390 * @since 4.0.0
1391 */
1392export const withExamples: {
1393 (examples: ReadonlyArray<Command.Example>): <const Name extends string, Input, E, R, ContextInput>(
1394 self: Command<Name, Input, ContextInput, E, R>
1395 ) => Command<Name, Input, ContextInput, E, R>
1396 <const Name extends string, Input, E, R, ContextInput>(
1397 self: Command<Name, Input, ContextInput, E, R>,
1398 examples: ReadonlyArray<Command.Example>
1399 ): Command<Name, Input, ContextInput, E, R>
1400} = dual(2, <const Name extends string, Input, E, R, ContextInput>(
1401 self: Command<Name, Input, ContextInput, E, R>,
1402 examples: ReadonlyArray<Command.Example>
1403) => makeCommand({ ...toImpl(self), examples }))
1404
1405/* ========================================================================== */
1406/* Providing Services */
1407/* ========================================================================== */
1408
1409// Internal helper: transforms a command's handler while preserving other properties
1410const mapHandler = <Name extends string, Input, E, R, ContextInput, E2, R2>(
1411 self: Command<Name, Input, ContextInput, E, R>,
1412 f: (handler: Effect.Effect<void, E | CliError.CliError, R | Environment>, input: Input) => Effect.Effect<void, E2, R2>
1413): Command<Name, Input, ContextInput, E2, R2> => {
1414 const impl = toImpl(self)
1415 return makeCommand({ ...impl, handle: (input, path) => f(impl.handle(input, path), input) })
1416}
1417
1418/**
1419 * Provides the handler of a command with the services produced by a layer
1420 * that optionally depends on the command-line input to be created.
1421 *
1422 * **Example** (Providing command services)
1423 *
1424 * ```ts import.meta.vitest
1425 * import { Effect, FileSystem, Layer, Path, PlatformError, Stdio, Terminal } from "effect"
1426 * import { Command, Flag } from "effect/cli"
1427 * import { ChildProcessSpawner } from "effect/process"
1428 *
1429 * const CliTestLayer = Layer.mergeAll(
1430 * FileSystem.layerNoop({}),
1431 * Path.layer,
1432 * Stdio.layerTest({}),
1433 * Layer.succeed(Terminal.Terminal, Terminal.make({
1434 * columns: Effect.succeed(80),
1435 * rows: Effect.succeed(24),
1436 * readInput: Effect.die("unused"),
1437 * readLine: Effect.die("unused"),
1438 * display: () => Effect.void
1439 * })),
1440 * Layer.succeed(
1441 * ChildProcessSpawner.ChildProcessSpawner,
1442 * ChildProcessSpawner.make(() => Effect.die("unused"))
1443 * )
1444 * )
1445 *
1446 * const output: Array<string> = []
1447 * const deploy = Command.make("deploy", {
1448 * env: Flag.String("env")
1449 * }, (config) =>
1450 * Effect.gen(function*() {
1451 * const fs = yield* FileSystem.FileSystem
1452 * yield* Effect.sync(() => output.push(`Using file system for ${config.env}`))
1453 * })).pipe(
1454 * // Provide FileSystem based on the --env flag
1455 * Command.provide((config) =>
1456 * config.env === "local"
1457 * ? FileSystem.layerNoop({})
1458 * : FileSystem.layerNoop({
1459 * access: () =>
1460 * Effect.fail(
1461 * PlatformError.badArgument({
1462 * module: "FileSystem",
1463 * method: "access"
1464 * })
1465 * )
1466 * })
1467 * )
1468 * )
1469 *
1470 * await Effect.runPromise(
1471 * Command.runWith(deploy, { version: "1.0.0" })(["--env", "local"]).pipe(Effect.provide(CliTestLayer))
1472 * )
1473 * output // => ["Using file system for local"]
1474 * ```
1475 *
1476 * @stability unstable
1477 * @category providing services
1478 * @since 4.0.0
1479 */
1480export const provide: {
1481 <Input, LR, LE, LA>(
1482 layer: Layer.Layer<LA, LE, LR> | ((input: Input) => Layer.Layer<LA, LE, LR>),
1483 options?: {
1484 readonly local?: boolean | undefined
1485 } | undefined
1486 ): <const Name extends string, E, R, ContextInput>(
1487 self: Command<Name, Input, ContextInput, E, R>
1488 ) => Command<Name, Input, ContextInput, E | LE, Exclude<R, LA> | LR>
1489 <const Name extends string, Input, E, R, ContextInput, LA, LE, LR>(
1490 self: Command<Name, Input, ContextInput, E, R>,
1491 layer: Layer.Layer<LA, LE, LR> | ((input: Input) => Layer.Layer<LA, LE, LR>),
1492 options?: {
1493 readonly local?: boolean | undefined
1494 } | undefined
1495 ): Command<Name, Input, ContextInput, E | LE, Exclude<R, LA> | LR>
1496} = dual((args) => isCommand(args[0]), <const Name extends string, Input, E, R, ContextInput, LA, LE, LR>(
1497 self: Command<Name, Input, ContextInput, E, R>,
1498 layer: Layer.Layer<LA, LE, LR> | ((input: Input) => Layer.Layer<LA, LE, LR>),
1499 options?: { readonly local?: boolean | undefined } | undefined
1500) =>
1501 mapHandler(
1502 self,
1503 (handler, input) => Effect.provide(handler, typeof layer === "function" ? layer(input) : layer, options)
1504 ))
1505
1506/**
1507 * Provides the handler of a command with the implementation of a service that
1508 * optionally depends on the command-line input to be constructed.
1509 *
1510 * **When to use**
1511 *
1512 * Use when a command handler needs a pure service implementation, optionally
1513 * derived from the parsed command input.
1514 *
1515 * @stability unstable
1516 * @category providing services
1517 * @since 4.0.0
1518 */
1519export const provideSync: {
1520 <I, S, Input>(
1521 service: Context.Key<I, S>,
1522 implementation: S | ((input: Input) => S)
1523 ): <const Name extends string, E, R, ContextInput>(
1524 self: Command<Name, Input, ContextInput, E, R>
1525 ) => Command<Name, Input, ContextInput, E, Exclude<R, I>>
1526 <const Name extends string, Input, E, R, ContextInput, I, S>(
1527 self: Command<Name, Input, ContextInput, E, R>,
1528 service: Context.Key<I, S>,
1529 implementation: S | ((input: Input) => S)
1530 ): Command<Name, Input, ContextInput, E, Exclude<R, I>>
1531} = dual(3, <const Name extends string, Input, E, R, ContextInput, I, S>(
1532 self: Command<Name, Input, ContextInput, E, R>,
1533 service: Context.Key<I, S>,
1534 implementation: S | ((input: Input) => S)
1535) =>
1536 mapHandler(self, (handler, input) =>
1537 Effect.provideService(
1538 handler,
1539 service,
1540 typeof implementation === "function" ? (implementation as (input: Input) => S)(input) : implementation
1541 )))
1542
1543/**
1544 * Provides the handler of a command with the service produced by an effect
1545 * that optionally depends on the command-line input to be created.
1546 *
1547 * **When to use**
1548 *
1549 * Use to acquire a service effectfully for each command run, optionally using
1550 * parsed command input.
1551 *
1552 * @see {@link provideSync} for synchronous service acquisition
1553 * @see {@link provide} for providing an already-available service
1554 * @see {@link provideEffectDiscard} for running an effect before the handler without providing a service
1555 *
1556 * @stability unstable
1557 * @category providing services
1558 * @since 4.0.0
1559 */
1560export const provideEffect: {
1561 <I, S, Input, R2, E2>(
1562 service: Context.Key<I, S>,
1563 effect: Effect.Effect<S, E2, R2> | ((input: Input) => Effect.Effect<S, E2, R2>)
1564 ): <const Name extends string, E, R, ContextInput>(
1565 self: Command<Name, Input, ContextInput, E, R>
1566 ) => Command<Name, Input, ContextInput, E | E2, Exclude<R, I> | R2>
1567 <const Name extends string, Input, E, R, ContextInput, I, S, R2, E2>(
1568 self: Command<Name, Input, ContextInput, E, R>,
1569 service: Context.Key<I, S>,
1570 effect: Effect.Effect<S, E2, R2> | ((input: Input) => Effect.Effect<S, E2, R2>)
1571 ): Command<Name, Input, ContextInput, E | E2, Exclude<R, I> | R2>
1572} = dual(3, <const Name extends string, Input, E, R, ContextInput, I, S, R2, E2>(
1573 self: Command<Name, Input, ContextInput, E, R>,
1574 service: Context.Key<I, S>,
1575 effect: Effect.Effect<S, E2, R2> | ((input: Input) => Effect.Effect<S, E2, R2>)
1576) =>
1577 mapHandler(
1578 self,
1579 (handler, input) =>
1580 Effect.provideServiceEffect(handler, service, typeof effect === "function" ? effect(input) : effect)
1581 ))
1582
1583/**
1584 * Allows for execution of an effect, which optionally depends on command-line
1585 * input to be created, prior to executing the handler of a command.
1586 *
1587 * @stability unstable
1588 * @category providing services
1589 * @since 4.0.0
1590 */
1591export const provideEffectDiscard: {
1592 <_, Input, E2, R2>(
1593 effect: Effect.Effect<_, E2, R2> | ((input: Input) => Effect.Effect<_, E2, R2>)
1594 ): <const Name extends string, E, R, ContextInput>(
1595 self: Command<Name, Input, ContextInput, E, R>
1596 ) => Command<Name, Input, ContextInput, E | E2, R | R2>
1597 <const Name extends string, Input, E, R, ContextInput, _, E2, R2>(
1598 self: Command<Name, Input, ContextInput, E, R>,
1599 effect: Effect.Effect<_, E2, R2> | ((input: Input) => Effect.Effect<_, E2, R2>)
1600 ): Command<Name, Input, ContextInput, E | E2, R | R2>
1601} = dual(2, <const Name extends string, Input, E, R, ContextInput, _, E2, R2>(
1602 self: Command<Name, Input, ContextInput, E, R>,
1603 effect: Effect.Effect<_, E2, R2> | ((input: Input) => Effect.Effect<_, E2, R2>)
1604) =>
1605 mapHandler(self, (handler, input) => Effect.andThen(typeof effect === "function" ? effect(input) : effect, handler)))
1606
1607/* ========================================================================== */
1608/* Execution */
1609/* ========================================================================== */
1610
1611/**
1612 * Interactively constructs command-line arguments for a command.
1613 *
1614 * **Details**
1615 *
1616 * The returned arguments include the command name and can be inspected,
1617 * modified, or passed to another command runner by the caller.
1618 *
1619 * **Example** (Constructing command arguments)
1620 *
1621 * ```ts import.meta.vitest
1622 * import { Console, Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect"
1623 * import { Command } from "effect/cli"
1624 * import { ChildProcessSpawner } from "effect/process"
1625 *
1626 * const CliTestLayer = Layer.mergeAll(
1627 * FileSystem.layerNoop({}),
1628 * Path.layer,
1629 * Stdio.layerTest({}),
1630 * Layer.succeed(Terminal.Terminal, Terminal.make({
1631 * columns: Effect.succeed(80),
1632 * rows: Effect.succeed(24),
1633 * readInput: Effect.die("unused"),
1634 * readLine: Effect.die("unused"),
1635 * display: () => Effect.void
1636 * })),
1637 * Layer.succeed(
1638 * ChildProcessSpawner.ChildProcessSpawner,
1639 * ChildProcessSpawner.make(() => Effect.die("unused"))
1640 * )
1641 * )
1642 *
1643 * const command = Command.make("app")
1644 * const silentConsole: Console.Console = Object.assign(Object.create(console), {
1645 * log: () => {}
1646 * })
1647 *
1648 * const program = Command.wizard(command).pipe(
1649 * Effect.provideService(Console.Console, silentConsole),
1650 * Effect.provide(CliTestLayer)
1651 * )
1652 *
1653 * await Effect.runPromise(program) // => ["app"]
1654 * ```
1655 *
1656 * @stability unstable
1657 * @category running
1658 * @since 4.0.0
1659 */
1660export const wizard = <Name extends string, Input, E, R, ContextInput>(
1661 command: Command<Name, Input, ContextInput, E, R>,
1662 options?: {
1663 readonly prefix?: ReadonlyArray<string> | undefined
1664 } | undefined
1665): Effect.Effect<Array<string>, CliError.CliError | Terminal.QuitError, Environment> =>
1666 Effect.map(Wizard.run(command, options), (result) => result.args)
1667
1668const getOutOfScopeGlobalFlagErrors = (
1669 allFlags: ReadonlyArray<GlobalFlag.GlobalFlag<any>>,
1670 activeFlags: ReadonlyArray<GlobalFlag.GlobalFlag<any>>,
1671 flagMap: Record<string, ReadonlyArray<string>>,
1672 commandPath: ReadonlyArray<string>
1673): ReadonlyArray<CliError.UnrecognizedOption> => {
1674 const activeSet = new Set(activeFlags)
1675 const errors: Array<CliError.UnrecognizedOption> = []
1676 const seen = new Set<string>()
1677
1678 for (const flag of allFlags) {
1679 if (activeSet.has(flag)) {
1680 continue
1681 }
1682
1683 const singles = Param.extractSingleParams(flag.flag)
1684 for (const single of singles) {
1685 const entries = flagMap[single.name]
1686 if (!entries || entries.length === 0) {
1687 continue
1688 }
1689 const option = `--${single.name}`
1690 if (seen.has(option)) {
1691 continue
1692 }
1693 seen.add(option)
1694 errors.push(
1695 new CliError.UnrecognizedOption({
1696 option,
1697 suggestions: [],
1698 command: commandPath
1699 })
1700 )
1701 }
1702 }
1703
1704 return errors
1705}
1706
1707const showHelp = <Name extends string, Input, E, R, ContextInput>(
1708 command: Command<Name, Input, ContextInput, E, R>,
1709 error: CliError.ShowHelp,
1710 renderErrors: boolean
1711): Effect.Effect<void, CliError.CliError, Environment> =>
1712 Effect.gen(function*() {
1713 const { builtIns } = yield* CliConfig.CliConfig
1714 const formatter = yield* CliOutput.Formatter
1715 const helpDoc = yield* getHelpForCommandPath(command, error.commandPath, builtIns)
1716 yield* Console.log(formatter.formatHelpDoc(helpDoc))
1717 if (renderErrors && error.errors.length > 0) {
1718 yield* Console.error(formatter.formatErrors(error.errors as any))
1719 }
1720 })
1721
1722const showUserError = (error: CliError.UserError): Effect.Effect<void> =>
1723 Effect.gen(function*() {
1724 const formatter = yield* CliOutput.Formatter
1725 yield* Console.error(formatter.formatError(error))
1726 error[Runtime.errorReported] = false
1727 })
1728
1729/**
1730 * Runs a command using the arguments supplied by the `Stdio` service.
1731 *
1732 * **When to use**
1733 *
1734 * Use when command-line arguments should come from `Stdio` at the application
1735 * entry point.
1736 *
1737 * Help documents are always rendered. By default, parse error details and
1738 * `CliError.UserError` failures are also rendered with the installed
1739 * `CliOutput.Formatter` before the error is rethrown. Set `renderErrors` to
1740 * `false` when the host application owns error rendering.
1741 *
1742 * **Example** (Running commands with standard input)
1743 *
1744 * ```ts import.meta.vitest
1745 * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect"
1746 * import { Command, Flag } from "effect/cli"
1747 * import { ChildProcessSpawner } from "effect/process"
1748 *
1749 * const CliTestLayer = Layer.mergeAll(
1750 * FileSystem.layerNoop({}),
1751 * Path.layer,
1752 * Stdio.layerTest({
1753 * args: Effect.succeed(["--name", "Alice"])
1754 * }),
1755 * Layer.succeed(Terminal.Terminal, Terminal.make({
1756 * columns: Effect.succeed(80),
1757 * rows: Effect.succeed(24),
1758 * readInput: Effect.die("unused"),
1759 * readLine: Effect.die("unused"),
1760 * display: () => Effect.void
1761 * })),
1762 * Layer.succeed(
1763 * ChildProcessSpawner.ChildProcessSpawner,
1764 * ChildProcessSpawner.make(() => Effect.die("unused"))
1765 * )
1766 * )
1767 *
1768 * const output: Array<string> = []
1769 * const greetCommand = Command.make("greet", {
1770 * name: Flag.String("name")
1771 * }, (config) =>
1772 * Effect.gen(function*() {
1773 * yield* Effect.sync(() => output.push(`Hello, ${config.name}!`))
1774 * }))
1775 *
1776 * // Automatically gets args from the Stdio service
1777 * const program = Command.run(greetCommand, {
1778 * version: "1.0.0"
1779 * })
1780 *
1781 * await Effect.runPromise(program.pipe(Effect.provide(CliTestLayer)))
1782 * output // => ["Hello, Alice!"]
1783 * ```
1784 *
1785 * @see {@link runWith} for running a command with an explicit argument array
1786 *
1787 * @stability unstable
1788 * @category running
1789 * @since 4.0.0
1790 */
1791export const run: {
1792 (config: {
1793 readonly version: string
1794 readonly renderErrors?: boolean | undefined
1795 }): <Name extends string, Input, E, R, ContextInput>(
1796 command: Command<Name, Input, ContextInput, E, R>
1797 ) => Effect.Effect<void, E | CliError.CliError, R | Environment>
1798 <Name extends string, Input, E, R, ContextInput>(
1799 command: Command<Name, Input, ContextInput, E, R>,
1800 config: {
1801 readonly version: string
1802 readonly renderErrors?: boolean | undefined
1803 }
1804 ): Effect.Effect<void, E | CliError.CliError, R | Environment>
1805} = dual(2, <Name extends string, Input, E, R, ContextInput>(
1806 command: Command<Name, Input, ContextInput, E, R>,
1807 config: {
1808 readonly version: string
1809 readonly renderErrors?: boolean | undefined
1810 }
1811) =>
1812 Stdio.Stdio.use(({ args }) =>
1813 Effect.flatMap(
1814 args,
1815 (args) => runWith(command, config)(args)
1816 )
1817 ))
1818
1819/**
1820 * Runs a command with explicitly provided arguments instead of using arguments from `Stdio`.
1821 *
1822 * **When to use**
1823 *
1824 * Use when you need to test CLI applications or programmatically execute
1825 * commands with specific arguments.
1826 *
1827 * Help documents are always rendered. By default, parse error details and
1828 * `CliError.UserError` failures are also rendered with the installed
1829 * `CliOutput.Formatter` before the error is rethrown. Set `renderErrors` to
1830 * `false` when the host application owns error rendering.
1831 *
1832 * **Example** (Running commands with explicit arguments)
1833 *
1834 * ```ts import.meta.vitest
1835 * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect"
1836 * import { Command, Flag } from "effect/cli"
1837 * import { ChildProcessSpawner } from "effect/process"
1838 *
1839 * const CliTestLayer = Layer.mergeAll(
1840 * FileSystem.layerNoop({}),
1841 * Path.layer,
1842 * Stdio.layerTest({}),
1843 * Layer.succeed(Terminal.Terminal, Terminal.make({
1844 * columns: Effect.succeed(80),
1845 * rows: Effect.succeed(24),
1846 * readInput: Effect.die("unused"),
1847 * readLine: Effect.die("unused"),
1848 * display: () => Effect.void
1849 * })),
1850 * Layer.succeed(
1851 * ChildProcessSpawner.ChildProcessSpawner,
1852 * ChildProcessSpawner.make(() => Effect.die("unused"))
1853 * )
1854 * )
1855 *
1856 * const output: Array<string> = []
1857 * const greet = Command.make("greet", {
1858 * name: Flag.String("name"),
1859 * count: Flag.Int("count").pipe(Flag.withDefault(1))
1860 * }, (config) =>
1861 * Effect.gen(function*() {
1862 * for (let i = 0; i < config.count; i++) {
1863 * yield* Effect.sync(() => output.push(`Hello, ${config.name}!`))
1864 * }
1865 * }))
1866 *
1867 * // Test with specific arguments
1868 * const testProgram = Effect.gen(function*() {
1869 * const runCommand = Command.runWith(greet, { version: "1.0.0" })
1870 *
1871 * yield* runCommand(["--name", "Alice", "--count", "2"])
1872 * })
1873 *
1874 * await Effect.runPromise(testProgram.pipe(Effect.provide(CliTestLayer)))
1875 * output // => ["Hello, Alice!", "Hello, Alice!"]
1876 * ```
1877 *
1878 * @stability unstable
1879 * @category running
1880 * @since 4.0.0
1881 */
1882export const runWith = <const Name extends string, Input, E, R, ContextInput>(
1883 command: Command<Name, Input, ContextInput, E, R>,
1884 config: {
1885 readonly version: string
1886 readonly renderErrors?: boolean | undefined
1887 }
1888): (
1889 input: ReadonlyArray<string>
1890) => Effect.Effect<void, Exclude<E, Terminal.QuitError> | CliError.CliError, R | Environment> => {
1891 const commandImpl = toImpl(command)
1892 return Effect.fnUntraced(
1893 function*(args: ReadonlyArray<string>) {
1894 const { builtIns } = yield* CliConfig.CliConfig
1895 const { tokens, trailingOperands } = Lexer.lex(args)
1896
1897 // 1. Collect known global flags from the command tree
1898 const allFlags = getGlobalFlagsForCommandTree(command, builtIns)
1899
1900 // 2. Extract global flag tokens
1901 const allFlagParams = allFlags.flatMap((f) => Param.extractSingleParams(f.flag))
1902 const globalRegistry = Parser.createFlagRegistry(allFlagParams.filter(Param.isFlagParam))
1903 const { flagMap, remainder, errors: globalFlagErrors } = Parser.consumeGlobalFlags(
1904 tokens,
1905 command,
1906 globalRegistry
1907 )
1908 const emptyArgs: Param.ParsedArgs = { flags: flagMap, arguments: [] }
1909
1910 // 3. Parse command arguments from remaining tokens
1911 const parsedArgs = yield* Parser.parseArgs({ tokens: remainder, trailingOperands }, command)
1912 const commandPath = [command.name, ...Parser.getCommandPath(parsedArgs)] as const
1913 const handlerCtx: GlobalFlag.HandlerContext = { builtIns, command, commandPath, version: config.version }
1914 const activeFlags = getGlobalFlagsForCommandPath(command, commandPath, builtIns)
1915
1916 // 4. Reject globals that were passed outside the active command scope
1917 const outOfScopeErrors = getOutOfScopeGlobalFlagErrors(allFlags, activeFlags, flagMap, commandPath)
1918 if (outOfScopeErrors.length > 0 || globalFlagErrors.length > 0) {
1919 const parseErrors = parsedArgs.errors ?? []
1920 return yield* new CliError.ShowHelp({
1921 commandPath,
1922 errors: [...globalFlagErrors, ...outOfScopeErrors, ...parseErrors]
1923 })
1924 }
1925
1926 // 5. Process action flags — first present action wins, then exit
1927 for (const flag of activeFlags) {
1928 if (flag._tag !== "Action") continue
1929 const singles = Param.extractSingleParams(flag.flag)
1930 const hasEntry = singles.some((s) => {
1931 const entries = flagMap[s.name]
1932 return entries !== undefined && entries.length > 0
1933 })
1934 if (!hasEntry) continue
1935 const [, value] = yield* flag.flag.parse(emptyArgs)
1936 if (flag === GlobalFlag.Wizard) {
1937 return yield* Effect.gen(function*() {
1938 yield* Console.log(Wizard.renderIntroduction(command.name, config.version, command.description))
1939 const prefix = [
1940 command.name,
1941 ...args.filter((arg) => arg !== "--wizard" && !arg.startsWith("--wizard="))
1942 ]
1943 const wizardResult = yield* Wizard.run(command, { commandPath, prefix })
1944 yield* Console.log(Wizard.renderCompletion(wizardResult.displayArgs))
1945 const shouldRun = yield* Prompt.run(Prompt.Toggle({
1946 message: "Run this command?",
1947 initial: true,
1948 active: "yes",
1949 inactive: "no"
1950 }))
1951 if (shouldRun) {
1952 yield* Console.log()
1953 yield* runWith(command, { ...config, renderErrors: false })(wizardResult.args.slice(1))
1954 }
1955 }).pipe(
1956 Effect.catchTag("QuitError", () => Console.log(Wizard.renderQuit()))
1957 )
1958 }
1959 yield* flag.run(value, handlerCtx)
1960 return
1961 }
1962
1963 // 6. Handle parsing errors
1964 if (parsedArgs.errors && parsedArgs.errors.length > 0) {
1965 return yield* new CliError.ShowHelp({ commandPath, errors: parsedArgs.errors })
1966 }
1967 const parseResult = yield* Effect.result(commandImpl.parse(parsedArgs))
1968 if (parseResult._tag === "Failure") {
1969 return yield* new CliError.ShowHelp({
1970 commandPath,
1971 errors: [parseResult.failure as CliError.NonShowHelpErrors]
1972 })
1973 }
1974
1975 // 7. Provide setting values
1976 let program = commandImpl.handle(parseResult.success, [command.name])
1977 const logLevel = activeFlags.includes(GlobalFlag.LogLevel)
1978 ? (yield* GlobalFlag.LogLevel.flag.parse(emptyArgs))[1]
1979 : Option.none()
1980 program = Effect.provideService(program, GlobalFlag.LogLevel, logLevel)
1981 for (const flag of activeFlags) {
1982 if (flag._tag !== "Setting" || flag === GlobalFlag.LogLevel) continue
1983 const [, value] = yield* flag.flag.parse(emptyArgs)
1984 program = Effect.provideService(program, flag, value)
1985 }
1986
1987 // 8. Apply built-in setting behavior
1988 const services = Option.match(logLevel, {
1989 onNone: () => Context.empty(),
1990 onSome: (level) => Context.make(References.MinimumLogLevel, level)
1991 })
1992
1993 // 9. Run command handler with composed context
1994 yield* Effect.provideContext(program, services)
1995 },
1996 Effect.catchFilter(
1997 (error) =>
1998 CliError.isCliError(error) && error._tag === "ShowHelp"
1999 ? Result.succeed(error)
2000 : Result.fail(error),
2001 (error) => Effect.andThen(showHelp(command, error, config.renderErrors !== false), Effect.fail(error))
2002 ),
2003 Effect.catchFilter(
2004 (error) =>
2005 config.renderErrors !== false && CliError.isCliError(error) && error._tag === "UserError"
2006 ? Result.succeed(error)
2007 : Result.fail(error),
2008 (error) => Effect.andThen(showUserError(error), Effect.fail(error))
2009 ),
2010 Effect.catchFilter(
2011 (e) =>
2012 Terminal.isQuitError(e)
2013 ? Result.succeed(e)
2014 : Result.fail(e as CliError.CliError | Exclude<E, Terminal.QuitError>),
2015 (_) => Effect.interrupt
2016 )
2017 )
2018}