YAML-driven command runner with nested subcommands
wand
wand is a tiny, cross-platform command runner driven by a simple YAML config file, written
in Go. Define your commands and subcommands in a wand.yml, and run them from anywhere in your
project tree.
π Features
- Simple YAML config: define commands, descriptions, and nested subcommands in a single file.
- Auto-discovery: finds
wand.ymlby searching the current directory, parent directories,~/, and~/.config/. - Nested subcommands: commands can have arbitrarily deep children.
- Positional arguments: pass arguments to commands and reference them with
$1,$2,$@. - Custom flags: define typed flags (string or bool) with aliases, defaults, and descriptions,
accessible as
$WAND_FLAG_<NAME>environment variables. - Global flags: declare flags once in
.configand use them with any command, before or after the command name. - Custom binary name: rename the tool in help output to present a config as its own CLI.
- Environment variables: define env vars globally in
.configor per command, with command-level overrides. - Working directory: override the working directory for any command.
- Aliases: define alternate names for commands.
- Confirmation prompts: require
y/Nconfirmation before running destructive commands. - Pre/post hooks: chain other wand commands to run before or after a command, with full flag/argument forwarding.
- Built-in help: auto-generated
--helpfor every command and subcommand. - Shell execution: runs commands via your
$SHELLwith proper stdin/stdout/stderr passthrough.
π― Installation
Download Precompiled Binaries
Grab the latest release for Linux, macOS, or Windows:
Homebrew (macOS/Linux)
Install directly from the tap:
brew install chenasraf/tap/wand
Or tap and then install the package:
brew tap chenasraf/tap
brew install wand
From Source
git clone https://github.com/chenasraf/wand
cd wand
make build
β¨ Getting Started
Create a wand.yml in your project root:
main:
description: run the main command
cmd: echo hello from wand
build:
description: build the project
cmd: go build -o myapp
test:
description: run tests
cmd: go test -v ./...
children:
coverage:
description: run tests with coverage
cmd: go test -coverprofile=coverage.out ./...
Run a command
# run the main (default) command
wand
# run a named command
wand build
# run a nested subcommand
wand test coverage
# show help
wand --help
wand test --help
π Config Resolution
wand searches for wand.yml (or wand.yaml) in the following order:
- Current working directory (
./wand.yml) - Parent directories (searching upward to the filesystem root)
- Home directory (
~/.wand.yml) - Config directory (
~/.config/wand.yml)
The first config file found is used.
You can override config discovery with an explicit path:
# via flag
wand --wand-file ./other-config.yml build
# via environment variable
WAND_FILE=./other-config.yml wand build
The --wand-file flag takes precedence over WAND_FILE.
π Config Reference
Each top-level key defines a command, except .config, which holds settings that apply to the whole
file. The special key main becomes the root (no-argument) command.
.config fields
| Field | Type | Description |
|---|---|---|
shell | string or map | Shell used to run commands, optionally per OS |
env | map[string]string | Environment variables for every command |
flags | map[string]Flag | Flags available to every command (see below) |
bin_name | string | Name shown in help output (default wand) |
Command fields
| Field | Type | Description |
|---|---|---|
description | string | Short description shown in --help |
cmd | string | Shell command to execute |
children | map[string]Command | Nested subcommands (same structure) |
flags | map[string]Flag | Custom flags (see below) |
env | map[string]string | Environment variables for this command |
working_dir | string | Working directory for the command |
aliases | []string | Alternate names for the command |
confirm | bool or string | Prompt for confirmation before running |
confirm_default | string | Default answer: "yes" or "no" (default) |
pre | []string | Wand commands to run before cmd |
post | []string | Wand commands to run after cmd |
Flag fields
| Field | Type | Description |
|---|---|---|
alias | string | Single-letter shorthand (e.g. o for -o) |
description | string | Description shown in --help |
default | any | Default value (string or bool) |
type | string | "bool" for boolean flags, omit for string flags |
π Positional Arguments
Commands receive any extra arguments passed on the command line. Use $1, $2, etc. for specific
positions, or $@ for all arguments:
greet:
description: greet someone
cmd: echo "Hello, $1! You said: $@"
wand greet world foo bar
# β Hello, world! You said: world foo bar
π© Flags
Define custom flags per command. Flag values are exposed as $WAND_FLAG_<NAME> environment
variables (uppercased):
build:
description: build the project
cmd: |
echo "output=$WAND_FLAG_OUTPUT verbose=$WAND_FLAG_VERBOSE"
flags:
output:
alias: o
description: output path
default: ./bin
verbose:
alias: v
description: enable verbose output
type: bool
wand build --output ./dist --verbose
# β output=./dist verbose=true
wand build -o ./dist -v
# β output=./dist verbose=true
wand build
# β output=./bin verbose=false
Global flags
Flags declared under .config are available to every command, and may be passed either before or
after the command name:
.config:
flags:
profile:
alias: p
description: config profile
default: dev
verbose:
alias: V
description: enable verbose output
type: bool
build:
cmd: echo "profile=$WAND_FLAG_PROFILE verbose=$WAND_FLAG_VERBOSE"
children:
docs:
cmd: echo "docs profile=$WAND_FLAG_PROFILE"
wand build --profile prod
wand --profile prod build
wand -p prod build docs
# β profile=prod verbose=false
A command flag of the same name shadows the global one for that command:
.config:
flags:
profile:
default: dev
deploy:
cmd: echo $WAND_FLAG_PROFILE
flags:
profile:
default: staging
wand deploy
# β staging
Global flag names and aliases must not collide with each other, with a commandβs own flags, or with
wandβs built-in --wand-file and --help.
π·οΈ Binary Name
Set bin_name to have help and usage output name your tool instead of wand. This suits a config
exposed through a wrapper or alias, so it reads as its own CLI:
.config:
bin_name: nxc
sync:
description: sync files
cmd: ./sync.sh
wand --wand-file ~/.config/wand/nextcloud.yml --help
Usage:
nxc [command]
Available Commands:
sync sync files
Use "nxc [command] --help" for more information about a command.
The name flows through nested command paths (nxc sync --help) and generated completion scripts.
--wand-file drops out of the help output when bin_name is set, since the renamed tool presents
itself as its own CLI β the flag keeps working, so a wrapper can still point at the config:
#!/bin/sh
exec wand --wand-file ~/.config/wand/nextcloud.yml "$@"
π Environment Variables
Define environment variables globally in .config or per command. Command-level env vars override
global ones:
.config:
env:
NODE_ENV: production
build:
description: build the project
cmd: echo "env=$NODE_ENV out=$OUTPUT_DIR"
env:
OUTPUT_DIR: ./dist
wand build
# β env=production out=./dist
β οΈ Confirmation Prompts
Add confirm: true for a default prompt, or provide a custom message:
deploy:
description: deploy to production
cmd: ./deploy.sh
confirm: 'Deploy to production?'
clean:
description: remove all build artifacts
cmd: rm -rf dist/
confirm: true
restart:
description: restart service
cmd: systemctl restart myapp
confirm: 'Restart the service?'
confirm_default: 'yes'
wand deploy
# β Deploy to production? [y/N]
π Pre & Post Hooks
Use pre and post to run other wand commands before or after a command. Each entry is a
shell-style string: the first token is the wand command name (subcommands are nested with
spaces), followed by any args and flags.
lint:
description: lint the project
cmd: golangci-lint run
test:
description: run tests
cmd: go test ./...
build:
description: build the project
pre:
- lint
- test
post:
- 'echo "build done: $WAND_FLAG_OUTPUT"'
flags:
output:
alias: o
default: ./bin
cmd: go build -o $WAND_FLAG_OUTPUT
wand build -o ./dist
# runs: lint β test β go build -o ./dist β echo "build done: ./dist"
Forwarding flags
Entries are passed through environment variable expansion ($VAR, ${VAR}) before being
parsed, so $WAND_FLAG_<NAME> references resolve to the current commandβs flag values (global flags
included):
deploy:
flags:
target:
description: deploy target
default: staging
pre:
- 'notify --channel deploys --message "deploying to $WAND_FLAG_TARGET"'
cmd: ./deploy.sh $WAND_FLAG_TARGET
Arbitrary flags and arguments can be passed directly:
release:
pre:
- 'test --verbose'
- 'build --output ./dist'
cmd: ./release.sh
Failure semantics
- If a
preentry fails, the maincmdand remainingpre/postentries are skipped. - If the main
cmdfails, nopostentries run. - If a
postentry fails, subsequentpostentries are skipped.
A command may omit cmd and define only pre/post to act as a pure aggregator.
Private commands
Prefix a command name with _ to mark it as private: it is hidden from --help output but
remains fully runnable, both directly (useful for testing) and from pre/post entries. The
same rule applies to nested children.
build:
pre:
- _ensure-deps
cmd: go build
_ensure-deps:
description: install required tools
cmd: ./scripts/install-deps.sh
wand --help # _ensure-deps is not listed
wand _ensure-deps # still runs directly
wand build # runs _ensure-deps then the build
π οΈ Contributing
I am developing this package on my free time, so any support, whether code, issues, or just stars is very helpful to sustaining its life. If you are feeling incredibly generous and would like to donate just a small amount to help sustain this project, I would be very very thankful!
I welcome any issues or pull requests on GitHub. If you find a bug, or would like a new feature, donβt hesitate to open an appropriate issue and I will do my best to reply promptly.
π License
wand is licensed under the CC0-1.0 License.