Skip to content

Latest commit

 

History

History

command

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

@molt/Command

🌱 Type-safe CLI command definition and execution.

Installation

npm add @molt/command

Example

import { Command } from '@molt/command'
import { Zod } from '@molt/command/extensions'
import { z } from 'zod'

const args = Command.create()
  .use(Zod)
  .parameter(`filePath`, z.string().describe(`Path to the file to convert.`))
  .parameter(
    `to`,
    z.enum([`json`, `yaml`, `toml`]).describe(`Format to convert to.`),
  )
  .parameter(
    `from`,
    z
      .enum([`json`, `yaml`, `toml`])
      .optional()
      .describe(
        `Format to convert from. By default inferred from the file extension.`,
      ),
  )
  .parameter(
    `verbose v`,
    z.boolean().default(false).describe(
      `Log detailed progress as conversion executes.`,
    ),
  )
  .parameter(
    `move m`,
    z.boolean().default(false).describe(
      `Delete the original file after it has been converted.`,
    ),
  )
  .parse()
$ mybin --file ./music.yaml --to json

Autogenerated help:

$ mybin --help

doc

Features

⛑ Type Safe

👘 Expressive Parameter Specification

  • Types that determine value parsing (casting)

  • Validations

  • Descriptions

  • Normalization between camel and kebab case and optional dash prefix:

    const args = Command.create()
      .parameter('--do-it-a', z.boolean())
      .parameter('--doItB', z.boolean())
      .parameter('doItC', z.boolean())
      .parse()
    
    args1.doItA
    args2.doItB
    args3.doItC
  • One or multiple short and/or long names:

    Command.create().parameter('-f --force --forcefully', z.boolean()).parse()
  • Default values:

    const args = Command.create().parameter(
      '--path',
      z.string().default('./a/b/c'),
    ).parse()
    args.path === './a/b/c/' //   $ mybin
    args.path === '/over/ride' // $ mybin --path /over/ride
  • mutually exclusive parameters

  • flexible argument passing

    • via environment variables (customizable)

      const args = Command.create().parameter('--path', z.string()).parse()
      args.path === './a/b/c/' // $ CLI_PARAM_PATH='./a/b/c' mybin
    • via flags in Kebab or camel case

      $ mybin --do-it
      $ mybin --doIt
      
    • Parameter stacking e.g. mybin -abc instead of mybin -a -b -c

    • Separator symbol of equals or space

      $ mybin -a=foo -b=bar
      $ mybin -a foo -b bar
      

📖 Autogenerated Help

doc doc

🧩 Extensible

  • Extend Molt Command with new capabilities tailored to your use-case
  • Zod Extension
    • Use z.string(), z.boolean etc.
    • .describe(...) to add description
    • .default(...) to set default value

Video Introduction

A video introduction if you like that format:

abc

Docs

Immutability

Molt Command is a fluent API which means methods return a central object of methods allowing you to chain methods one after another.

Each method in the chain returns a new version of the chain. So for example let's say you had two scripts that had some overlapping requirements. You could factor those requirements out into a common base chain.

// helpers.ts
import { Command } from '@molt/command'
import { Zod } from '@molt/command/extensions'

export const command = Command.create()
  .use(Zod)
  .parameter('woof', z.enum(['soft', 'loud', 'deafening']))
// foo.ts
import { command } from './helpers.ts'

const args = command.parameter('bravo', z.boolean())
// bar.ts
const args = command.parameter('charlie', z.number())

Parameter Naming

Flag Syntax

You can define parameters using dash prefixes (flag syntax).

const args = Command.create()
  .parameter('--foo -f', z.string())
  .parameter('qux q', z.string())
  .parse()

args.foo
args.qux

Short, Long, & Aliasing

You can give your parameters short and long names, as well as aliases.

const args = Command.create()
  .parameter('--foobar --foo -f ', z.string())
  .parameter('--bar -b -x', z.number())
  .parameter('-q --qux', z.boolean())
  .parameter('-m -n', z.boolean())
  .parse()

// $ mybin --foobar moo --bar 2 --qux -m
// $ mybin --foo    moo  -x   2 --qux -m
// $ mybin  -f      moo  -b   1  -q   -n
args.foobar === 'moo'
args.bar === 1
args.qux === true
args.m === true

If you prefer you can use a dash-prefix free syntax:

const args = Command.create()
  .parameter('foobar foo f ', z.string())
  .parameter('bar b x', z.number())
  .parameter('q qux', z.boolean())
  .parameter('m n', z.boolean())
  .parse()

Kebab / Camel Case

You can use kebab or camel case.

const args = Command.create()
  .parameter('--foo-bar', z.string())
  .parameter('--quxLot', z.string())
  .parameter('foo-bar-2', z.string())
  .parameter('quxLot2', z.string())
  .parse()

// $ mybin --foo-bar moo --qux-lot zoo
// $ mybin --fooBar moo --quxLot zoo
args.fooBar === 'moo'
args.quxLot === 'zoo'

Duplicate Detection

Duplicate parameter names will be caught statically via TypeScript. The following shows examples of the many forms duplication can happen. Note how case and syntax are not contributing factors to uniqueness.

const args = Command.create()
  .parameter('f foo barBar', z.string())
  .parameter('bar-bar', z.string()) //  <-- TS error: already taken
  .parameter('f', z.string()) //        <-- TS error: already taken
  .parameter('--foo', z.string()) //    <-- TS error: already taken
  .parse()

Reserved Names

Some names are reserved for use by default.

const args = Command.create()
  .parameter('help', z.string()) //     <-- TS error: reserved name
  .parameter('h', z.string()) //        <-- TS error: reserved name
  .parse()

Internal Canonical Form

Internally, the canonical form of a parameter name is the representation that will be used whenever that name has to be referenced. Primarily this means the names you will find on the arguments returned from .parse.

The algorithm is:

  1. The first long name, else first short
  2. Stripped of flag syntax
  3. Using camel case

The following shows an exaggerated example of how the many permutations normalize.

const args = Command.create()
  .parameter('--foo-foo f', z.string())
  .parameter('-q quxQux', z.string())
  .parameter('a b c --a-b-c', z.string())
  .parameter('x', z.string())
  .parameter('-z', z.string())
  .parse()

args.fooFoo
args.quxQux
args.ABC
args.x
args.z

Parameter Types

This section covers the different kinds of built-in types and how they affect argument parsing.

Boolean

  • Flag does not accept any arguments.
  • Environment variable accepts "true" or "1" for true and "false" or "0" for false.
  • Negated form of parameters automatically accepted.

Examples:

const args = Command.create().parameter('f force forcefully', z.boolean())
  .parse()
// $ CLI_PARAM_NO_F='true' mybin
// $ CLI_PARAM_NO_FORCE='true' mybin
// $ CLI_PARAM_NO_FORCEFULLY='true' mybin
// $ CLI_PARAM_F='false' mybin
// $ CLI_PARAM_FORCE='false' mybin
// $ CLI_PARAM_FORCEFULLY='false' mybin
// $ mybin --no-f
// $ mybin --noF
// $ mybin --no-force
// $ mybin --noForce
// $ mybin --no-forcefully
// $ mybin --noForcefully
args.force === false
// $ CLI_PARAM_NO_F='false' mybin
// $ CLI_PARAM_NO_FORCE='false' mybin
// $ CLI_PARAM_NO_FORCEFULLY='false' mybin
// $ CLI_PARAM_F='true' mybin
// $ CLI_PARAM_FORCE='true' mybin
// $ CLI_PARAM_FORCEFULLY='true' mybin
// $ mybin -f
// $ mybin --force
// $ mybin --forcefully
args.force === true

String

  • Flag expects an argument.
Transformations
  • trim
  • toLowerCase
  • toUpperCase
Validations
  • startsWith - A prefix-string the value must be begin with.
  • endsWith - A suffix-string the value must end with.
  • includes - A sub-string the value must exactly contain.
  • regex - An arbitrary Regular Expression that the value must conform to.
  • min - The minimum allowed string length
  • max - The maximum allowed string length
  • length - An exact length the string must be
  • pattern - Different well known patterns that the value must conform to.
    • email - An email
    • ip - An IP address. Can be configured:
      • Version 4
      • Version 6
      • Accept any version
    • url - A URL
    • emoji - An emoji
    • ulid - A ULID
    • uuid - A UUID
    • cuid - A CUID
    • cuid2 - A CUID v2
    • dateTime - An ISO DateTime. Can be configured:
      • To forbid or accept an offset
      • To require a specific level of precision

Number

  • Flag expects an argument.
  • Argument is cast via the Number() function.
Validations
  • min - The minimum allowed number.
  • max - the maximum allowed number.
  • multipleOf - The multiple that the given number must be of. For example 20, 15, 10,5 would all be allowed if multipleOf was 5 since all those numbers are divisible by 5.
  • int

Enum

  • Flag expects an argument.

Union

  • If no variant is a boolean then flag expects an argument.

  • If one variant is a boolean then flag will interpret no argument as being an argument of the boolean variant. For example given this CLI:

    Command.create().parameter('xee', z.union([z.boolean(), z.number()]))

    A user could call your CLI in any of these ways:

    $ mybin --xee
    $ mybin --no-xee
    $ mybin --xee 1
    
  • When a parameter is a union type, the variant that can first successfully parse the given value becomes the interpreted type for the given value. Variant parsers are tried in order of most specific to least, which is: enum, number, boolean, string. So for example if you had a union parameter like this:

    Command.create().parameter('xee', z.union([z.string(), z.number()]))
Help Rendering
  • By default help rendering will render something like so:

    Command.create().parameter(
      'xee',
      z.union([z.string(), z.number()]).description('Blah blah blah.'),
    )
    PARAMETERS
    
      Name    Type/Description                              Default
    
      xee     string | number                               REQUIRED
              Blah blah blah.
    
  • When the parameters have descriptions then it will cause an expanded layout e.g.:

    Command.create().parameter(
      'xee',
      z
        .union([
          z.string().description('Blah blah blah string.'),
          z.number().description('Blah blah blah number.'),
        ])
        .description('Blah blah blah Overview'),
    )
    PARAMETERS
    
      Name    Type/Description                              Default
    
      xee     ┌─union                                       REQUIRED
              │ Blah blah blah overview.
              │
              ◒ string
              │ Blah blah blah string.
              │
              ◒ number
              │ Blah blah blah number.
              └─
    
  • You can force expanded layout even when parameters do not have descriptions via the settings, e.g.:

    Command.create()
      .parameter('xee', z.union([z.string(), z.number()]))
      .parameter('foo', z.union([z.string(), z.number()]))
      .settings({
        helpRendering: {
          union: {
            mode: 'expandAlways',
          },
        },
      })
    PARAMETERS
    
      Name    Type/Description                              Default
    
      xee     ┌─union                                       REQUIRED
              ◒ string
              ◒ number
              └─
    
      foo     ┌─union                                       REQUIRED
              │ Blah blah blah string.
              ◒ string
              ◒ number
              └─
    

Parameter Prompts

You can make Molt Command interactively prompt users for arguments. This enables richer experiences for your users, like:

  • Graceful recovery from invalid up front arguments.
  • Guided argument passing meaning no need to know ahead of time the required parameters, just follow the prompts.

Example:

$ mybin --filePath ./a/b/c.yaml

1/1  to
     ❯ jsonn
     Invalid value: Value is not a member of the enum.
     ❯ json

Overview

  • By default, disabled.

  • Can be configured at parameter level or command level. Parameter level overrides command level.

  • Only basic parameters support prompting (so e.g. not mutually exclusive parameters).

  • Prompt interaction honours the parameter type. Here are some examples:

    • enumeration

      1/3  level
           ❯ high | medium | low
      
    • string

      1/9  name
           ❯
      
    • boolean

      2/3  verbose
           ❯ no | yes
      
    • enum

      5/5  level
      
          Different kinds of answers are accepted.
          Which kind do you want to give?
      
          ❯ string | number | enum
      
          ❯ low | medium | high
      
  • Can be enabled conditionally via pattern matching on events.

    • Common patterns have been pre-defined and exported at Command.eventPatterns for you.
    • Custom patterns may be defined in a type-safe way.
  • The order of prompts will match the order of your parameter definitions.

  • When enabled, a default pattern is used when none explicitly set.

    • The default pattern may be changed.
  • The default settings are:

    Command.create().settings({
      prompt: {
        enabled: false,
        when: Command.eventPatterns.rejectedMissingOrInvalid,
      },
    })
  • When there is no TTY (process.stdout.isTTY === false) then prompts are always disabled.

  • Arguments are validated just like they are when given "up front". However, when invalid, the user will be shown an error message and re-prompted, instead of the process exiting non-zero.

  • Prompts are asynchronously executed so you must await the return of .parse().

Conditional

You can enable parameter prompts conditionally by pattern matching on their parse event emitted when Command runs. Every parameter whose parse event matches with your given pattern will subsequently be prompted for.

All defined parameters emit parse events, irregardless if arguments were given, or from where those arguments originated (line, environment). Therefore this gives you lots of flexibility about when to prompt your user for input. For example:

  • When they miss a required parameter
  • When their input does not pass validation
  • When they an optional parameter is not given an argument up front
  • ...

All you need to do is pass a pattern to prompt either at the parameter level or the command level settings. There are three parse events you can match against:

  • Accepted The parameter received an argument and it was successfully parsed.
  • Rejected The parameter was not successfully parsed. This could be for various reasons such as a required parameter not receiving an argument or the given argument failing pass the parameter's validation rules.
  • Omitted The parameter was not passed an argument by the user. Since this is not a Rejected event it implies that the parameter was either optional, be that with or without a default.

Each event type share some core properties but also have their own unique fields. For example with Accepted you can match against what the value given was and with Rejected you can match against the specific error that occurred.

const args = await Command.create()
  .parameter(`filePath`, z.string())
  .parameter(`to`, {
    schema: z.enum([`json`, `yaml`, `toml`]),
    prompt: {
      result: 'rejected',
      error: 'ErrorMissingArgument',
    },
  })
  .parse()

The pattern matching library will be open-sourced and thoroughly documented in the future.

Examples

For the Default Event Pattern

Passing true will enable using the default event pattern.

const args = await Command.create()
  .parameter(`filePath`, z.string())
  .parameter(`to`, {
    schema: z.enum([`json`, `yaml`, `toml`]),
    prompt: true,
  })
  .parse()
For Particular Event(s)

You can enable prompt when one of the built-in event patterns occur:

const args = await Command.create()
  .parameter(`filePath`, z.string())
  .parameter(`to`, {
    schema: z.enum([`json`, `yaml`, `toml`]),
    prompt: {
      when: Command.EventPatterns.rejectedMissingOrInvalid,
    },
  })
  .parse()

Or when one of multiple events occurs:

const args = await Command.create()
  .parameter(`filePath`, z.string())
  .parameter(`to`, {
    schema: z.enum([`json`, `yaml`, `toml`]),
    prompt: {
      when: [
        Command.EventPatterns.rejectedMissingOrInvalid,
        Command.EventPatterns.omittedWithoutDefault,
      ],
    },
  })
  .parse()
For a Custom Event Pattern

You can enable prompt when your given event pattern occurs.

const args = await Command.create()
  .parameter(`filePath`, z.string())
  .parameter(`to`, {
    schema: z.enum([`json`, `yaml`, `toml`]),
    prompt: {
      when: {
        rejected: {
          reason: 'missing',
        },
      },
    },
  })
  .parse()
At Command Level

You can configure prompts for the entire instance in the settings. The configuration mirrors the parameter level. Parameter level overrides command level.

Enable explicitly with shorthand approach using a boolean:

const args = await Command.create()
  .parameter(`filePath`, z.string())
  .parameter(`to`, z.enum([`json`, `yaml`, `toml`]))
  .settings({ prompt: true })
  .parse()

Enable explicitly with longhand approach using the enabled nested property and include a condition.

Note that in the following enabled could be omitted because passing an object implies enabled: true by default.

const args = await Command.create()
  .parameter(`filePath`, z.string())
  .parameter(`to`, z.enum([`json`, `yaml`, `toml`]))
  .settings({
    prompt: {
      enabled: true,
      when: {
        rejected: {
          reason: 'missing',
        },
      },
    },
  })
  .parse()

Line Arguments

This section is about users passing arguments via the command line (as opposed to the environment), also known as "flags", to the parameters you've defined for your CLI.

Parameter Argument Separator

Arguments can be separated from parameters using the following characters:

  • whitespace
  • equals sign

Examples:

$ mybin --foo=moo
$ mybin --foo= moo
$ mybin --foo = moo
$ mybin --foo moo

Note that when = is attached to the value side then it is considered part of the value:

$ mybin --foo =moo

Stacked Short Flags

Boolean short flags can be stacked. Imagine you have defined three parameters a, b, c. They could be passed like so:

$ mybin -abc

The last short flag does not have to be boolean flag. For example if there were a d parameter taking a string, this could work:

$ mybin -abcd foobar

Case

You can write flags in kebab or camel case:

$ mybin --foo-bar moo
$ mybin --fooBar moo

Environment Arguments

Parameter arguments can be passed by environment variables instead of flags.

Environment arguments have lower precedence than Flags, so if an argument is available from both places, the environment argument is ignored while the flag argument is used.

Default Name Pattern

By default environment arguments can be set using one of the following naming conventions (note: Molt reads environment variables with case-insensitivity):

CLI_PARAMETER_{parameter_name}
CLI_PARAM_{parameter_name}
const args = Command.create().parameter('--path', z.string()).parse()
args.path === './a/b/c/' // $ CLI_PARAMETER_PATH='./a/b/c' mybin

Toggling

You can toggle environment arguments on/off. It is on by default.

const command = Command.create().parameter('--path', z.string()).settings({
  environment: false,
})
// $ CLI_PARAMETER_PATH='./a/b/c' mybin
// Throws error because no argument given for "path"
command.parse()

You can also toggle with the environment variable CLI_SETTINGS_READ_ARGUMENTS_FROM_ENVIRONMENT (case insensitive):

const command = Command.create().parameter('--path', z.string())
// $ CLI_SETTINGS_READ_ARGUMENTS_FROM_ENVIRONMENT='false' CLI_PARAMETER_PATH='./a/b/c' mybin
// Throws error because no argument given for "path"
command.parse()

Selective Toggling

You can toggle environment on for just one or some parameters.

const args = Command.create()
  .parameter('--foo', z.string())
  .parameter('--bar', z.string().default('not_from_env'))
  .settings({ environment: { foo: true } })
  .parse()

// $ CLI_PARAMETER_FOO='foo' CLI_PARAMETER_BAR='bar' mybin
args.foo === 'foo'
args.bar === 'not_from_env'

You can toggle environment on except for just one or some parameters.

const args = Command.create()
  .parameter('--foo', z.string().default('not_from_env'))
  .parameter('--bar', z.string().default('not_from_env'))
  .parameter('--qux', z.string().default('not_from_env'))
  .settings({ environment: { $default: true, bar: false } })
  .parse()

// $ CLI_PARAMETER_FOO='foo' CLI_PARAMETER_BAR='bar' CLI_PARAMETER_QUX='qux' mybin
args.foo === 'foo'
args.bar === 'not_from_env'
args.qux === 'qux'

Custom Prefix

You can customize the environment variable name prefix:

const args = Command.create()
  .parameter('--path', z.string())
  //                                              o-- case insensitive
  .settings({ environment: { $default: { prefix: 'foo' } } })
  .parse()

args.path === './a/b/c/' // $ FOO_PATH='./a/b/c' mybin

You can pass a list of accepted prefixes instead of just one. Earlier ones take precedence over later ones:

const args = Command.create()
  .parameter('--path', z.string())
  //                                               o---------o--- case insensitive
  .settings({ environment: { $default: { prefix: ['foobar', 'foo'] } } })
  .parse()

args.path === './a/b/c/' // $ FOOBAR_PATH='./a/b/c' mybin
args.path === './a/b/c/' // $ FOO_PATH='./a/b/c' mybin
args.path === './a/b/c/' // $ FOO_PATH='./x/y/z' FOOBAR_PATH='./a/b/c' mybin

Selective Custom Prefix

You can customize the environment variable name prefix for just one or some parameters.

const args = Command.create()
  .parameter('--foo', z.string().default('not_from_env'))
  .parameter('--bar', z.string().default('not_from_env'))
  .parameter('--qux', z.string().default('not_from_env'))
  .settings({ environment: { bar: { prefix: 'MOO' } } })
  .parse()

// $ CLI_PARAMETER_FOO='foo' MOO_BAR='bar' CLI_PARAMETER_QUX='qux' mybin
args.foo === 'foo'
args.bar === 'bar'
args.qux === 'qux'

You can customize the environment variable name prefix except for just one or some parameters.

const args = Command.create()
  .parameter('--foo', z.string().default('not_from_env'))
  .parameter('--bar', z.string().default('not_from_env'))
  .parameter('--qux', z.string().default('not_from_env'))
  .settings({
    environment: {
      $default: { enabled: true, prefix: 'MOO' },
      bar: { prefix: true },
    },
  })
  .parse()

// $ MOO_FOO='foo' CLI_PARAM_BAR='bar' MOO_QUX='qux' mybin
args.foo === 'foo'
args.bar === 'bar'
args.qux === 'qux'

Prefix Disabling

You can remove the prefix altogether. Pretty and convenient, but be careful for unexpected use of variables in host environment that would affect your CLI execution!

const args = Command.create()
  .parameter('--path', z.string())
  .settings({ environment: { $default: { prefix: false } } })
  .parse()

args.path === './a/b/c/' // $ PATH='./a/b/c' mybin

Selective Prefix Disabling

You can disable environment variable name prefixes for just one or some parameters.

const args = Command.create()
  .parameter('--foo', z.string().default('not_from_env'))
  .parameter('--bar', z.string().default('not_from_env'))
  .parameter('--qux', z.string().default('not_from_env'))
  .settings({ environment: { bar: { prefix: false } } })
  .parse()

// $ CLI_PARAMETER_FOO='foo' BAR='bar' CLI_PARAMETER_QUX='qux' mybin
args.foo === 'foo'
args.bar === 'bar'
args.qux === 'qux'

You can disable environment variable name prefixes except for just one or some parameters.

const args = Command.create()
  .parameter('--foo', z.string().default('not_from_env'))
  .parameter('--bar', z.string().default('not_from_env'))
  .parameter('--qux', z.string().default('not_from_env'))
  .settings({
    environment: {
      $default: { enabled: true, prefix: false },
      bar: { prefix: true },
    },
  })
  .parse()

// $ FOO='foo' CLI_PARAM_BAR='bar' QUX='qux' mybin
args.foo === 'foo'
args.bar === 'bar'
args.qux === 'qux'

Case Insensitive

Environment variables are considered in a case insensitive way so all of these work:

const args = Command.create().parameter('--path', z.string()).parse()
// $ CLI_PARAM_PATH='./a/b/c' mybin
// $ cli_param_path='./a/b/c' mybin
// $ cLi_pAraM_paTh='./a/b/c' mybin
args.path === './a/b/c/'

Validation

By default, when a prefix is defined, a typo will raise an error:

const command = Command.create().parameter('--path', z.string())

// $ CLI_PARAM_PAH='./a/b/c' mybin
// Throws error because there is no parameter named "pah" defined.
command.parse()

If you pass arguments for a parameter multiple times under different environment variable name aliases an error will be raised.

const command = Command.create().parameter('--path', z.string())

// $ CLI_PARAMETER_PAH='./1/2/3' CLI_PARAM_PAH='./a/b/c' mybin
/ole/ Throws error because user intent is ambiguous.
command.parse()

Mutually Exclusive Parameters

With the chaining API you can declaratively state that two or more parameters are mutually exclusive using the parametersExclusive method.

Here is an example where you might want this feature. You are building a CLI for publishing software packages that allows the user to specify the version to publish either by semver level to bump by OR an exact version.

const args = Command.create()
  .parametersExclusive(
    `method`,
    (_) =>
      _.parameter(`v version`, z.string().regex(semverRegex()))
        .parameter(`b bump`, z.enum([`major`, `minor`, `patch`])),
  )

There are three key benefits to this method:

  1. Type safety for your implementation
  2. Autogenerated documentation for your users
  3. Clear runtime feedback for your users when they mistakenly pass arguments for more than one of the mutually exclusive parameters.

Type Safety

In the above example args will end up with a method property whose type is:

type Method =
  | { _tag: 'version'; value: string }
  | { _tag: 'bump'; value: 'major' | 'minor' | 'patch' }

You automatically get a proper TypeScript-ready discriminant property based on the canonical names of your parameters. This helps you to write type-safe code. Also, it pairs well with Alge 🌱 :). In the following example Semver.inc expects a strongly typed semver bump level of 'major'|'minor'|'patch':

const newVersion = Alge.match(args.method)
  .bump(({ value }) => Semver.inc(pkg.version, value))
  .version(({ value }) => value)
  .done()

Optional

By default, input for a group of mutually exclusive parameters is required. You can mark the group as being optional:

const args = Command.create()
  .parametersExclusive(
    `method`,
    (_) =>
      _.parameter(`v version`, z.string().regex(semverRegex()))
        .parameter(`b bump`, z.enum([`major`, `minor`, `patch`]))
        .optional(),
  )

Default

By default, input for a group of mutually exclusive parameters is required. You can mark the group as being optional for users via a default so that internally there is always a value:

const args = Command.create()
  .parametersExclusive(
    `method`,
    (_) =>
      _.parameter(`v version`, z.string().regex(semverRegex()))
        .parameter(`b bump`, z.enum([`major`, `minor`, `patch`]))
        .optional()
        .default('bump', 'patch'),
  )

Autogenerated Documentation

Your users will clearly see that these parameters are mutually exclusive. Here's an example from the CLI/script Molt itself uses to publish new releases:

doc

Description

You can give your command a description similar to how you can give each of your parameters a description.

const args = Command.create()
  .description(
    'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam.',
  )
  .parameter() /* ... */

Descriptions will show up in the auto generated help.

Settings

You can control certain settings about the command centrally using the .settings method. Sometimes the options here approximate the same options passable to parameter level settings, with the difference that configuration here affects all parameters at once. However, parameter level settings will always override command level ones.

Settings documentation is not co-located. Documentation for various features will mention when there are command level settings available.

Command.create().settings({...})

Extensions

Zod

The Zod extension lets you use Zod schemas wherever types are accepted.

To use this extension you must install zod into your project:

npm add zod

The supported Zod types and their mapping is as follows:

Zod Molt Type
boolean boolean
string string
number number
enum enum
nativeEnum enum
union (of any other Zod type above) union

All validation methods are accepted (.min(1), .regex(/.../), .cuid(), ...).

The following modifiers are accepted:

.optional()
.default(...)

If both optional and default are used then default takes precedence. The describe method is used for adding docs. It can show up in any part of the chain. All the following are fine:

z.string().describe('...').optional()
z.string().optional().describe('...')
z.string().min(1).describe('...').optional()

Recipes

Use dprint Instead of Prettier

Prettier formatting does not work well with Prettier due to the how much Molt Command relies on chaining APIs (aka. fluent APIs) whilst Prettier formats them poorly. You can find more detail and examples of its problems here.

You can try https://dprint.dev instead which works well. All examples in this repo are formatted using it.

Optional Argument With Default Behavior

Say you want this CLI design:

mybin             <-- Disable xee (default)
mybin --xee       <-- Enable xee, use default
mybin --xee x     <-- enable xee using x
mybin --xee y     <-- enable xee using y
mybin --xee z     <-- enable xee using z

You could achieve this with the following parameter definition:

const args = Command.create().parameter(
  'xee',
  z.union([z.boolean(), z.enum(['x', 'y', 'z'])]).default(false),
)

args.xee // type: false | true | 'x' | 'y' | 'z'

Architecture

Molt Command is composed from multiple distinct layers that execute in a flow:

  1. Settings Parser
  2. Parameter Parser
    • Use settings to enrich parameter specifications (like environment support)
  3. Up Front Arguments Parser
    • Accept inputs from difference sources:
      • Line
      • Environment
    • Cast values to primitive types based on parameter specification
  4. Pre-Prompt Argument Validation
  5. Prompt Plan (prompt matchers executed, matches mean prompt should run)
  6. Prompt Apply or Mistake Reporter (if not all mistakes recovered as prompts)
  7. Prompt/Up Front Arguments Merger (prompt overrides up front)