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.
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_cliis 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.yamluses 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_clilets 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_clisupports a powerful type system with string, boolean, integer, double, number, 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_cliautomatically 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: required: - 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 thecommands.yamlfile, 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 iosbuild androidbuild webbuild allrun all testsrun integration tests- …and more
This switch-based design makes commands easier to discover, remember, and use.
-
Activate the package:
$ dart pub global activate commands_cli
-
Create a
commands.yamlfile in the root of your project or type:$ commands create
This will create this
commands.yamlfor you,already pre-filled with a simple
helloexample. -
Define your commands:
# commands.yaml hello: ## Prints "Hello {message}" script: echo "Hello {message}" params: required: - message: default: "World"
-
Activate your defined commands:
$ commands ✅ hello: Prints "Hello {message}". Type "hello --help" to learn more. -
Run your defined commands:
$ hello Hello World $ hello dev Hello dev
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.
Here are some examples of how to define and use commands_cli in your commands.yaml file:
# commands.yaml
hello:
script: echo "Hello, World!"Activate your defined commands:
$ commands
✅ hello: Type "hello --help" to learn more.Run:
$ hello
Hello, World!# commands.yaml
greet:
script: echo "{greeting} {name}!"
params:
required:
- greeting:
optional:
- name:Activate your defined commands:
$ commands
✅ greet: Type "greet --help" to learn more.Run:
$ greet Hi dev
Hi dev!
$ greet Yo
Yo !
$ greet
❌ Missing required positional param: greeting# commands.yaml
greet:
script: echo "{greeting} {value}!"
params:
required:
- greeting: '-g, --greeting'
optional:
- value: '-n, --name'Activate your defined commands:
$ commands
✅ greet: Type "greet --help" to learn more.Run:
$ greet --greeting "Hi" --name "Alice"
Hi Alice!
$ greet -g "Hi"
Hi !
$ greet
❌ Missing required named param: greeting# commands.yaml
goodbye:
script: echo "Goodbye, {name}{punctuation}"
params:
optional:
- name:
default: "World"
- punctuation:
default: "!"Activate your defined commands:
$ commands
✅ goodbye: Type "goodbye --help" to learn more.Run:
$ goodbye
Goodbye, World!
$ goodbye --name "Bob" -p "."
Goodbye, Bob.# commands.yaml
d: ## dart alias
script: dart ...argsHere, ...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. Type "d --help" to learn more.Run:
$ d --version
Dart SDK version: 3.9.0...# commands.yaml
analyze: ## dart analyze
script: |
echo "Analyzing ignoring warnings..."
dart analyze ...args --no-fatal-warningsActivate your defined commands:
$ commands
✅ analyze: dart analyze. Type "analyze --help" to learn more.Run:
$ analyze --fatal-infos
Analyzing ignoring warnings...
Analyzing example... 0.5s
No issues found!commands_cli supports a powerful type system for parameters, allowing you to define explicit types and constrain values for better validation and user experience.
| Type | Aliases | Description |
|---|---|---|
string |
- | Text values (default if not specified) |
boolean |
bool |
Boolean values (true or false) |
integer |
int |
Whole numbers (no decimal point allowed) |
double |
- | Decimal numbers (must include decimal point) |
number |
num |
Any numeric value (integer or decimal) |
The integer type (or int) accepts only whole numbers. Values with decimal points are rejected.
# commands.yaml
deploy: ## Deploy application
script: echo "Deploying to port {port}"
params:
optional:
- port: '-p, --port'
type: int
default: 3000Run:
$ deploy -p 8080
Deploying to port 8080
$ deploy -p 3.14
❌ Parameter port expects an [integer]
Got: 3.14 [double]
$ deploy -p abc
❌ Parameter port expects an [integer]
Got: abc [string]The double type accepts only decimal numbers. The value must include a decimal point.
# commands.yaml
configure: ## Configure timeout
script: echo "Timeout set to {timeout}s"
params:
optional:
- timeout: '-t, --timeout'
type: double
default: 30.5Run:
$ configure -t 60.0
Timeout set to 60.0s
$ configure -t 1
❌ Parameter timeout expects a [double]
Got: 1 [integer]The number type (or num) accepts both integers and decimals - any numeric value.
# commands.yaml
calculate: ## Calculate with value
script: echo "Value is {value}"
params:
optional:
- value: '-v, --value'
type: numberRun:
$ calculate -v 42
Value is 42
$ calculate -v 3.14
Value is 3.14
$ calculate -v abc
❌ Parameter value expects a [number]
Got: abc [string]The boolean type (or bool) accepts only true or false values. When used as a flag without a value, it toggles the default.
# commands.yaml
build: ## Build with options
script: |
echo "verbose={verbose} debug={debug}"
params:
optional:
- verbose: '-v, --verbose'
type: boolean
default: false
- debug: '-d, --debug'
type: boolean
default: trueRun:
$ build
verbose=false debug=true
$ build -v
verbose=true debug=true
$ build -v -d
verbose=true debug=falseRestrict 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: stagingRun:
$ deploy
Deploying to staging
$ deploy -e prod
Deploying to prod
$ deploy -e invalid
❌ Invalid value 'invalid' for parameter env
💡 Allowed values: dev, staging, prodWhen 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: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.
# 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: iosActivate your defined commands:
$ commands
✅ build: Build application. Type "build --help" to learn more.Run:
$ build
Building iOS...
$ build android
Building Android...
$ build web
Building web app...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 productionRun:
$ 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: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: stagingRun:
$ deploy
Deploying to staging with 2 replicas
$ deploy production -r 10
Deploying to production with 10 replicasCombine 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: unitRun:
$ 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: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 ~/.zshrczsh (and bash) keep a hash table of command lookups to speed things up, to rehash:
$ hash -rDefine 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. Type "ls --help" to learn more.Run:
$ ls
ls is overridden!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
lscase).
# .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 test. Type "test --help" to learn more.
✅ which: custom which. Type "which --help" to learn more.Run:
$ test
test is overridden!
$ which
which is overridden!
