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 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_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: 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 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}" -
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
✅ helloRun:
$ hello
Hello, World!# commands.yaml
greet:
script: echo "{greeting} {name}!"
params:
required:
- greeting:
optional:
- name:Activate your defined commands:
$ commands
✅ greetRun:
$ 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
✅ greetRun:
$ 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
✅ goodbyeRun:
$ 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 aliasRun:
$ 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 analyzeRun:
$ 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.
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.5Run:
$ 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 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: 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 applicationRun:
$ 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 lsRun:
$ 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
✅ which: custom whichRun:
$ test
test is overridden!
$ which
which is overridden!
