cliweave 0.4.0 copy "cliweave: ^0.4.0" to clipboard
cliweave: ^0.4.0 copied to clipboard

A typed CLI framework: command routing, argument scanning with kebab/camel aliasing, did-you-mean suggestions, rendered help, structured exit codes, shell-completion proposals, and a terminal logger, [...]

cliweave #

A typed CLI framework for Dart: command routing, argument scanning, rendered help, structured exit codes, and shell-completion proposals — plus a terminal logger, spinner, and colour theme.

Status: 0.x. The API is in production use but can still change before 1.0. Dotweave is its first consumer.

Why not package:args? #

package:args is the right choice for most Dart CLIs. Reach for this one only if you need what it does not provide:

  • Completion, end to end, for four shells. proposeCompletions computes the candidate list for any partial command line — including per-argument dynamic values (file paths, remote names) from your own callback — and CompletionScripts generates the bash, zsh, fish, and PowerShell scripts that call back into it. package:args has no completion API; cli_completion adds one for bash and zsh only.
  • Full control of help layout. Help is rendered by this package rather than a private class, so USAGE / FLAGS / ARGUMENTS / COMMANDS sections, column alignment, and the --flag/--no-flag presentation are all part of the contract you can pin in tests.
  • kebab⇄camelCase aliasing. --dry-run and --dryRun both bind to a dryRun flag, without per-option alias lists.
  • Structured exit codes. Distinct negative codes for unknown command, invalid argument, and command-load failure, plus a hook to derive an exit code from a thrown error.

Libraries #

import 'package:cliweave/cliweave.dart'; // commands, flags, help, completion
import 'package:cliweave/terminal.dart'; // logger, spinner, colour theme

They are separate so that a consumer who only needs argument parsing does not pull in the terminal layer.

Example #

See example/main.dart for a runnable two-command app. The shape is:

final greetCommand = buildCommand(
  docs: const CommandDocs(brief: 'Greet someone'),
  parameters: CommandParameters(
    flags: FlagSet.one(
      BooleanFlag.required<ApplicationContext>(
        name: 'loud',
        brief: 'Shout the greeting',
      ),
    ).map((loud) => (loud: loud)),
    positional: PositionalSet.one(
      Positional.required<String, ApplicationContext>(
        brief: 'Who to greet',
        parse: stringParser,
        placeholder: 'name',
      ),
    ).map((name) => (name: name)),
  ),
  func: (context, flags, args) {
    final greeting = 'Hello, ${args.name}!';
    context.process.stdout.write(
      '${flags.loud ? greeting.toUpperCase() : greeting}\n',
    );
  },
);

and builds typed record pairs and map turns the result into the named record or class your handler should receive:

final output = ParsedFlag.optional<String, MyContext>(
  name: 'output',
  brief: 'Output path',
  parse: (context, input) => context.resolvePath(input),
);
final force = BooleanFlag.required<MyContext>(
  name: 'force',
  brief: 'Overwrite output',
);

final flags = FlagSet.one(output)
    .and(force)
    .map((values) => CopyOptions(values.$1, values.$2));

Use RunContext.direct(context) for one prebuilt context, or RunContext.forCommands to load one asynchronously from CommandInfo.prefix. The same context reaches parsers, completion callbacks, hooks, and the handler.

Integrations #

Pass an ordered List<CliIntegration<C>> to buildApplication for validation, lifecycle hooks, and application-level flags. Supplying a list—including an empty list—replaces the defaults:

final app = buildApplication(
  root,
  const ApplicationConfiguration(name: 'example'),
  integrations: [
    helpIntegration<MyContext>(),
    versionIntegration<MyContext>(
      info: const VersionInformation(currentVersion: '2.0.0'),
    ),
    CliIntegration(
      name: 'diagnostics',
      hooks: LifecycleHooks(
        commandStart: (args) => args.context.trace(args.result.prefix),
      ),
      flag: ApplicationFlag(
        brief: 'Print diagnostics',
        aliases: const ['d'],
        global: true,
        run: (args) => args.context.process.stdout.write('ok\n'),
      ),
    ),
  ],
);

Omit integrations to install the default help/help-all integrations and the version integration configured through ApplicationConfiguration.versionInfo.

$ dart run example/main.dart greet --loud world
HELLO, WORLD!

$ dart run example/main.dart gret world
No command registered for `gret`, did you mean `greet`?

$ dart run example/main.dart --help
USAGE
  example greet [--loud] <name>
  example --help

Example CLI

FLAGS
  -h --help  Print help information and exit

COMMANDS
  greet  Greet someone

Shell completion #

Register a hidden route that answers with one completion<TAB>description line per candidate, then print the matching script from a user-facing command:

final scripts = CompletionScripts(
  executableName: 'example',
  aliases: const ['ex'],
);

// Inside the hidden `__complete` command:
final candidates = await proposeCompletions(
  app,
  scripts.resolveCompletionInputs(positional.cast<String>()),
  context,
);

// Inside `example completion zsh`:
context.process.stdout.write(scripts.zsh);

The generated scripts invoke the hidden command without forwarding the flag-like completion words as arguments. They pass the raw command line in COMP_LINE, which preserves a trailing space (meaning "start a new word") and keeps prefixes such as --wit out of the application's argument scanner. resolveCompletionInputs drops the first token from COMP_LINE as the invocation name, so aliases, symlinks, paths, and renamed executables complete the same as the configured executable. It still accepts positional tokens as a fallback for direct callers and applies the stricter executable-name check to that path.

Shell function names are derived from the executable with illegal characters replaced, so my-cli produces __my_cli_complete rather than an unparseable __my-cli_complete.

aliases registers additional command names with the generated completion scripts without defining the aliases themselves. The canonical executableName is always registered first and duplicate aliases are ignored.

The package test suite compiles a standalone fixture CLI and loads the generated scripts in bash, zsh, fish, and PowerShell. To require locally installed shells rather than skip them, set CLIWEAVE_E2E_SHELLS:

CLIWEAVE_E2E_SHELLS=bash,zsh,fish dart test -t e2e

On Windows PowerShell:

$env:CLIWEAVE_E2E_SHELLS = 'powershell'
dart test -t e2e

Environment access #

Anywhere this package consults the environment (NO_COLOR, FORCE_COLOR, CI, TERM, STRICLI_NO_COLOR) it takes an EnvLookup — a String? Function(String name) — defaulting to lookupPlatformEnv, which reads Platform.environment with a case-insensitive fallback on Windows. Pass your own to read from a validated wrapper, a config overlay, or a test double.

Origin #

This is a Dart implementation of the design introduced by Bloomberg's TypeScript @stricli/core: its command model, help layout, scanner error messages, and completion approach were the reference. It is an independent project, not a binding, and is not affiliated with or endorsed by Bloomberg.

0
likes
150
points
262
downloads

Documentation

API reference

Publisher

verified publishertinyrack.net

Weekly Downloads

A typed CLI framework: command routing, argument scanning with kebab/camel aliasing, did-you-mean suggestions, rendered help, structured exit codes, shell-completion proposals, and a terminal logger, spinner, and colour theme.

Topics

#cli #command-line #argument-parser #shell-completion

License

MIT (license)

More

Packages that depend on cliweave