Skip to content

A powerful CLI tool that generates type-safe, project-local command wrappers from YAML definitions for Dart and Flutter developers.

License

Notifications You must be signed in to change notification settings

Nikoro/commands_cli

Repository files navigation

Pub Package Build Status MIT License

A simple, yet powerful command-line interface (CLI) tool to define and run project-local commands, similar to a Makefile.

This package allows you to create a commands.yaml file in your project's root directory and define a set of keywords, which can then be executed from the command line.

Demo

Why use commands instead of Makefile? 🤔

While Makefile is a powerful and widely used tool, commands_cli offers several advantages for modern development workflows, especially for Dart and Flutter projects:

  • Cross-platform Compatibility: commands_cli is written in Dart and runs on any platform where the Dart SDK is available. This means your commands will work consistently across macOS, Linux, and Windows.

  • Simplicity and Readability: commands.yaml uses the clean and human-readable YAML format. This makes your scripts easier to read, write, and maintain, even for teammates who aren’t familiar with traditional Makefiles.

    # commands.yaml
    
    tests: ## Run all tests with coverage
      script: flutter test --coverage --no-pub
    $ tests
    00:00 +1: All tests passed!
  • Structured Parameters: commands_cli lets you define both positional and named parameters in a clear, structured way. Parameters can be required or optional, and you can set default values when needed. This makes your commands self-documenting, easy to use, and far more powerful than Makefile's limited and often clumsy parameter handling.

    # commands.yaml
    
    tell:
      script: echo "{message} {name}"
      params:
        required:
          - message:
        optional:  
          - name: '-n, --name'
    $ tell hello
    hello
    
    $ tell Goodbye -n Makefile
    Goodbye Makefile
    
    $ tell 
    ❌ Missing required positional param: message
  • Strong Type System: Unlike Makefile's string-based approach, commands_cli supports a powerful type system with int, double, boolean, and enum types. This provides built-in validation, preventing common errors and making your commands more robust.

    # commands.yaml
    
    deploy: ## Deploy with replicas
      script: echo "Deploying with {replicas} replicas"
      params:
        optional:
          - replicas: '-r, --replicas'
            type: int
            default: 3
    $ deploy -r 5
    Deploying with 5 replicas
    
    $ deploy -r abc
    ❌ Parameter replicas expects an [integer]
       Got: "abc" [string]
  • Built-in Interactive Pickers: When you define enum parameters or switch commands without defaults, commands_cli automatically presents a beautiful interactive menu. No need to parse input manually or write custom prompts—it's all handled for you.

    # commands.yaml
    
    build: ## Build for platform
      script: echo "Building for {platform}"
      params:
        optional:
          - platform: '-p, --platform'
            values: [ios, android, web]
    $ build
    
    Select value for platform:
    
        1. ios     ✓
        2. android
        3. web
    
    Press number (1-3) or press Esc to cancel:
  • Automatic Help Generation: Every command automatically gets a --help (or -h) parameter. It collects information from your defined parameters and optional comments directly from the commands.yaml file, providing clear, up-to-date guidance without any extra work.

    # commands.yaml
    
    hello: ## Prints "Hello {message}"
      script: echo "Hello {message}"
      params:
        required:
        - message: ## The name to include in the greeting
          default: "World"
    $ hello --help
    hello: Prints "Hello {message}"
    params:
      required:
        message: The name to include in the greeting
          default: "World"
  • Composable, Human-Readable Commands: With commands_cli, you can define keyword chains that read like plain English. Instead of cryptic flags, you can run natural phrases such as:

    • build ios
    • build android
    • build web
    • build all
    • run all tests
    • run integration tests
    • …and more

    This switch-based design makes commands easier to discover, remember, and use.

Getting Started 🚀

  1. Activate the package:

    $ dart pub global activate commands_cli
  2. Create a commands.yaml file in the root of your project or type:

    $ commands create

    This will create this commands.yaml for you,

    already pre-filled with a simple hello example.

  3. Define your commands:

    # commands.yaml
    
    hello: ## Prints "Hello {message}"
      script: echo "Hello {message}"
      params:
        required:
        - message:
          default: "World"

    See the Usage and Examples sections below for more details.

  4. Activate your defined commands:

    $ commands
    ✅ hello: Prints "Hello {message}"
  5. Run your defined commands:

    $ hello
    Hello World
    
    $ hello dev
    Hello dev

Usage

The commands.yaml file has a simple structure:

<command_name>: ## <command_description>
  script: |
    # Your script goes here
  params:
    required:
      - <param_name>: '<flags>' ## <param_description>
        default: <default_value>
    optional:
      - <param_name>: '<flags>' ## <param_description>
        default: <default_value>
  • <command_name>: The name of your command.
  • <command_description>: An optional description for your command.
  • script: The script to be executed. You can use multi-line scripts using the | character.
  • params: An optional section to define parameters for your command.
  • required: A list of required parameters.
  • optional: A list of optional parameters.
  • <param_name>: The name of the parameter.
  • <flags>: Optional flags for named parameters (e.g., -n, --name).
  • <param_description>: An optional description for the parameter.
  • <default_value>: An optional default value for the parameter.

Examples

Here are some examples of how to define and use commands_cli in your commands.yaml file:

Basic Command

# commands.yaml

hello:
  script: echo "Hello, World!"

Activate your defined commands:

$ commands
✅ hello

Run:

$ hello
Hello, World!

Positional Parameters

# commands.yaml

greet:
  script: echo "{greeting} {name}!"
  params:
    required:
      - greeting:
    optional:  
      - name:

Activate your defined commands:

$ commands
✅ greet

Run:

$ greet Hi dev
Hi dev!

$ greet Yo
Yo !

$ greet
❌ Missing required positional param: greeting

Named Parameters

# commands.yaml

greet:
  script: echo "{greeting} {value}!"
  params:
    required:
      - greeting: '-g, --greeting'
    optional:  
      - value: '-n, --name'

Activate your defined commands:

$ commands
✅ greet

Run:

$ greet --greeting "Hi" --name "Alice"
Hi Alice!

$ greet -g "Hi"
Hi !

$ greet
❌ Missing required named param: greeting

Optional Parameters with Default Values

# commands.yaml

goodbye:
  script: echo "Goodbye, {name}{punctuation}"
  params:
    optional:
      - name:
        default: "World"
      - punctuation:
        default: "!"

Activate your defined commands:

$ commands
✅ goodbye

Run:

$ goodbye
Goodbye, World!

$ goodbye --name "Bob" -p "."
Goodbye, Bob.

Passthrough Arguments

Basic Alias

# commands.yaml

d: ## dart alias
  script: dart ...args

Here, ...args is a placeholder that automatically forwards any parameters you pass to the alias into the underlying command.

For example, running d --version will expand to dart --version.

This allows you to create concise aliases while still keeping the flexibility to inject flags, options, or arguments dynamically at runtime.

Activate your defined commands:

$ commands
✅ d: dart alias

Run:

$ d --version
Dart SDK version: 3.9.0...

Multiline script with passthrough arguments

# commands.yaml

analyze: ## dart analyze
  script: |
    echo "Analyzing ignoring warnings..."
    dart analyze ...args --no-fatal-warnings

Activate your defined commands:

$ commands
✅ analyze: dart analyze

Run:

$ analyze --fatal-infos
Analyzing ignoring warnings...
Analyzing example...                  0.5s
No issues found!

Strongly Typed Parameters

commands_cli supports a powerful type system for parameters, allowing you to define explicit types and constrain values for better validation and user experience.

Numeric Types

You can explicitly specify int or double types for numeric parameters:

# commands.yaml

deploy: ## Deploy application
  script: |
    echo "Deploying to port {port} with timeout {timeout}s"
  params:
    optional:
      - port: '-p, --port'
        type: int
        default: 3000
      - timeout: '-t, --timeout'
        type: double
        default: 30.5

Run:

$ deploy -p 8080 -t 60.0
Deploying to port 8080 with timeout 60.0s

$ deploy -p abc
❌ Parameter port expects an [integer]
   Got: "abc" [string]

Boolean Flags

Boolean parameters can be toggled on/off:

# commands.yaml

build: ## Build with options
  script: |
    echo "verbose={verbose} debug={debug}"
  params:
    optional:
      - verbose: '-v, --verbose'
        default: false
      - debug: '-d, --debug'
        default: true

Run:

$ build
verbose=false debug=true

$ build -v
verbose=true debug=true

$ build -v -d
verbose=true debug=false

Enum Parameters with Default

Restrict parameter values to a predefined set using values:

# commands.yaml

deploy: ## Deploy to environment
  script: |
    echo "Deploying to {env}"
  params:
    optional:
      - env: '-e, --environment'
        values: [dev, staging, prod]
        default: staging

Run:

$ deploy
Deploying to staging

$ deploy -e prod
Deploying to prod

$ deploy -e invalid
❌ Invalid value 'invalid' for parameter env
💡 Allowed values: dev, staging, prod

Interactive Enum Picker

When you define an enum parameter without a default value, commands_cli will automatically present an interactive picker when the parameter is not provided:

# commands.yaml

build: ## Build for platform
  script: |
    echo "Building for {platform}"
  params:
    optional:
      - platform: '-p, --platform'
        values: [ios, android, web]

Run:

$ build -p ios
Building for ios

$ build

Select value for platform:

    1. ios ✓
    2. android
    3. web

Press number (1-3) or press Esc to cancel:

Switch Commands (Nested Options)

The switch feature allows you to create commands with multiple named sub-options, enabling natural command structures like build ios, build android, or run tests.

Basic Switch with Default

# commands.yaml

build: ## Build application
  switch:
    - ios: ## Build for iOS
      script: flutter build ios
    - android: ## Build for Android
      script: flutter build apk
    - web: ## Build for web
      script: flutter build web
    - default: ios

Activate your defined commands:

$ commands
✅ build: Build application

Run:

$ build
Building iOS...

$ build android
Building Android...

$ build web
Building web app...

Interactive Switch Picker

When no default option is specified, commands_cli presents an interactive menu:

# commands.yaml

deploy: ## Deploy application
  switch:
    - staging: ## Deploy to staging
      script: ./deploy.sh staging
    - production: ## Deploy to production
      script: ./deploy.sh production

Run:

$ deploy staging
Deploying to staging...

$ deploy

Select an option for deploy:

    1. staging ✓ - Deploy to staging
    2. production   - Deploy to production

Press number (1-2) or press Esc to cancel:

Switch with Parameters

Each switch option can have its own parameters:

# commands.yaml

deploy: ## Deploy with configuration
  switch:
    - staging: ## Deploy to staging
      script: |
        echo "Deploying to staging with {replicas} replicas"
      params:
        optional:
          - replicas: '-r, --replicas'
            type: int
            default: 2
    - production: ## Deploy to production
      script: |
        echo "Deploying to production with {replicas} replicas"
      params:
        optional:
          - replicas: '-r, --replicas'
            type: int
            default: 5
    - default: staging

Run:

$ deploy
Deploying to staging with 2 replicas

$ deploy production -r 10
Deploying to production with 10 replicas

Switch with Enum Parameters

Combine switches with enum parameters for even more powerful command structures:

# commands.yaml

test: ## Run tests
  switch:
    - unit: ## Run unit tests
      script: |
        echo "Running unit tests on {platform}"
      params:
        optional:
          - platform: '-p, --platform'
            values: [vm, chrome, all]
            default: vm
    - integration: ## Run integration tests
      script: |
        echo "Running integration tests on {platform}"
      params:
        optional:
          - platform: '-p, --platform'
            values: [ios, android, all]
    - default: unit

Run:

$ test unit
Running unit tests on vm

$ test unit -p chrome
Running unit tests on chrome

$ test integration

Select value for platform:

    1. ios     ✓
    2. android
    3. all

Press number (1-3) or press Esc to cancel:

Overriding existing commands

In order to override commands like: clear, ls, cd, make etc.

You want your .pub-cache/bin dir to be first, not last. So instead of:

# .zshrc

export PATH="$PATH:$HOME/.pub-cache/bin"

use:

# .zshrc

export PATH="$HOME/.pub-cache/bin:$PATH"

After changing .zshrc, reload it:

$ source ~/.zshrc

zsh (and bash) keep a hash table of command lookups to speed things up, to rehash:

$ hash -r

Define your custom ls command and explicitly mark it as overridable using override: true:

# commands.yaml

ls: ## custom ls
  override: true # required when overriding reserved commands
  script: echo "ls is overridden!"

Activate your defined commands:

$ commands
✅ ls: custom ls

Run:

$ ls
ls is overridden!

Overriding test and which keywords

On POSIX shells (bash, zsh, sh), test and which are not just programs in /bin — they're also shell builtins. That means the shell resolves these commands before looking into $PATH.

So even if you put executables called test or which at the front of your $PATH, the shell will happily use its own builtins instead.

Shadow them with functions. That way:

  • Your functions always override the builtins.
  • By default, they just delegate to the system binaries.
  • If later you drop custom commands, they will be found first in $PATH (just like ls case).
# .zshrc

# Shadow the builtin "test" with a function
test() {
  # Explicitly call the system binary unless PATH provides an override
  command test "$@"
}

# Shadow the builtin "which" with a function
which() {
  # Explicitly call the system binary unless PATH provides an override
  command which "$@"
}

Define your custom commands:

# commands.yaml

test: ## custom test
  override: true   # required when overriding reserved commands
  script: echo "test is overridden!"

which: ## custom which
  override: true   # required when overriding reserved commands
  script: echo "which is overridden!"

Activate your defined commands:

$ commands
✅ test: custom testwhich: custom which

Run:

$ test
test is overridden!

$ which
which is overridden!

About

A powerful CLI tool that generates type-safe, project-local command wrappers from YAML definitions for Dart and Flutter developers.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages