Brocli

repository·main·Indexed 19 days ago

https://github.com/drizzle-team/brocli

A modern, type-safe library for building command-line interfaces (CLIs) with TypeScript or JavaScript. Brocli features explicit APIs, built-in validation for typed options, and high testability. It provides a comprehensive option builder for strings, numbers, booleans, and positional arguments, along with support for subcommands, lifecycle hooks, and custom event handlers for managing help and error output.

Tokens
7.5K
Snippets
26
Records
28
Agent score
66%

What's inside @drizzle-team/brocli

  1. Create a CLI with Brocli

    main

    Brocli is a type-safe way to build CLIs using TypeScript or JavaScript. You define commands using the command function, specify their options (including validation and defaults), and provide a handler function to execute the business logic. Finally, use run() to parse shell arguments and execute the commands.

    To use Brocli, import the necessary primitives from @drizzle-team/brocli and pass an array of command objects to run().

    import { command, string, boolean, run } from "@drizzle-team/brocli";
    
    const push = command({
      name: "push",
      options: {
        dialect: string().enum("postgresql", "mysql", "sqlite"),
        databaseSchema: string().required(),
        databaseUrl: string().required(),
        strict: boolean().default(false),
      },
      handler: (opts) => {
        // Business logic using typed opts
      },
    });
    
    run([push]);
  2. Use lifecycle hooks in BroCli

    main

    You can intercept command execution using the hook property in BroCliConfig. The hook is called twice: once with the 'before' event and once with the 'after' event.

    Hook Signature: (event: 'before' | 'after', command: Command, options: TOptsData) => any

    await run(commands, {
      hook: (event, command, options) => {
        if (event === 'before') {
          // Perform setup
        } else {
          // Perform cleanup
        }
      },
    });
  3. Define a command with positional arguments

    main

    You can define positional arguments within the options object of a command using the positional() helper. This allows you to capture arguments that are not prefixed with flags (e.g., echo text instead of echo --text text).

    Use .desc() to add a description and .default() to provide a fallback value.

    import { run, command, positional } from "@drizzle-team/brocli";
    
    const echo = command({
      name: "echo",
      options: {
        text: positional().desc("Text to echo").default("echo"),
      },
      handler: (opts) => {
        console.log(opts.text);
      },
    });
    
    run([echo]);
  4. Build options using the Option Builder

    main

    Brocli provides builder functions to define typed CLI arguments.

    Core Builders

    • string(name?: string): Defines a string option. Passed as --option=value or --option value. If name is not provided, it defaults to the key name.
    • number(name?: string): Defines a number option. Passed as --option=value or --option value.
    • boolean(name?: string): Defines a boolean flag. Passed as --option.
    • positional(displayName?: string): Defines a positional option. Passed as command value.

    Option Extensions

    All builders support the following methods to refine behavior:

    • .alias(...aliases: string[]): Adds aliases (e.g., .alias('f')).
    • .desc(description: string): Sets the description for the help command.
    • .required(): Makes the option mandatory; the app will error if it's missing.
    • .default(value: string | boolean): Sets a default value.
    • .hidden(): Hides the option from the help output.
    • .enum(values: [string, ...string[]]): Restricts string values to a specific set.
    • .int(): Ensures a number is an integer.
    • .min(value: number) / .max(value: number): Sets numeric boundaries.

    Naming Convention: If a name is one character long, it is automatically prefixed with -. If longer, it uses --. To force a single hyphen on a long name, pass it explicitly: string('-longname').

    import { string, boolean } from "@drizzle-team/brocli";
    
    const options = {
      dialect: string().enum("postgresql", "mysql", "sqlite"),
      databaseSchema: string().required(),
      databaseUrl: string().required(),
      strict: boolean().default(false),
    };
  5. Execute your CLI with `run()`

    main

    The run() function is the entry point that starts the command execution loop. It takes your collection of commands and a configuration object.

    Key Configuration Options

    • name: The name used to invoke the application in help/usage examples.
    • description: Global description of your app.
    • version: String or handler for the app version.
    • globals: An object of global options available to all commands via the hook.
    • theme(event: BroCliEvent): A function to customize how messages (like commandHelp or unknownError) are printed. Return true to indicate you handled the event, or false to fall back to the default theme.
    • hook(event, command, globals): Executes code before or after every command's transform and handler execution.
    • omitKeysOfUndefinedOptions: If true, undefined options are not passed to transform or handler.
    • argSource: The array of arguments to parse (defaults to process.argv).
    import { command, run, string, boolean, type TypeOf } from '@drizzle-team/brocli'
    
    const commands: Command[] = [
      command({
        name: 'command',
        options: { opt1: string() },
        handler: (opts) => { /* ... */ },
      })
    ];
    
    run(commands, {
        name: 'mysoft',
        description: 'MySoft CLI',
        version: '1.0.0',
        globals: {
            flag: boolean('gflag').description('Global flag').default(false)
        },
        hook: (event, command, globals) => {
            if(event === 'before') console.log(`Command '${command.name}' started with flag ${globals.flag}`)
            if(event === 'after') console.log(`Command '${command.name}' successfully finished it's work with flag ${globals.flag}`)
        }
    })
  6. Define a command with `command()`

    main

    Use the command() function to define a CLI command. A command can include a name, aliases, descriptions, options, a transform hook for preprocessing, and a handler for the main logic. If a command has subcommands, it does not require a handler (the help text will be shown instead).

    Note: A command cannot have both subcommands and positional options simultaneously.

    import { command, type Command, string, boolean, type TypeOf } from '@drizzle-team/brocli'
    
    const commandOptions = {
        opt1: string(),
        opt2: boolean('flag').alias('f'),
    }
    
    const commands: Command[] = []
    
    commands.push(command({
        name: 'command', 
        aliases: ['c', 'cmd'],
        desc: 'Description goes here',
        shortDesc: 'Short description',
        hidden: false,
        options: commandOptions,
        transform: (options) => {
            // Preprocess options here...
            return processedOptions
        },
        handler: (processedOptions) => {
            // Your logic goes here...
        },
        help: () => 'This command works like this: ...',
        subcommands: [
            command(
                // You can define subcommands like this
            )
        ]
    }));
  7. Infer handler types with `TypeOf` or `handler()`

    main

    When defining handlers separately from the command() call, you need to ensure type safety for the options object. You can achieve this in two ways:

    1. Using the TypeOf utility

    Define your options object first, then use TypeOf to extract its type for your handler function.

    import { string, boolean, type TypeOf } from '@drizzle-team/brocli'
    
    const commandOptions = {
        opt1: string(),
        opt2: boolean('flag').alias('f'),
    }
    
    export const commandHandler = (options: TypeOf<typeof commandOptions>) => {
        // Your logic goes here...
    }

    2. Using the handler() wrapper

    Wrap your logic in the handler() function, which automatically handles the type inference based on the provided options schema.

    import { string, boolean, handler } from '@drizzle-team/brocli'
    
    const commandOptions = {
        opt1: string(),
        opt2: boolean('flag').alias('f'),
    }
    
    export const commandHandler = handler(commandOptions, (options) => {
        // Your logic goes here...
    });
    import { string, boolean, type TypeOf } from '@drizzle-team/brocli'
    
    const commandOptions = {
        opt1: string(),
        opt2: boolean('flag').alias('f'),
    }
    
    export const commandHandler = (options: TypeOf<typeof commandOptions>) => {
        // Your logic goes here...
    }
  8. Get a command's full path with `getCommandNameWithParents()`

    main
    Use getCommandNameWithParents(command: Command) to retrieve the full command path, including all parent command names. This is useful when dealing with deeply nested subcommands.
  9. Configure the CLI version

    main

    Brocli supports automatic --version and -v flags. You can configure the version in two ways via the second argument of the run() function:

    1. Static String: Pass a simple string to display a fixed version.
    2. Async Callback: Pass an async function to perform I/O (like reading an environment variable or fetching a version from a dependency) before printing the version.

    Note: If you use an async callback, you are responsible for printing the version to the console within that function.

    import { run, command, positional } from "@drizzle-team/brocli";
    
    // Option 1: Static version
    run([echo], {
      version: "1.0.0",
    });
    
    // Option 2: Async callback version
    const version = async () => {
      const envVersion = process.env.CLI_VERSION;
      console.log(envVersion, "\n");
    };
    
    run([echo], {
      version: version,
    });
  10. Test command behavior with `test()`

    main

    The test() function allows you to verify the behavior of a specific command with a provided string of arguments.

    Warning: If the command has a transform hook, it will be executed, but the handler will not be called. This makes it suitable for testing argument parsing and transformation logic without triggering side effects.

    import { test } from '@drizzle-team/brocli';
    
    // test(command, args)
    test(myCommand, '--option=value');
  11. Configure the CLI with BroCliConfig

    main

    Pass a BroCliConfig object to run() to customize the CLI behavior.

    Key Configuration Options:

    • name: The name of the CLI.
    • description: A description of the CLI.
    • argSource: The array of arguments to parse (defaults to process.argv).
    • help: A string or a function (options) => void to define help output.
    • version: A string or a function (options) => void to define version output.
    • globals: Global options available to all commands.
    • omitKeysOfUndefinedOptions: If true, undefined options are removed from the resulting options object.
    • hook: A lifecycle hook called before and after a command handler executes.
    • theme: An EventHandler to customize how the CLI renders output (help, errors, etc.).
    • noExit: If true, prevents the process from exiting on errors (useful for testing).
    await run(commands, {
      name: 'my-app',
      description: 'My awesome app',
      omitKeysOfUndefinedOptions: true,
      hook: (event, command, options) => {
        if (event === 'before') console.log(`Executing ${command.name}...`);
      },
    });
  12. Use the BroCLI command line interface

    main

    In BroCLI, commands do not need to be the first argument; they can be passed in any order within the command line string.

    To support this flexible ordering, any option passed immediately before a command must have an explicit value, even if that option is a boolean flag. For example, instead of using --verbose <command>, use --verbose true <command>.

    Note: This requirement does not apply to the following reserved flags:

    • --help / -h
    • --version / -v

    Additionally, BroCLI uses strict mode for option parsing: providing any unrecognized options will result in an error.

    # Example of passing an option with an explicit value before a command
    --verbose true <command-name>
    
    # Reserved flags that do not require explicit values
    --help
    -h
    --version
    -v