Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
9ca73a0
refactor(cli): clarify parser scan flow
kitlangton Nov 25, 2025
77a2938
chore(cli): document parser pipeline and simplify subcommand lookup
kitlangton Nov 25, 2025
e1bf50c
refactor(cli): use parse context and phase headers
kitlangton Nov 25, 2025
85f344e
refactor(cli): centralize flag spec for readability
kitlangton Nov 25, 2025
fab2607
refactor(cli): encapsulate flag bag helper
kitlangton Nov 25, 2025
09c94dc
docs(cli): record parsing semantics and add -- terminator test
kitlangton Nov 25, 2025
4e2d4d0
docs(cli): add cross-library semantics comparison
kitlangton Nov 25, 2025
957bd09
test(cli): cover boolean false, combined shorts, -- terminator, and e…
kitlangton Nov 25, 2025
5df5430
refactor(cli): namespace Command.Config types
kitlangton Nov 25, 2025
ff4ff6f
refactor(cli): dynamic-only completions, extract internal/command.ts
kitlangton Nov 26, 2025
03219c5
refactor(cli): remove internal types from Command namespace
kitlangton Nov 26, 2025
304e3c2
refactor(cli): make withSubcommands a proper dual with array param
kitlangton Nov 26, 2025
c091c23
refactor(cli): clean up withSubcommands and reduce as-any casts
kitlangton Nov 26, 2025
20c2c24
refactor(cli): simplify error handling in runWith
kitlangton Nov 26, 2025
8c5deec
refactor(cli): clean up runWith structure
kitlangton Nov 26, 2025
e0949f3
refactor(cli): simplify type extractors using T[number]
kitlangton Nov 26, 2025
0c0a4f4
refactor(cli): miscellaneous Command.ts cleanups
kitlangton Nov 26, 2025
b4c166c
refactor(cli): rename HelpFormatter to CliOutput, improve type names
kitlangton Nov 26, 2025
5be1ca0
refactor(cli): simplify withSubcommands, remove type widening
kitlangton Nov 26, 2025
d88a250
docs(cli): improve Command.ts docstrings
kitlangton Nov 26, 2025
319125b
refactor(cli): make Param internal, fix bugs, add missing Argument co…
kitlangton Nov 26, 2025
99e7236
refactor(cli): add Command.Any, remove as any casts in handler/command
kitlangton Nov 26, 2025
f59c550
refactor(cli): remove unnecessary generics from parser.ts
kitlangton Nov 26, 2025
8fe2b29
refactor(cli): surface all errors to user, add formatErrors
kitlangton Nov 26, 2025
bf8aef3
refactor(cli): naming consistency cleanup
kitlangton Nov 26, 2025
098211a
refactor(cli): simplify variance struct pattern
kitlangton Nov 26, 2025
829475b
refactor(cli): use unknown instead of any in .Any types
kitlangton Nov 26, 2025
05f23f9
refactor(cli): remove deprecated exports, normalize JSDoc categories
kitlangton Nov 26, 2025
800892b
fix errors
kitlangton Nov 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 180 additions & 30 deletions packages/effect/src/unstable/cli/Argument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
*/
import type * as Option from "../../data/Option.ts"
import type * as Redacted from "../../data/Redacted.ts"
import type * as Result from "../../data/Result.ts"
import type * as Effect from "../../Effect.ts"
import { dual } from "../../Function.ts"
import { dual, type LazyArg } from "../../Function.ts"
import type * as FileSystem from "../../platform/FileSystem.ts"
import type * as Path from "../../platform/Path.ts"
import type * as Schema from "../../schema/Schema.ts"
Expand All @@ -13,13 +14,26 @@ import type { Environment } from "./Command.ts"
import * as Param from "./Param.ts"
import type * as Primitive from "./Primitive.ts"

// -------------------------------------------------------------------------------------
// models
// -------------------------------------------------------------------------------------

/**
* Represents a positional command-line argument.
*
* Note: `boolean` is intentionally omitted from Argument constructors.
* Positional boolean arguments are ambiguous in CLI design since there's
* no flag name to negate (e.g., `--no-verbose`). Use Flag.boolean instead,
* or use Argument.choice with explicit "true"/"false" strings if needed.
*
* @since 4.0.0
* @category models
*/
export interface Argument<A> extends Param.Param<typeof Param.Argument, A> {}
export interface Argument<A> extends Param.Param<typeof Param.argumentKind, A> {}

// -------------------------------------------------------------------------------------
// constructors
// -------------------------------------------------------------------------------------

/**
* Creates a positional string argument.
Expand All @@ -34,7 +48,7 @@ export interface Argument<A> extends Param.Param<typeof Param.Argument, A> {}
* @since 4.0.0
* @category constructors
*/
export const string = (name: string): Argument<string> => Param.string(Param.Argument, name)
export const string = (name: string): Argument<string> => Param.string(Param.argumentKind, name)

/**
* Creates a positional integer argument.
Expand All @@ -49,7 +63,7 @@ export const string = (name: string): Argument<string> => Param.string(Param.Arg
* @since 4.0.0
* @category constructors
*/
export const integer = (name: string): Argument<number> => Param.integer(Param.Argument, name)
export const integer = (name: string): Argument<number> => Param.integer(Param.argumentKind, name)

/**
* Creates a positional file path argument.
Expand All @@ -67,7 +81,7 @@ export const integer = (name: string): Argument<number> => Param.integer(Param.A
*/
export const file = (name: string, options?: {
readonly mustExist?: boolean | undefined
}): Argument<string> => Param.file(Param.Argument, name, options)
}): Argument<string> => Param.file(Param.argumentKind, name, options)

/**
* Creates a positional directory path argument.
Expand All @@ -84,7 +98,7 @@ export const file = (name: string, options?: {
*/
export const directory = (name: string, options?: {
readonly mustExist?: boolean | undefined
}): Argument<string> => Param.directory(Param.Argument, name, options)
}): Argument<string> => Param.directory(Param.argumentKind, name, options)

/**
* Creates a positional float argument.
Expand All @@ -99,7 +113,7 @@ export const directory = (name: string, options?: {
* @since 4.0.0
* @category constructors
*/
export const float = (name: string): Argument<number> => Param.float(Param.Argument, name)
export const float = (name: string): Argument<number> => Param.float(Param.argumentKind, name)

/**
* Creates a positional date argument.
Expand All @@ -114,7 +128,7 @@ export const float = (name: string): Argument<number> => Param.float(Param.Argum
* @since 4.0.0
* @category constructors
*/
export const date = (name: string): Argument<Date> => Param.date(Param.Argument, name)
export const date = (name: string): Argument<Date> => Param.date(Param.argumentKind, name)

/**
* Creates a positional choice argument.
Expand All @@ -132,7 +146,7 @@ export const date = (name: string): Argument<Date> => Param.date(Param.Argument,
export const choice = <const Choices extends ReadonlyArray<string>>(
name: string,
choices: Choices
): Argument<Choices[number]> => Param.choice(Param.Argument, name, choices)
): Argument<Choices[number]> => Param.choice(Param.argumentKind, name, choices)

/**
* Creates a positional path argument.
Expand All @@ -150,7 +164,7 @@ export const choice = <const Choices extends ReadonlyArray<string>>(
export const path = (name: string, options?: {
pathType?: "file" | "directory" | "either"
mustExist?: boolean
}): Argument<string> => Param.path(Param.Argument, name, options)
}): Argument<string> => Param.path(Param.argumentKind, name, options)

/**
* Creates a positional redacted argument that obscures its value.
Expand All @@ -165,7 +179,7 @@ export const path = (name: string, options?: {
* @since 4.0.0
* @category constructors
*/
export const redacted = (name: string): Argument<Redacted.Redacted<string>> => Param.redacted(Param.Argument, name)
export const redacted = (name: string): Argument<Redacted.Redacted<string>> => Param.redacted(Param.argumentKind, name)

/**
* Creates a positional argument that reads file content as a string.
Expand All @@ -180,7 +194,7 @@ export const redacted = (name: string): Argument<Redacted.Redacted<string>> => P
* @since 4.0.0
* @category constructors
*/
export const fileText = (name: string): Argument<string> => Param.fileString(Param.Argument, name)
export const fileText = (name: string): Argument<string> => Param.fileText(Param.argumentKind, name)

/**
* Creates a positional argument that reads and validates file content using a schema.
Expand All @@ -198,7 +212,7 @@ export const fileText = (name: string): Argument<string> => Param.fileString(Par
export const fileParse = (
name: string,
options?: Primitive.FileParseOptions | undefined
): Argument<unknown> => Param.fileParse(Param.Argument, name, options)
): Argument<unknown> => Param.fileParse(Param.argumentKind, name, options)

/**
* Creates a positional argument that reads and validates file content using a schema.
Expand All @@ -225,7 +239,7 @@ export const fileSchema = <A>(
name: string,
schema: Schema.Codec<A, string>,
options?: Primitive.FileSchemaOptions | undefined
): Argument<A> => Param.fileSchema(Param.Argument, name, schema, options)
): Argument<A> => Param.fileSchema(Param.argumentKind, name, schema, options)

/**
* Creates an empty sentinel argument that always fails to parse.
Expand All @@ -241,7 +255,11 @@ export const fileSchema = <A>(
* @since 4.0.0
* @category constructors
*/
export const none: Argument<never> = Param.none(Param.Argument)
export const none: Argument<never> = Param.none(Param.argumentKind)

// -------------------------------------------------------------------------------------
// combinators
// -------------------------------------------------------------------------------------

/**
* Makes a positional argument optional.
Expand Down Expand Up @@ -409,21 +427,6 @@ export const mapTryCatch: {
onError: (error: unknown) => string
) => Param.mapTryCatch(self, f, onError))

/**
* Creates a variadic argument that accepts multiple values (same as variadic).
*
* @example
* ```ts
* import { Argument } from "effect/unstable/cli"
*
* const files = Argument.string("files").pipe(Argument.repeated)
* ```
*
* @since 4.0.0
* @category combinators
*/
export const repeated = <A>(arg: Argument<A>): Argument<ReadonlyArray<A>> => Param.repeated(arg)

/**
* Creates a variadic argument that requires at least n values.
*
Expand Down Expand Up @@ -498,3 +501,150 @@ export const withSchema: {
<A, B>(schema: Schema.Codec<B, A>): (self: Argument<A>) => Argument<B>
<A, B>(self: Argument<A>, schema: Schema.Codec<B, A>): Argument<B>
} = dual(2, <A, B>(self: Argument<A>, schema: Schema.Codec<B, A>) => Param.withSchema(self, schema))

/**
* Creates a positional choice argument with custom value mapping.
*
* @example
* ```ts
* import { Argument } from "effect/unstable/cli"
*
* const logLevel = Argument.choiceWithValue("level", [
* ["debug", 0],
* ["info", 1],
* ["warn", 2],
* ["error", 3]
* ])
* ```
*
* @since 4.0.0
* @category constructors
*/
export const choiceWithValue = <const Choices extends ReadonlyArray<readonly [string, any]>>(
name: string,
choices: Choices
): Argument<Choices[number][1]> => Param.choiceWithValue(Param.argumentKind, name, choices)

// -------------------------------------------------------------------------------------
// metadata
// -------------------------------------------------------------------------------------

/**
* Sets a custom metavar (placeholder name) for the argument in help documentation.
*
* The metavar is displayed in usage text to indicate what value the user should provide.
* For example, `<FILE>` shows `FILE` as the metavar.
*
* @example
* ```ts
* import { Argument } from "effect/unstable/cli"
*
* const port = Argument.integer("port").pipe(
* Argument.withMetavar("PORT")
* )
* ```
*
* @since 4.0.0
* @category metadata
*/
export const withMetavar: {
<A>(metavar: string): (self: Argument<A>) => Argument<A>
<A>(self: Argument<A>, metavar: string): Argument<A>
} = dual(2, <A>(self: Argument<A>, metavar: string) => Param.withMetavar(self, metavar))

/**
* Filters parsed values, failing with a custom error message if the predicate returns false.
*
* @example
* ```ts
* import { Argument } from "effect/unstable/cli"
*
* const positiveInt = Argument.integer("count").pipe(
* Argument.filter(
* n => n > 0,
* n => `Expected positive integer, got ${n}`
* )
* )
* ```
*
* @since 4.0.0
* @category combinators
*/
export const filter: {
<A>(predicate: (a: A) => boolean, onFalse: (a: A) => string): (self: Argument<A>) => Argument<A>
<A>(self: Argument<A>, predicate: (a: A) => boolean, onFalse: (a: A) => string): Argument<A>
} = dual(3, <A>(
self: Argument<A>,
predicate: (a: A) => boolean,
onFalse: (a: A) => string
) => Param.filter(self, predicate, onFalse))

/**
* Filters and transforms parsed values, failing with a custom error message
* if the filter function returns None.
*
* @example
* ```ts
* import { Option } from "effect/data"
* import { Argument } from "effect/unstable/cli"
*
* const positiveInt = Argument.integer("count").pipe(
* Argument.filterMap(
* (n) => n > 0 ? Option.some(n) : Option.none(),
* (n) => `Expected positive integer, got ${n}`
* )
* )
* ```
*
* @since 4.0.0
* @category combinators
*/
export const filterMap: {
<A, B>(f: (a: A) => Option.Option<B>, onNone: (a: A) => string): (self: Argument<A>) => Argument<B>
<A, B>(self: Argument<A>, f: (a: A) => Option.Option<B>, onNone: (a: A) => string): Argument<B>
} = dual(3, <A, B>(
self: Argument<A>,
f: (a: A) => Option.Option<B>,
onNone: (a: A) => string
) => Param.filterMap(self, f, onNone))

/**
* Provides a fallback argument to use if this argument fails to parse.
*
* @example
* ```ts
* import { Argument } from "effect/unstable/cli"
*
* const value = Argument.integer("value").pipe(
* Argument.orElse(() => Argument.string("value"))
* )
* ```
*
* @since 4.0.0
* @category combinators
*/
export const orElse: {
<B>(that: LazyArg<Argument<B>>): <A>(self: Argument<A>) => Argument<A | B>
<A, B>(self: Argument<A>, that: LazyArg<Argument<B>>): Argument<A | B>
} = dual(2, <A, B>(self: Argument<A>, that: LazyArg<Argument<B>>) => Param.orElse(self, that))

/**
* Provides a fallback argument, wrapping results in Result to distinguish which succeeded.
*
* @example
* ```ts
* import { Argument } from "effect/unstable/cli"
*
* const source = Argument.file("source").pipe(
* Argument.orElseResult(() => Argument.string("url"))
* )
* // Returns Result<string, string>
* ```
*
* @since 4.0.0
* @category combinators
*/
export const orElseResult: {
<B>(that: LazyArg<Argument<B>>): <A>(self: Argument<A>) => Argument<Result.Result<A, B>>
<A, B>(self: Argument<A>, that: LazyArg<Argument<B>>): Argument<Result.Result<A, B>>
} = dual(2, <A, B>(self: Argument<A>, that: LazyArg<Argument<B>>) => Param.orElseResult(self, that))
Loading
Loading