Détail du package

@black-flag/extensions

Xunnamius422MIT3.1.2

A collection of set-theoretic declarative-first APIs for yargs and Black Flag

cli, commandline, command-line, yargs

readme

A collection of set-theoretic declarative-first APIs for Yargs and Black Flag


![Black Lives Matter!][x-badge-blm-image] ![Last commit timestamp][x-badge-lastcommit-image] ![Codecov][x-badge-codecov-image] [![Source license][x-badge-license-image]][x-badge-license-link] [![Uses Semantic Release!][x-badge-semanticrelease-image]][x-badge-semanticrelease-link] ![NPM version][x-badge-npm-image] ![Monthly Downloads][x-badge-downloads-image]


@black-flag/extensions 🏴

Black Flag Extensions (BFE) is a collection of surprisingly simple set-theoretic APIs that wrap Black Flag commands' exports to provide a bevy of new declarative features, some of which are heavily inspired by Yargs's GitHub Issues reports. It's like type-fest or jest-extended, but for Black Flag and Yargs!

In exchange for straying a bit from the vanilla Yargs API, BFE greatly increases Black Flag's declarative powers.

Note that BFE does not represent a complete propositional logic and so cannot describe every possible relation between arguments. Nor should it; BFE makes it easy to fall back to using the Yargs API imperatively in those rare instances it's necessary.



Install

To install:

npm install @black-flag/extensions

Usage

[!NOTE]

See also: differences between BFE and Yargs.

[!WARNING]

Most of the examples in this section (not including syntax-highlighted code blocks) are using hyphen characters followed by a word joiner character to prevent breaking examples awkwardly across lines. Be cautious copying and pasting.

withBuilderExtensions

⪢ API reference: withBuilderExtensions

This function enables several additional options-related units of functionality via analysis of the returned options configuration object and the parsed command line arguments (i.e. argv).

import { withBuilderExtensions } from '@black-flag/extensions';

export default function command({ state }) {
  const [builder, withHandlerExtensions] = withBuilderExtensions(
    (blackFlag, helpOrVersionSet, argv) => {
      blackFlag.strict(false);

      // ▼ The "returned options configuration object"
      return {
        'my-argument': {
          alias: ['arg1'],
          demandThisOptionXor: ['arg2'],
          string: true
        },
        arg2: {
          boolean: true,
          demandThisOptionXor: ['my-argument']
        }
      };
    },
    { disableAutomaticGrouping: true }
  );

  return {
    name: 'my-command',
    builder,
    handler: withHandlerExtensions(({ myArgument, arg2 }) => {
      state.outputManager.log(
        'Executing command with arguments: arg1=${myArgument} arg2=${arg2}'
      );
    })
  };
}

Note how, in the previous example, the option names passed to configuration keys, e.g. { demandThisOptionXor: ['my-argument'] }, are represented by their exact canonical names as defined (e.g. 'my-⁠argument') and not their aliases ('arg1') or camel-case expanded forms ('myArgument'). All BFE configuration keys expect canonical option names in this way; passing an alias or a camel-case expansion will result in erroneous behavior.

In the same vein, withBuilderExtensions will throw if you attempt to add a command option with a name, alias, or camel-case expansion that conflicts with another of that command's options. This sanity check takes into account the following yargs-parser configuration settings: camel-case-expansion, strip-aliased, strip-dashed.

Also note how withBuilderExtensions returns a two-element array of the form: [builder, withHandlerExtensions]. builder should be exported as your command's builder function without being invoked. If you want to implement additional imperative logic, pass a customBuilder function to withBuilderExtensions as demonstrated in the previous example; otherwise, you should pass an options configuration object.

On the other hand, withHandlerExtensions should be invoked immediately, and its return value should be exported as your command's handler function as demonstrated in the previous example. You should pass a customHandler to withHandlerExtensions upon invocation, though this is not required. If you call withHandlerExtensions() without providing a customHandler, a placeholder function that throws CommandNotImplementedError will be used instead, indicating that the command has not yet been implemented. This mirrors Black Flag's default behavior for unimplemented command handlers.

New Option Configuration Keys

This section details the new configuration keys made available by BFE, each implementing an options-related unit of functionality beyond that offered by vanilla Yargs and Black Flag.

Note that the checks enabled by these configuration keys:

  • Are run on Black Flag's [second parsing pass][11] except where noted. This allows BFE to perform checks against argument values in addition to the argument existence checks enabled by vanilla Yargs.

  • Will ignore the existence of the default key (unless it's a custom check). This means you can use keys like requires and conflicts alongside default without causing unresolvable CLI errors. This avoids a rather unintuitive Yargs footgun.

  • Will take into account the following yargs-parser settings configuration settings: camel-case-expansion, strip-aliased, strip-dashed. Note that dot-notation is not currently recognized or considered by BFE, but may be in the future.

Logical Keys

[!NOTE]

In the below definitions, P, Q, and R are arguments (or argument-value pairs) configured via a hypothetical call to blackFlag.options({ P: { [key]: [Q, R] }}). The truth values of P, Q, and R as described in the "Definition" column represent the existence of each respective argument (and its value) in the argv parse result. gwav is a predicate standing for "given with any value," meaning the argument was given on the command line.

Key Definition
requires P ⟹ (Q ∧ R) or ¬P ∨ (Q ∧ R)
conflicts P ⟹ (¬Q ∧ ¬R) or ¬P ∨ (¬Q ∧ ¬R)
implies P ⟹ (Q ∧ R ∧ (gwav(Q) ⟹ Q) ∧ (gwav(R) ⟹ R))
demandThisOptionIf (Q ∨ R) ⟹ P or P ∨ (¬Q ∧ ¬R)
demandThisOption P
demandThisOptionOr P ∨ Q ∨ R
demandThisOptionXor P ⊕ Q ⊕ R

Relational Keys

Key
check
subOptionOf
looseImplications
vacuousImplications

requires

⪢ API reference: requires

[!IMPORTANT]

requires is a superset of and replacement for vanilla Yargs's implies. BFE also has its own implication implementation.

requires enables checks to ensure the specified arguments, or argument-value pairs, are given conditioned on the existence of another argument. For example:

{
  "x": { "requires": "y" }, // ◄ Disallows x without y
  "y": {}
}

This configuration will trigger a check to ensure that -⁠y is given whenever -⁠x is given.

requires also supports checks against the parsed values of arguments in addition to the argument existence checks demonstrated above. For example:

{
  // ▼ Disallows x unless y == 'one' and z is given
  "x": { "requires": [{ "y": "one" }, "z"] },
  "y": {},
  "z": { "requires": "y" } // ◄ Disallows z unless y is given
}

This configuration allows the following arguments: no arguments (), -⁠y=..., -⁠y=... -⁠z, -⁠xz -⁠y=one; and disallows: -⁠x, -⁠z, -⁠x -⁠y=..., -⁠xz -⁠y=..., -⁠xz.

Note that, when performing a check using the parsed value of an argument and that argument is configured as an array ({ array: true }), that array will be searched for said value. Otherwise, a strict deep equality check is performed.

requires versus implies

Choose BFE's implies over requires when you want one argument to imply the value of another without requiring the other argument to be explicitly given in argv (e.g. via the command line).


conflicts

⪢ API reference: conflicts

[!IMPORTANT]

conflicts is a superset of vanilla Yargs's conflicts.

conflicts enables checks to ensure the specified arguments, or argument-value pairs, are never given conditioned on the existence of another argument. For example:

{
  "x": { "conflicts": "y" }, // ◄ Disallows y if x is given
  "y": {}
}

This configuration will trigger a check to ensure that -⁠y is never given whenever -⁠x is given.

conflicts also supports checks against the parsed values of arguments in addition to the argument existence checks demonstrated above. For example:

{
  // ▼ Disallows y == 'one' or z if x is given
  "x": { "conflicts": [{ "y": "one" }, "z"] },
  "y": {},
  "z": { "conflicts": "y" } // ◄ Disallows y if z is given
}

This configuration allows the following arguments: no arguments (), -⁠y=..., -⁠x, -⁠z, -⁠x -⁠y=...; and disallows: -⁠y=... -⁠z, -⁠x -⁠y=one, -⁠xz -⁠y=one, -⁠xz.

Note that, when performing a check using the parsed value of an argument and that argument is configured as an array ({ array: true }), that array will be searched for said value. Otherwise, a strict deep equality check is performed.

conflicts versus implies

Choose BFE's implies over conflicts when you want the existence of one argument to override the default/given value of another argument while not preventing the two arguments from being given simultaneously.


implies

⪢ API reference: implies

[!IMPORTANT]

BFE's implies replaces vanilla Yargs's implies in a breaking way. The two implementations are nothing alike. If you're looking for vanilla Yargs's functionality, see requires.

implies will set a default value for the specified arguments conditioned on the existence of another argument. This will override the default value of the specified arguments.

Unless looseImplications is set to true, if any of the specified arguments are explicitly given in argv (e.g. via the command line), their values must match the specified argument-value pairs respectively (similar to requires/conflicts). For this reason, implies only accepts one or more argument-value pairs and not raw strings. For example:

{
  "x": { "implies": { "y": true } }, // ◄ x becomes synonymous with xy
  "y": {}
}

This configuration makes it so that -⁠x and -⁠x -⁠y=true result in the exact same argv. Further, unlike requires, implies makes no demands on argument existence and so allows the following arguments: no arguments (), -⁠x, -⁠y=true, -⁠y=false, -⁠x -⁠y=true; and disallows: -⁠x -⁠y=false.

Note that attempting to imply a value for a non-existent option will throw a framework error.

Additionally, if any of the specified arguments have their own defaults configured, said defaults will be overridden by the values of implies. For example:

{
  "x": { "implies": { "y": true } },
  "y": { "default": false } // ◄ y will still default to true if x is given
}

Also note the special behavior of implies specifically in the case where an argument value in argv is strictly equal to false.

For describing much more intricate implications between various arguments and their values, see subOptionOf.

Handling Transitive Implications

implies configurations do not cascade transitively. This means if argument P implies argument Q, and argument Q implies argument R, and P is given, the only check that will be performed is on P and Q. If P must imply some value for both Q and R, specify this explicitly in P's configuration. For example:

{
- P: { "implies": { Q: true } },
+ P: { "implies": { Q: true, R: true } },
  Q: { "implies": { R: true } },
  R: {}
}

This has implications beyond just implies. An implied value will not transitively satisfy any other BFE logic checks (such as demandThisOptionXor) or trigger any relational behavior (such as with subOptionOf). The implied argument-value pair will simply be merged into argv as if you had done it manually in your command's handler. If this is a problem, prefer the explicit direct relationships described by other configuration keys instead of relying on the implicit transitive relationships described by implies.

Despite this constraint, any per-option checks you've configured, which are run last (at the very end of withHandlerExtensions), will see the implied argument-value pairs. Therefore, use check to guarantee any complex invariants, if necessary; ideally, you shouldn't be setting bad defaults via implies, but BFE won't stop you from doing so.

Handling Parser Configuration

Like other BFE checks, implies does take into account the yargs-parser settings camel-case-expansion, strip-aliased, and strip-dashed; but does not currently pay attention to dot-notation or duplicate-arguments-array. implies may still work when using the latter parser configurations, but it is recommended you turn them off instead.

implies versus requires/conflicts

BFE's implies, since it sets arguments in argv if they are not explicitly given, is a weaker form of requires/conflicts.

Choose requires over BFE's implies when you want one argument to imply the value of another while requiring the other argument to be explicitly given in argv (e.g. via the command line).

Choose conflicts over BFE's implies when you think you want to use implies but you don't actually need to override the default value of the implied argument and only want the conflict semantics.

Alternatively, choose subOptionOf over BFE's implies when you want the value of one argument to imply something complex about another argument and/or its value, such as updating the other argument's options configuration.

looseImplications

If looseImplications is set to true, any of the specified arguments, when explicitly given in argv (e.g. via the command line), will override any configured implications instead of causing an error. When looseImplications is set to false, which is the default, values explicitly given in argv must match the specified argument-value pairs respectively (similar to requires/conflicts).

vacuousImplications

By default, an option's configured implications will only take effect if said option is given in argv with a non-false value. For example:

{
  "x": {
    "boolean": true,
    "implies": { "y": true }
  },
  "y": {
    // This example works regardless of the type of y!
    "boolean": true,
    //"array": true,
    //"count": true,
    //"number": true,
    //"string": true,
    "default": false
  }
}

If -⁠x (or -⁠x=true) is given, it is synonymous with -⁠x -⁠y (or -⁠x=true -⁠y=true) being given and vice-versa. However, if -⁠x=false (or -⁠no-x) is given, the implies key is effectively ignored. This means -⁠x=false does not imply anything about -⁠y; -⁠x=false -y=true and -⁠x=false -y=false are both accepted by BFE without incident.

In this way, the configured implications of boolean-type options are never vacuously satisfied; a strictly false condition does not "imply" anything about its consequent.

This feature reduces confusion for end users. For instance, suppose we had a CLI build tool that accepted the arguments -⁠patch and -⁠only-⁠patch. -⁠patch instructs the tool to patch any output before committing it to disk while -⁠only-⁠patch instructs the tool to only patch pre-existing output already on disk. The command's options configuration could look something like the following:

{
  "patch": {
    "boolean": true,
    "description": "Patch output using the nearest patcher file",
    "default": true
  },
  "only-patch": {
    "boolean": true,
    "description": "Instead of building new output, only patch existing output",
    "default": false,
    "implies": { "patch": true }
  }
}

The following are rightly allowed by BFE (synonymous commands are grouped):

Is building and patching:

  • build-tool
  • build-tool -⁠patch
  • build-tool -⁠patch=true
  • build-tool -⁠only-⁠patch=false
  • build-tool -⁠no-⁠only-⁠patch

Is building and not patching:

  • build-tool -⁠patch=false
  • build-tool -⁠no-⁠patch
  • build-tool -⁠no-⁠patch -⁠no-⁠only-⁠patch (this is the interesting one)

Is patching and not building:

  • build-tool -⁠only-⁠patch
  • build-tool -⁠only-⁠patch=true
  • build-tool -⁠patch -⁠only-⁠patch

On the other hand, the following rightly cause BFE to throw:

  • build-tool -⁠patch=false -⁠only-⁠patch
  • build-tool -⁠no-⁠patch -⁠only-⁠patch

If BFE didn't ignore vacuous implications by default, the command build-tool -⁠no-⁠patch -⁠no-⁠only-⁠patch would erroneously cause BFE to throw since implies: { patch: true } means "any time -⁠only-⁠patch is given, set { patch: true } in argv", which conflicts with -⁠no-⁠patch which already sets { patch: false } in argv. This can be confusing for end users since the command, while redundant, technically makes sense; it is logically indistinguishable from build-tool -⁠no-⁠only-patch, which does not throw an error.

To remedy this, BFE simply ignores the implies configurations of options when their argument value is strictly equal to false in argv. To disable this behavior for a specific option, set vacuousImplications to true (it is false by default) or consider using requires/conflicts/subOptionOf over implies.


demandThisOptionIf

⪢ API reference: [demandThisOptionIf][35]

[!IMPORTANT]

demandThisOptionIf is a superset of vanilla Yargs's demandOption.

demandThisOptionIf enables checks to ensure an argument is given when at least one of the specified groups of arguments, or argument-value pairs, is also given. For example:

{
  "x": {},
  "y": { "demandThisOptionIf": "x" }, // ◄ Demands y if x is given
  "z": { "demandThisOptionIf": "x" } // ◄ Demands z if x is given
}

This configuration allows the following arguments: no arguments (), -⁠y, -⁠z, -⁠yz, -⁠xyz; and disallows: -⁠x, -⁠xy, -⁠xz.

demandThisOptionIf also supports checks against the parsed values of arguments in addition to the argument existence checks demonstrated above. For example:

{
  // ▼ Demands x if y == 'one' or z is given
  "x": { "demandThisOptionIf": [{ "y": "one" }, "z"] },
  "y": {},
  "z": {}
}

This configuration allows the following arguments: no arguments (), -⁠x, -⁠y=..., -⁠x -⁠y=..., -⁠xz, -⁠xz y=...; and disallows: -⁠z, -⁠y=one, -⁠y=... -⁠z.

Note that, when performing a check using the parsed value of an argument and that argument is configured as an array ({ array: true }), that array will be searched for said value. Otherwise, a strict deep equality check is performed.

Also note that a more powerful implementation of demandThisOptionIf can be achieved via subOptionOf.


demandThisOption

⪢ API reference: [demandThisOption][36]

[!IMPORTANT]

demandThisOption is an alias of vanilla Yargs's demandOption. demandOption is disallowed by intellisense.

demandThisOption enables checks to ensure an argument is always given. This is equivalent to demandOption from vanilla Yargs. For example:

{
  "x": { "demandThisOption": true }, // ◄ Disallows ∅, y
  "y": { "demandThisOption": false }
}

This configuration will trigger a check to ensure that -⁠x is given.

[!NOTE]

As an alias of vanilla Yargs's demandOption, this check is outsourced to Yargs, which means it runs on Black Flag's first and second parsing passes like any other configurations key coming from vanilla Yargs.


demandThisOptionOr

⪢ API reference: [demandThisOptionOr][37]

[!IMPORTANT]

demandThisOptionOr is a superset of vanilla Yargs's demandOption.

demandThisOptionOr enables non-optional inclusive disjunction checks per group. Put another way, demandThisOptionOr enforces a "logical or" relation within groups of required options. For example:

{
  "x": { "demandThisOptionOr": ["y", "z"] }, // ◄ Demands x or y or z
  "y": { "demandThisOptionOr": ["x", "z"] }, // ◄ Mirrors the above (discarded)
  "z": { "demandThisOptionOr": ["x", "y"] } // ◄ Mirrors the above (discarded)
}

This configuration will trigger a check to ensure at least one of x, y, or z is given. In other words, this configuration allows the following arguments: -⁠x, -⁠y, -⁠z, -⁠xy, -⁠xz, -⁠yz, -⁠xyz; and disallows: no arguments ().

In the interest of readability, consider mirroring the appropriate demandThisOptionOr configuration to the other relevant options, though this is not required (redundant groups are discarded). The previous example demonstrates proper mirroring.

demandThisOptionOr also supports checks against the parsed values of arguments in addition to the argument existence checks demonstrated above. For example:

{
  // ▼ Demands x or y == 'one' or z
  "x": { "demandThisOptionOr": [{ "y": "one" }, "z"] },
  "y": {},
  "z": {}
}

This configuration allows the following arguments: -⁠x, -⁠y=one, -⁠z, -⁠x -⁠y=..., -⁠xz, -⁠y=... -⁠z, -⁠xz -⁠y=...; and disallows: no arguments (), -⁠y=....

Note that, when performing a check using the parsed value of an argument and that argument is configured as an array ({ array: true }), that array will be searched for said value. Otherwise, a strict deep equality check is performed.


demandThisOptionXor

⪢ API reference: [demandThisOptionXor][38]

[!IMPORTANT]

demandThisOptionXor is a superset of vanilla Yargs's demandOption + conflicts.

demandThisOptionXor enables non-optional exclusive disjunction checks per exclusivity group. Put another way, demandThisOptionXor enforces mutual exclusivity within groups of required options. For example:

{
  "x": { "demandThisOptionXor": ["y"] }, // ◄ Disallows ∅, z, w, xy, xyw, xyz, xyzw
  "y": { "demandThisOptionXor": ["x"] }, // ◄ Mirrors the above (discarded)
  "z": { "demandThisOptionXor": ["w"] }, // ◄ Disallows ∅, x, y, zw, xzw, yzw, xyzw
  "w": { "demandThisOptionXor": ["z"] } // ◄ Mirrors the above (discarded)
}

This configuration will trigger a check to ensure exactly one of -⁠x or -⁠y is given, and exactly one of -⁠z or -⁠w is given. In other words, this configuration allows the following arguments: -⁠xz, -⁠xw, -⁠yz, -⁠yw; and disallows: no arguments (), -⁠x, -⁠y, -⁠z, -⁠w, -⁠xy, -⁠zw, -⁠xyz, -⁠xyw, -⁠xzw, -⁠yzw, -⁠xyzw.

In the interest of readability, consider mirroring the appropriate demandThisOptionXor configuration to the other relevant options, though this is not required (redundant groups are discarded). The previous example demonstrates proper mirroring.

demandThisOptionXor also supports checks against the parsed values of arguments in addition to the argument existence checks demonstrated above. For example:

{
  // ▼ Demands x xor y == 'one' xor z
  "x": { "demandThisOptionXor": [{ "y": "one" }, "z"] },
  "y": {},
  "z": {}
}

This configuration allows the following arguments: -⁠x, -⁠y=one, -⁠z, -⁠x -⁠y=..., -⁠y=... -⁠z; and disallows: no arguments (), -⁠y=..., -⁠x -⁠y=one, -⁠xz, -⁠y=one -⁠z, -⁠xz -⁠y=....

Note that, when performing a check using the parsed value of an argument and that argument is configured as an array ({ array: true }), that array will be searched for said value. Otherwise, a strict deep equality check is performed.


check

⪢ API reference: check

check is the declarative option-specific version of vanilla Yargs's yargs::check().

This function receives the currentArgumentValue, which you are free to type as you please, and the fully parsed argv. If this function throws, the exception will bubble. If this function returns an instance of Error, a string, or any non-truthy value (including undefined or not returning anything), Black Flag will throw a CliError on your behalf.

All check functions are run in definition order and always at the very end of the [second parsing pass][11], well after all other BFE checks have passed and all updates to argv have been applied (including from subOptionOf and BFE's implies). This means check always sees the final version of argv, which is the same version that the command's handler is passed.

[!IMPORTANT]

check functions are skipped if their corresponding argument does not exist in argv.

When a check fails, execution of its command's handler function will cease and configureErrorHandlingEpilogue will be invoked (unless you threw/returned a GracefulEarlyExitError). For example:

export const [builder, withHandlerExtensions] = withBuilderExtensions({
  x: {
    number: true,
    check: function (currentXArgValue, fullArgv) {
      if (currentXArgValue < 0 || currentXArgValue > 10) {
        throw new Error(
          `"x" must be between 0 and 10 (inclusive), saw: ${currentXArgValue}`
        );
      }

      return true;
    }
  },
  y: {
    boolean: true,
    default: false,
    requires: 'x',
    check: function (currentYArgValue, fullArgv) {
      if (currentYArgValue && fullArgv.x <= 5) {
        throw new Error(
          `"x" must be greater than 5 to use 'y', saw: ${fullArgv.x}`
        );
      }

      return true;
    }
  }
});

You may also pass an array of check functions, each being executed after the other. This makes it easy to reuse checks between options. For example:

[!WARNING]

Providing an array with one or more async check functions will result in them all being awaited concurrently.

export const [builder, withHandlerExtensions] = withBuilderExtensions({
  x: {
    number: true,
    check: [checkArgBetween0And10('x'), checkArgGreaterThan5('x')]
  },
  y: {
    number: true,
    check: checkArgBetween0And10('y')
  },
  z: {
    number: true,
    check: checkArgGreaterThan5('z')
  }
});

function checkArgBetween0And10(argName) {
  return function (argValue, fullArgv) {
    return (
      (argValue >= 0 && argValue <= 10) ||
      `"${argName}" must be between 0 and 10 (inclusive), saw: ${argValue}`
    );
  };
}

function checkArgGreaterThan5(argName) {
  return function (argValue, fullArgv) {
    return (
      argValue > 5 || `"${argName}" must be greater than 5, saw: ${argValue}`
    );
  };
}

See the Yargs documentation on yargs::check() for more information.


subOptionOf

⪢ API reference: subOptionOf

One of Black Flag's killer features is native support for dynamic options. However, taking advantage of this feature in a command's builder export requires a strictly imperative approach.

Take, for example, the init command from @black-flag/demo:

// Taken at 03/23/2025 from @black-flag/demo "myctl" CLI

const PYTHON_DEFAULT_VERSION = '3.13';
const NODE_DEFAULT_VERSION = '23.3';

export function builder(yargs, _helpOrVersionSet, argv) {
  // Tell Yargs to leave strings that look like numbers as strings
  yargs.parserConfiguration({ 'parse-numbers': false });

  // ▼ This imperative logic is a bit of an eyesore...
  if (argv?.lang === 'node') {
    return {
      lang: { choices: ['node'], default: 'node' },
      version: {
        choices: ['20.18', '22.12', '23.3'],
        default: NODE_DEFAULT_VERSION
      }
    };
  } else if (argv?.lang === 'python') {
    return {
      lang: { choices: ['python'], default: 'python' },
      version: {
        choices: ['3.11', '3.12', '3.13'],
        default: PYTHON_DEFAULT_VERSION
      }
    };
  }

  return {
    lang: {
      choices: ['node', 'python'],
      // ▼ Having to use a default description is a little suboptimal...
      defaultDescription: '"python"',
      // ▼ This imperative logic is a little ugly...
      default: argv ? 'python' : undefined
    },
    version: {
      string: true,
      // ▼ Having to use a default description is a little suboptimal...
      defaultDescription: `"${PYTHON_DEFAULT_VERSION}"`,
      // ▼ This imperative logic is a little ugly...
      default: argv ? PYTHON_DEFAULT_VERSION : undefined
    }
  };
}

export function handler(argv) {
  console.log(`> initializing new ${argv.lang}@${argv.version} project...`);
  // ...
}

Taking advantage of dynamic options support like like we did above gifts your CLI with help text more meaningful than anything you could accomplish with vanilla Yargs.

For example:

myctl init --lang node --version=23.3
> initializing new node@23.3 project...
myctl init --lang python --version=23.3
Usage: myctl init

Options:
  --help     Show help text                                            [boolean]
  --lang                                 [choices: "python"] [default: "python"]
  --version                  [choices: "3.11", "3.12", "3.13"] [default: "3.13"]

Invalid values:
  Argument: version, Given: "23.3", Choices: "3.10", "3.11", "3.12"
myctl init --lang fake
Usage: myctl init

Options:
  --help     Show help text                                            [boolean]
  --lang                         [choices: "node", "python"] [default: "python"]
  --version                                           [string] [default: "3.13"]

Invalid values:
  Argument: lang, Given: "fake", Choices: "node", "python"
myctl init --help
Usage: myctl init

Options:
  --help     Show help text                                            [boolean]
  --lang                         [choices: "node", "python"] [default: "python"]
  --version                                           [string] [default: "3.13"]
myctl init --lang node --help
Usage: myctl init

Options:
  --help     Show help text                                            [boolean]
  --lang                                     [choices: "node"] [default: "node"]
  --version                [choices: "20.18", "22.12", "23.3"] [default: "23.3"]

Ideally, Black Flag would allow us to describe the relationship between -⁠-⁠lang and its suboption -⁠-⁠version declaratively, without having to drop down to imperative interactions with the Yargs API like we did above.

This is the goal of the subOptionOf configuration key. Using subOptionOf, developers can take advantage of dynamic options without sweating the implementation details.

[!NOTE]

subOptionOf updates are run and applied during Black Flag's [second parsing pass][11].

For example:

/**
 * @type {import('@black-flag/core').Configuration['builder']}
 */
export const [builder, withHandlerExtensions] = withBuilderExtensions({
  x: {
    choices: ['a', 'b', 'c'],
    demandThisOption: true,
    description: 'A choice'
  },
  y: {
    number: true,
    description: 'A number'
  },
  z: {
    // ▼ These configurations are applied as the baseline or "fallback" during
    //   Black Flag's first parsing pass. The updates within subOptionOf are
    //   evaluated and applied during Black Flag's second parsing pass.
    boolean: true,
    description: 'A useful context-sensitive flag',
    subOptionOf: {
      // ▼ Ignored if x is not given
      x: [
        {
          when: (currentXArgValue, fullArgv) => currentXArgValue === 'a',
          update:
            // ▼ We can pass an updater function that returns an opt object.
            //   This object will *replace* the argument's old configuration!
            (oldXArgumentConfig, fullArgv) => {
              return {
                // ▼ We don't want to lose the old config, so we spread it
                ...oldXArgumentConfig,
                description: 'This is a switch specifically for the "a" choice'
              };
            }
        },
        {
          when: (currentXArgValue, fullArgv) => currentXArgValue !== 'a',
          update:
            // ▼ Or we can just pass the replacement configuration object. Note
            //   that, upon multiple `when` matches, the last update in the
            //   chain will win. If you want merge behavior instead of overwrite,
            //   spread the old config in the object you return.
            {
              string: true,
              description: 'This former-flag now accepts a string instead'
            }
        }
      ],
      // ▼ Ignored if y is not given. If x and y ARE given, since this occurs
      //   after the x config, this update will overwrite any others. Use the
      //   functional form + object spread to preserve the old configuration.
      y: {
        when: (currentYArgValue, fullArgv) =>
          fullArgv.x === 'a' && currentYArgValue > 5,
        update: (oldConfig, fullArgv) => {
          return {
            array: true,
            demandThisOption: true,
            description:
              'This former-flag now accepts an array of two or more strings',
            check: function (currentZArgValue, fullArgv) {
              return (
                currentZArgValue.length >= 2 ||
                `"z" must be an array of two or more strings, only saw: ${currentZArgValue.length ?? 0}`
              );
            }
          };
        }
      },
      // ▼ Since "does-not-exist" is not an option defined anywhere, this will
      //   always be ignored
      'does-not-exist': []
    }
  }
});

[!IMPORTANT]

You cannot nest subOptionOf keys within each other nor return an object containing subOptionOf from an update that did not already have one. Doing so will trigger a framework error.

Now we're ready to re-implement the init command from myctl using our new declarative superpowers:

export const [builder, withHandlerExtensions] = withBuilderExtensions(
  function (blackFlag) {
    blackFlag.parserConfiguration({ 'parse-numbers': false });

    return {
      lang: {
        // ▼ These two are fallback or "baseline" configurations for --lang
        choices: ['node', 'python'],
        default: 'python',

        subOptionOf: {
          // ▼ Yep, --lang is also a suboption of --lang
          lang: [
            {
              when: (lang) => lang === 'node',
              // ▼ Remember: updates completely overwrite baseline config...
              update: {
                choices: ['node'],
                default: 'node'
              }
            },
            {
              when: (lang) => lang !== 'node',
              // ▼ ... though we can still reuse the "old" baseline config
              update(oldOptionConfig) {
                return {
                  ...oldOptionConfig,
                  choices: ['python']
                };
              }
            }
          ]
        }
      },

      version: {
        // ▼ These two are fallback or "baseline" configurations for --version
        string: true,
        default: '3.13',

        subOptionOf: {
          // ▼ --version is a suboption of --lang
          lang: [
            {
              when: (lang) => lang === 'node',
              update: {
                choices: ['20.18', '22.12', '23.3'],
                default: '23.3'
              }
            },
            {
              when: (lang) => lang !== 'node',
              update(oldOptionConfig) {
                return {
                  ...oldOptionConfig,
                  choices: ['3.11', '3.12', '3.13']
                };
              }
            }
          ]
        }
      }
    };
  }
);

Easy peasy!

Another benefit of subOptionOf: all configuration relevant to an option is co-located within that option and not spread across some function or file. We don't have to go looking for the logic that's modifying --version since it's all right there in one code block. We also don't have to repeat ourselves or pass around X_DEFAULT_VERSION variables to hold defaults anymore!

See the examples directory for more subOptionOf demonstrations, including a fleshed out version of myctl implemented using BFE.

Support for default with conflicts/requires/etc

BFE (and, consequently, BF/Yargs when not generating help text) will ignore the existence of the default key until near the end of BFE's execution.

[!IMPORTANT]

This means the optional customBuilder function passed to withBuilderExtensions will not see any defaulted values. However, your command handlers will.

[!WARNING]

An explicitly undefined default, i.e. { default: undefined }, will be deleted from the configuration object by BFE and completely ignored by Black Flag and Yargs. This differs from BF/Yargs's default behavior, which is to recognize undefined defaults.

Defaults are set before any check functions are run, before any implications are set, and before the relevant command handler is invoked, but after all other BFE checks have succeeded. This enables the use of keys like requires and conflicts alongside default without causing impossible configurations that throw unresolvable CLI errors.

This workaround avoids a (in my opinion) rather unintuitive Yargs footgun, though there are decent arguments in support of vanilla Yargs's behavior.

Strange and Impossible Configurations

Note that there are no sanity checks performed to prevent options configurations that are unresolvable, so care must be taken not to ask for something insane.

For example, the following configurations are impossible to resolve:

{
  "x": { "requires": "y" },
  "y": { "conflicts": "x" }
}
{
  "x": { "requires": "y", "demandThisOptionXor": "y" },
  "y": {}
}

Similarly, silly configurations like the following, while typically resolvable, are strange and may not work as expected:

{
  "x": { "requires": "x", "demandThisOptionXor": "x" }
}
{
  "x": { "implies": { "x": 5 } }
}

Automatic Grouping of Related Options

[!CAUTION]

To support this functionality, options must be described declaratively. Defining options imperatively will break this feature.

BFE supports automatic grouping of related options for improved UX, which is enabled by default. These new groups are:

  • "Required Options": options configured with demandThisOption.
  • "Required Options (at least one)": options configured with demandThisOptionOr.
  • "Required Options (mutually exclusive)": options configured with demandThisOptionXor.
  • "Common Options": options provided via { commonOptions: [...] } to withBuilderExtensions as its second parameter: withBuilderExtensions({/*...*/}, { commonOptions });
  • "Optional Options": remaining options that do not fall into any of the above categories.

An example from xunnctl:

$ x f b --help
Usage: xunnctl firewall ban

Add an IP from the global hostile IP list.

Required Options:
  --ip  An ipv4, ipv6, or supported CIDR                                                        [array]

Optional Options:
  --comment  Include custom text with the ban comment where applicable                         [string]

Common Options:
  --help         Show help text                                                               [boolean]
  --hush         Set output to be somewhat less verbose                      [boolean] [default: false]
  --quiet        Set output to be dramatically less verbose (implies --hush) [boolean] [default: false]
  --silent       No output will be generated (implies --quiet)               [boolean] [default: false]
  --config-path  Use a custom configuration file
                                [string] [default: "/home/freelance/.config/xunnctl-nodejs/state.json"]
$ x d z u --help
Usage: xunnctl dns zone update

Reinitialize a DNS zones.

Required Options (at least one):
  --apex            Zero or more zone apex domains                                              [array]
  --apex-all-known  Include all known zone apex domains                                       [boolean]

Optional Options:
  --force        Disable protections                                                          [boolean]
  --purge-first  Delete pertinent records on the zone before recreating them                  [boolean]

Common Options:
  --help         Show help text                                                               [boolean]
  --hush         Set output to be somewhat less verbose                      [boolean] [default: false]
  --quiet        Set output to be dramatically less verbose (implies --hush) [boolean] [default: false]
  --silent       No output will be generated (implies --quiet)               [boolean] [default: false]
  --config-path  Use a custom configuration file
                                [string] [default: "/home/freelance/.config/xunnctl-nodejs/state.json"]

By including an explicit group property in an option's configuration, the option will be included in said group in addition to the result of automatic grouping, e.g.:

const [builder, withHandlerExtensions] = withBuilderExtensions({
  'my-option': {
    boolean: true,
    description: 'mine',
    default: true,
    // This option will be placed into the "Custom Grouped Options" group AND
    // ALSO the "Common Options" group IF it's included in `commonOptions`
    group: 'Custom Grouped Options'
  }
});

[!NOTE]

Options configured with an explicit group property will never be automatically included in the "Optional Options" group.

This feature can be disabled entirely by passing { disableAutomaticGrouping: true } to withBuilderExtensions as its second parameter:

const [builder, withHandlerExtensions] = withBuilderExtensions(
  {
    // ...
  },
  { disableAutomaticGrouping: true }
);

Automatic Sorting of Options

[!CAUTION]

To support this functionality, options must be described declaratively. Defining options imperatively will break this feature.

BFE supports automatic alpha-sorting of a command's options in help text for improved UX, similar to how Black Flag sorts commands themselves in help text.

This feature is disabled by default, but can be enabled by passing { enableAutomaticSorting: true } to withBuilderExtensions as its second parameter:

const [builder, withHandlerExtensions] = withBuilderExtensions(
  {
    // ...
  },
  { enableAutomaticSorting: true }
);

withUsageExtensions

⪢ API reference: withUsageExtensions

This thin wrapper function is used for more consistent and opinionated usage string generation.

// file: xunnctl/commands/firewall/ban.js
return {
  // ...
  description: 'Add an IP from the global hostile IP list',
  usage: withUsageExtensions(
    "$1.\n\nAdditional description text that only appears in this command's help text."
  )
};
$ x f b --help
Usage: xunnctl firewall ban

Add an IP from the global hostile IP list.

Additional description text that only appears in this command's help text.

Required Options:
  --ip  An ipv4, ipv6, or supported CIDR                                                        [array]

Optional Options:
  --comment  Include custom text with the ban comment where applicable                         [string]

Common Options:
  --help         Show help text                                                               [boolean]
  --hush         Set output to be somewhat less verbose                      [boolean] [default: false]
  --quiet        Set output to be dramatically less verbose (implies --hush) [boolean] [default: false]
  --silent       No output will be generated (implies --quiet)               [boolean] [default: false]
  --config-path  Use a custom configuration file
                                [string] [default: "/home/freelance/.config/xunnctl-nodejs/state.json"]

getInvocableExtendedHandler

⪢ API reference: getInvocableExtendedHandler

Unlike Black Flag, BFE puts strict constraints on the order in which command exports must be invoked and evaluated. Specifically: an extended command's builder export must be invoked twice, with the correct parameters each time, before that extended command's handler can be invoked.

This can make it especially cumbersome to import an extended command from a file and then invoke its handler, which is dead simple for normal Black Flag commands, and can introduce transitive tight-couplings between commands, which makes bugs more likely and harder to spot.

getInvocableExtendedHandler solves this by returning a version of the extended command's handler function that is ready to invoke immediately. Said handler expects a single argv parameter which is "safely" cloned, merged with several defaults (see API reference), and then passed-through to your command's handler as-is.

One of those defaults is the value of the context parameter that was supplied to getInvocableExtendedHandler. Similar to argv, context will be "safely" cloned.

[!TIP]

A "safe" clone is a [StructuredClone-like operation][52] that passes through as-is any values that cannot be cloned rather than throwing an error. Since BFE is leveraging [safeDeepClone][52] under the hood, all clone operations can be tweaked by configuring [context.state.extensions.transfer][53] appropriately (where context is the ExecutionContext instance passed to getInvocableExtendedHandler).

Setting context.state.extensions.transfer is useful when, for instance, you have an object stored in context that should not be deep cloned but passed through as-is instead.

[!TIP]

Command handler exports invoked via getInvocableExtendedHandler will receive an argv containing the $artificiallyInvoked symbol. This allows handlers to determine programmatically when the command isn't actually being invoked by Black Flag, which can be useful.

However, to get intellisense/TypeScript support for the existence of $artificiallyInvoked in argv, you must use BfeStrictArguments.

[!CAUTION]

Command handler exports invoked via getInvocableExtendedHandler will never check the given argv for correctness or update any of its keys/values (aside from setting $artificiallyInvoked, $executionContext, and defaults for $0 and _ if they are omitted).

By invoking a command's handler function outside of Black Flag, you're essentially treating it like a normal function. And all handler functions require a "reified argv" parameter, i.e. the object given to a command handler after all BF/BFE checks have passed and all updates to argv have been applied.

If you want to invoke a full Black Flag command programmatically, use [runProgram][58]. If instead you want to call an individual command's (relatively) lightweight handler function directly, use getInvocableExtendedHandler.

getInvocableExtendedHandler can be used with both BFE and normal Black Flag command exports.

For example, in JavaScript:

// file: my-cli/commands/command-B.js
export default function command(context) {
  const [builder, withHandlerExtensions] = withBuilderExtensions({
    // ...
  });

  return {
    builder,
    handler: withHandlerExtensions(async function (argv) {
      const handler = await getInvocableExtendedHandler(
        // This accepts a function, an object, a default export, a Promise, etc
        import('./command-A.js'),
        context
      );

      await handler({ somethingElse: true });

      // ...
    })
  };
}

Or in TypeScript:

// file: my-cli/commands/command-B.ts
import { type CustomExecutionContext } from '../configure';

import {
  default as commandA,
  type CustomCliArguments as CommandACliArguments
} from './command-A';

export type CustomCliArguments = {
  /* ... */
};

export default function command(context: CustomExecutionContext) {
  const [builder, withHandlerExtensions] =
    withBuilderExtensions<CustomCliArguments>({
      // ...
    });

  return {
    builder,
    handler: withHandlerExtensions<CustomCliArguments>(async function (argv) {
      const handler = await getInvocableExtendedHandler<
        CommandACliArguments,
        typeof context
      >(commandA, context);

      await handler({ somethingElse: true });

      // ...
    })
  };
}

Examples

See the examples directory.

Appendix 🏴

Further documentation can be found under docs/.

Differences between Black Flag Extensions and Yargs

When using BFE, several options function differently, such as implies. Other options have their effect deferred, like default. [coerce][59] will always receive an array when the same option also has array: true. See the configuration keys section for a list of changes and their justifications.

Additionally, command options must be configured by returning an opt object from your command's builder rather than imperatively invoking the Yargs API.

For example:

export function builder(blackFlag) {
- // DO NOT use Yargs's imperative API to define options! This *BREAKS* BFE!
- blackFlag.option('f', {
-   alias: 'file',
-   demandOption: true,
-   default: '/etc/passwd',
-   describe: 'x marks the spot',
-   type: 'string',
-   group: 'custom'
- });
-
- // DO NOT use Yargs's imperative API to define options! This *BREAKS* BFE!
- blackFlag
-   .alias('f', 'file')
-   .demandOption('f')
-   .default('f', '/etc/passwd')
-   .describe('f', 'x marks the spot')
-   .string('f')
-   .group('custom');
-
- // DO NOT use Yargs's imperative API to define options! This *BREAKS* BFE!
- blackFlag.options({
-   f: {
-     alias: 'file',
-     demandOption: true,
-     default: '/etc/passwd',
-     describe: 'x marks the spot',
-     type: 'string',
-     group: 'custom'
-   }
- });
-
+ // INSTEAD, use Yargs / Black Flag's declarative API to define options 🙂
+ return {
+   f: {
+     alias: 'file',
+     demandThisOption: true,
+     default: '/etc/passwd',
+     describe: 'x marks the spot',
+     string: true,
+     group: 'custom'
+   }
+ };
}

[!TIP]

The Yargs API can and should still be invoked for purposes other than defining options on a command, e.g. blackFlag.strict(false).

To this end, the following [Yargs API functions][60] are soft-disabled via intellisense:

  • option
  • options

However, no attempt is made by BFE to restrict your use of the Yargs API at runtime. Therefore, using Yargs's API to work around these artificial limitations, e.g. in your command's builder function or via the [configureExecutionPrologue][61] hook, will result in undefined behavior.

Black Flag versus Black Flag Extensions

The goal of [Black Flag (@black-flag/core)][62] is to be as close to a drop-in replacement as possible for vanilla Yargs, specifically for users of [yargs::commandDir()][63]. This means Black Flag must go out of its way to maintain 1:1 parity with the vanilla Yargs API ([with a few minor exceptions][64]).

As a consequence, Yargs's imperative nature tends to leak through Black Flag's abstraction at certain points, such as with the blackFlag parameter of the builder export. This is a good thing! Since we want access to all of Yargs's killer features without Black Flag getting in the way.

However, this comes with costs. For one, the Yargs's API has suffered from a bit of feature creep over the years. A result of this is a rigid API [with][65] an [abundance][66] [of][67] [footguns][68] and an [inability][69] to [address][70] them without introducing [massively][71] [breaking][72] [changes][73].

BFE takes the "YOLO" approach by exporting several functions that build on top of Black Flag's feature set without worrying too much about maintaining 1:1 parity with the vanilla Yargs's API. This way, one can opt-in to a more opinionated but (in my opinion) cleaner, more consistent, and more intuitive developer experience.

Published Package Details

This is a [CJS2 package][x-pkg-cjs-mojito] with statically-analyzable exports built by Babel for use in Node.js versions that are not end-of-life. For TypeScript users, this package supports both "Node10" and "Node16" module resolution strategies.

<summary>Expand details</summary> That means both CJS2 (via require(...)) and ESM (via import { ... } from ... or await import(...)) source will load this package from the same entry points when using Node. This has several benefits, the foremost being: less code shipped/smaller package size, avoiding [dual package hazard][x-pkg-dual-package-hazard] entirely, distributables are not packed/bundled/uglified, a drastically less complex build process, and CJS consumers aren't shafted. Each entry point (i.e. ENTRY) in package.json's exports[ENTRY] object includes one or more [export conditions][x-pkg-exports-conditions]. These entries may or may not include: an [exports[ENTRY].types][x-pkg-exports-types-key] condition pointing to a type declaration file for TypeScript and IDEs, a [exports[ENTRY].module][x-pkg-exports-module-key] condition pointing to (usually ESM) source for Webpack/Rollup, a exports[ENTRY].node and/or exports[ENTRY].default condition pointing to (usually CJS2) source for Node.js require/import and for browsers and other environments, and [other conditions][x-pkg-exports-conditions] not enumerated here. Check the package.json file to see which export conditions are supported. Note that, regardless of the [{ "type": "..." }][x-pkg-type] specified in package.json, any JavaScript files written in ESM syntax (including distributables) will always have the .mjs extension. Note also that package.json may include the [sideEffects][x-pkg-side-effects-key] key, which is almost always false for optimal tree shaking where appropriate.

License

See LICENSE.

Contributing and Support

[New issues][x-repo-choose-new-issue] and pull requests are always welcome and greatly appreciated! 🤩 Just as well, you can star 🌟 this project to let me know you found it useful! ✊🏿 Or buy me a beer, I'd appreciate it. Thank you!

See CONTRIBUTING.md and SUPPORT.md for more information.

Contributors

See the table of contributors.

[x-badge-blm-image]: https://xunn.at/badge-blm 'Join the movement!'

[x-badge-codecov-image]: https://img.shields.io/codecov/c/github/Xunnamius/black-flag/main?style=flat-square&token=HWRIOBAAPW&flag=package.main_extensions 'Is this package well-tested?'

[x-badge-downloads-image]: https://img.shields.io/npm/dm/@black-flag/extensions?style=flat-square 'Number of times this package has been downloaded per month'

[x-badge-lastcommit-image]: https://img.shields.io/github/last-commit/Xunnamius/black-flag?style=flat-square 'Latest commit timestamp' [x-badge-license-image]: https://img.shields.io/npm/l/@black-flag/extensions?style=flat-square "This package's source license" [x-badge-license-link]: https://github.com/Xunnamius/black-flag/blob/main/LICENSE [x-badge-npm-image]: https://xunn.at/npm-pkg-version/@black-flag/extensions 'Install this package using npm or yarn!'

[x-badge-semanticrelease-image]: https://xunn.at/badge-semantic-release 'This repo practices continuous integration and deployment!' [x-badge-semanticrelease-link]: https://github.com/semantic-release/semantic-release [x-pkg-cjs-mojito]: https://dev.to/jakobjingleheimer/configuring-commonjs-es-modules-for-nodejs-12ed#publish-only-a-cjs-distribution-with-property-exports [x-pkg-dual-package-hazard]: https://nodejs.org/api/packages.html#dual-package-hazard [x-pkg-exports-conditions]: https://webpack.js.org/guides/package-exports#reference-syntax [x-pkg-exports-module-key]: https://webpack.js.org/guides/package-exports#providing-commonjs-and-esm-version-stateless [x-pkg-exports-types-key]: https://devblogs.microsoft.com/typescript/announcing-typescript-4-5-beta#packagejson-exports-imports-and-self-referencing [x-pkg-side-effects-key]: https://webpack.js.org/guides/tree-shaking#mark-the-file-as-side-effect-free

[x-pkg-type]: https://github.com/nodejs/node/blob/8d8e06a345043bec787e904edc9a2f5c5e9c275f/doc/api/packages.md#type [x-repo-choose-new-issue]: https://github.com/Xunnamius/black-flag/issues/new/choose

[11]: ../../docs/advanced.md#justification-for-the-parent-child-and-tripartite-program-design

[35]: ./docs/index/type-aliases/BfeBuilderObjectValueExtensions.md#demandThisOptionIf [36]: ./docs/index/type-aliases/BfeBuilderObjectValueExtensions.md#demandThisOption [37]: ./docs/index/type-aliases/BfeBuilderObjectValueExtensions.md#demandThisOptionOr [38]: ./docs/index/type-aliases/BfeBuilderObjectValueExtensions.md#demandThisOptionXor

[52]: https://github.com/Xunnamius/js-utils/blob/main/docs/src/functions/safeDeepClone.md [53]: https://github.com/Xunnamius/js-utils/blob/main/docs/src/type-aliases/SafeDeepCloneOptions.md#transfer

[58]:

changelog

Changelog

All notable changes to this project will be documented in this auto-generated file. The format is based on Conventional Commits; this project adheres to Semantic Versioning.


@black-flag/core@3.0.0 (2025-05-28)

💥 BREAKING CHANGES 💥

  • Minimum supported node version is now 20.18.0

🪄 Fixes

  • src: make GracefulEarlyExitError call signature consistent with CliError (b60bc29)

⚙️ Build System

  • Bump yargs from 17.7.2 to 18.0.0 and adjust usage accordingly (379d98d)
  • deps: bump core-js from 3.41.0 to 3.42.0 (870c143)
  • deps: bump type-fest from 4.38.0 to 4.41.0 (f88034e)
  • package: bump experimental yargs-parser library version to 22.x (d6b35b9)
  • package: drop support for node\@18 (7a70c7e)


🏗️ Patch @black-flag/core@3.0.2 (2025-06-14)

⚙️ Build System

  • deps: bump @-xun/fs from 1.0.0 to 2.0.0 (c8a8693)
  • deps: bump @-xun/js from 1.1.1 to 2.0.0 (134f715)
  • deps: bump core-js from 3.42.0 to 3.43.0 (cafae4b)
  • deps: bump rejoinder from 1.2.5 to 2.0.1 (85cbf9d)


🏗️ Patch @black-flag/core@3.0.1 (2025-05-31)

🪄 Fixes

  • Account for ConfigurationHooks in contravariant position in runProgram params (19fa7dc)


@black-flag/core@2.0.0 (2025-03-14)

💥 BREAKING CHANGES 💥

  • All instances in source where commandModulePath appeared have been replaced by commandModulesPath. This includes the call signatures of functions like makeRunner.

    The fix is simple: find-and-replace all instances of commandModulePath with commandModulesPath.

  • Along with implementing the errorHandlingBehavior DX improvement in makeRunner, this update also addresses several small discrepancies in the behavior of configureProgram, runProgram, and makeRunner. These functions should now behave identically where appropriate (i.e. as described in their documentation), including consistently triggering the same error handling behavior at the same points for the same reasons given the same inputs.

    Additionally, non-graceful errors that are not handled by ConfigureErrorHandlingEpilogue will be consistently presented to the user as framework errors (assertion failures). As such, runProgram (and the low-order function returned by makeRunner) should no longer throw in some edge cases, such as when being passed a rejected promise or when a hook that is evaluated early throws.

  • Better help text output for dynamic options

    With this change, Black Flag lets Yargs fully parse argv and run through the builder twice before bailing to print help text when --help (or the equivalent option) is given.

    This allows input that triggers dynamic options like my-command --flag-1 --flag-2 --help to show help text specific to the final resolved builder configurations of --flag-1 --flag-2 rather than always showing the most generic help text, which was the behavior of older Black Flag versions. See documentation for details.

  • Do not output entire help text when a command fails

    Skip all but the first line of usage text in output by default.

  • Positionals are now available to builders alongside all other flags

    This is how vanilla Yargs does it. In earlier versions, builders' argv param had all positionals dumped into argv._ due to a suboptimal parsing extension.

  • Show any available child commands in ALL error text

    This includes when trying to use a command that is not found.

  • Surface new CliError::showHelp parameter values

    • "short"/true, which will print the command help text without all but the first line of usage text.

    • "default", which will defer to ExecutionContext::state.showHelpOnFail.

    • "full", which will force the old functionality.

    • false, which ensures help text is never printed with respect to the current error instance.

  • Upgrade ExecutionContext::state.showHelpOnFail to allow configuration of help text output style

    New output style options include "short" (first line of usage only, this is now the default) and "full" (full usage string). Also allows configuration of which error kinds trigger help text output and which do not. See documentation for details.

  • $executionContext and $artificiallyInvoked symbols are now drawn from the global symbol registry. They will not match symbols from previous versions!

  • ErrorMessage export was renamed to BfErrorMessage

✨ Features

  • src: export getDeepestErrorCause under /util (bf001c4)
  • src: implement errorHandlingBehavior option in makeRunner (5e4eb3d)
  • src: make expectedHelpTextRegExp utility a public export (8f2cb13)
  • src: make positionals available to builders normally (no longer in argv._) (42367ce)
  • src: output help text with greater fidelity (02a497f) see #172
  • Support Windows-style paths (cd288c5)

🪄 Fixes

  • Fix Windows interop issues (b3abf95) see #174
  • package: add @types/yargs as production dependency (8d50a56)
  • src: ensure ESM file bare exports are seen by black flag (389a2dc)
  • src: fix node\@18 interop issue (c1b5f61) see #173
  • src: improve isX type assertion exports (b980544)
  • src: improve intellisense across various exports (d6b1e73)
  • src: more consistently handle various errors; improve output fidelity (9412aa6)
  • src: throw upon encountering a command export with invalid yargs DSL (7312b8d)
  • src: throw upon encountering an async builder export (78eb0a2)

⚙️ Build System

  • deps: bump type-fest from 4.35.0 to 4.36.0 (33b2099)
  • deps: bump type-fest from 4.36.0 to 4.37.0 (cdd8f61)
  • husky: force lint-staged (via husky) to only use global config file (5d3f2cc)
  • package: add @-xun/symbiote dev dependency (074a930)
  • package: prune unnecessary dependencies (1b5cdbf)
  • Throw in runProgram when an incompatible Node.js runtime version is detected (cb56f8d)

🧙🏿 Refactored

  • Make exported symbols cross-realm (af78a8f)
  • Rename and restructure exports for better docs generation (8303ba7)
  • src: ErrorMessage export is now BfErrorMessage (3918a29)

🔥 Reverted

  • "build(deps): bump core-js from 3.40.0 to 3.41.0" (488206d)


🏗️ Patch @black-flag/core@2.0.5 (2025-03-28)

🪄 Fixes

  • assets/transformers: improve error output when handling internal yargs error instances (269046f)
  • src: loosen builder function form return type (f15e922)

⚙️ Build System

  • readme: improve documentation (5231dd4)


🏗️ Patch @black-flag/core@2.0.4 (2025-03-25)

🪄 Fixes

  • src: allow Configuration::builder function value to explicitly accept void return type (2676cbe)

⚙️ Build System

  • deps: bump type-fest from 4.37.0 to 4.38.0 (9e25b0c)


🏗️ Patch @black-flag/core@2.0.2 (2025-03-19)

🪄 Fixes

  • packages/extensions: improve safeDeepClone to account for own symbol properties (119919e)
  • readme: update quick start example (bdafbf8)
  • readme: use latest interface to extract positional in quick start example (33895e7)
  • src: do not make context descriptors unenumerable (2e6c05b)

⚙️ Build System

  • deps: bump @-xun/js from 1.0.0 to 1.1.0 (0adf956)
  • deps: bump @-xun/js from 1.1.0 to 1.1.1 (f3f1f74)


🏗️ Patch @black-flag/core@2.0.1 (2025-03-17)

🪄 Fixes

⚙️ Build System

  • deps: bump core-js from 3.40.0 to 3.41.0 (9371719)


@black-flag/core@1.3.0 (2024-07-12)

✨ Features

  • clierror: add dangerouslyFatal option, update cause option handling (1c369fb)


🏗️ Patch @black-flag/core@1.3.2 (2024-07-12)

🪄 Fixes

  • src: tweak error handling debug verboseness in certain edge cases (96ce293)


🏗️ Patch @black-flag/core@1.3.1 (2024-07-12)

🪄 Fixes

  • src: add trap door to alert developer when erroneously re-entering top-level error handler (99e2b3a)


@black-flag/core@1.2.0 (2024-03-18)

✨ Features

  • Add showHelp option to CliError (b5a1e58)


🏗️ Patch @black-flag/core@1.2.7 (2024-06-30)


🏗️ Patch @black-flag/core@1.2.6 (2024-06-02)

🪄 Fixes

  • src: ignore --help and --version if they occur after -- in argv (35f66cc)


🏗️ Patch @black-flag/core@1.2.5 (2024-05-30)

🪄 Fixes

  • src: permanently fix --version support regression in node\@22 (c201c2f)

⚙️ Build System

  • package: append node\@22 to supported node versions (98815d1)


🏗️ Patch @black-flag/core@1.2.4 (2024-03-27)

🪄 Fixes

  • src: explicitly ignore .d.ts files within command dirs (d6618d3)


🏗️ Patch @black-flag/core@1.2.3 (2024-03-27)

🪄 Fixes

  • package: bump minimum node support to 20 LTS (4b8c975)
  • src: ignore unknown file extension errors when discovering commands (4babf12)


🏗️ Patch @black-flag/core@1.2.2 (2024-03-21)

🪄 Fixes

  • No longer include default command when listing subcommands (be2960a)

⚙️ Build System

  • husky: update to latest hooks (75d5c66)
  • src: do not filter to own methods when proxying and rebinding (8bb0254)


🏗️ Patch @black-flag/core@1.2.1 (2024-03-19)


@black-flag/core@1.1.0 (2023-12-31)

✨ Features

  • src: support file://-style URLs (0e5067e)

🪄 Fixes

  • src: ensure --version is included in help text output when relevant (4f159dc)


🏗️ Patch @black-flag/core@1.1.4 (2024-03-16)


🏗️ Patch @black-flag/core@1.1.3 (2024-03-15)

🪄 Fixes

  • Add support for export default X syntax (bad391d)
  • Ensure demandOption is properly supported (2f205c1)


🏗️ Patch @black-flag/core@1.1.2 (2023-12-31)

🪄 Fixes

  • readme: move parserConfiguration call out of dead zone in example (f79c114)


🏗️ Patch @black-flag/core@1.1.1 (2023-12-31)

🪄 Fixes

  • Fix Node10 type resolution failure (b6178c9)


@black-flag/core@1.0.0 (2023-12-29)

✨ Features

  • src: disallow creating commands with conflicting names/aliases (78bf8ff)

🪄 Fixes

  • Rename package from "black-flag" to "@black-flag/core" npm typosquat workaround (292ead5)

⚙️ Build System

  • tsconfig: fix internal path resolution (fbe3a69)
  • tsconfig: upgrade to NodeNext (d3a499e)