Conversation
There was a problem hiding this comment.
Pull request overview
Adds shell autocompletion support for the Warden CLI by introducing bash/zsh completion scripts and wiring their installation into warden install, improving discoverability of commands and common subcommands.
Changes:
- Added bash completion script that completes top-level commands (dynamic) and selected subcommands/env types (static).
- Added zsh completion function with similar behavior and option descriptions.
- Updated
warden installto symlink completion scripts into~/.warden/completions/and append initialization snippets to shell RC files.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
completions/warden.bash |
New bash completion script with dynamic command discovery + hardcoded subcommands/env types. |
completions/_warden |
New zsh completion function with dynamic command discovery + hardcoded subcommands/env types. |
commands/install.cmd |
Installs completion scripts and modifies user shell RC configuration during warden install. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| warden_dir="" | ||
| if [[ -n "${warden_bin}" ]]; then | ||
| local real_bin | ||
| real_bin="$(readlink "${warden_bin}" 2>/dev/null || echo "${warden_bin}")" |
There was a problem hiding this comment.
readlink can return a relative symlink target; if that happens, dirname "${real_bin}" is interpreted relative to the current directory and warden_dir detection can break. Since bin/warden already contains logic to normalize relative symlinks, consider applying a similar normalization here so completion can reliably locate commands/ and environments/.
| real_bin="$(readlink "${warden_bin}" 2>/dev/null || echo "${warden_bin}")" | |
| real_bin="$(readlink "${warden_bin}" 2>/dev/null || echo "${warden_bin}")" | |
| # Normalize relative symlink targets so dirname/.. resolution is reliable | |
| if [[ "${real_bin}" != /* ]]; then | |
| local warden_bin_dir real_bin_dir real_bin_name | |
| warden_bin_dir="$(dirname "${warden_bin}")" | |
| real_bin_dir="$(dirname "${real_bin}")" | |
| real_bin_name="$(basename "${real_bin}")" | |
| real_bin="$(cd "${warden_bin_dir}/${real_bin_dir}" 2>/dev/null && printf '%s\n' "${PWD}/${real_bin_name}")" | |
| fi |
|
|
||
| _warden() { | ||
| local cur prev words cword | ||
| _init_completion || return |
There was a problem hiding this comment.
The bash completion relies on _init_completion, which is only available when the bash-completion package is installed and sourced. On systems without it, completion will silently fail; consider adding a small fallback to initialize cur/prev/words/cword from COMP_WORDS/COMP_CWORD when _init_completion is unavailable (or detect and emit a helpful message).
| _init_completion || return | |
| if declare -F _init_completion >/dev/null 2>&1; then | |
| _init_completion || return | |
| else | |
| words=("${COMP_WORDS[@]}") | |
| cword=${COMP_CWORD} | |
| cur=${COMP_WORDS[COMP_CWORD]} | |
| prev=${COMP_WORDS[COMP_CWORD-1]} | |
| fi |
| if [[ -n "${warden_dir}" && -d "${warden_dir}/commands" ]]; then | ||
| commands="$(ls "${warden_dir}/commands/"*.cmd 2>/dev/null \ | ||
| | xargs -I{} basename {} .cmd \ | ||
| | grep -v usage)" | ||
| fi |
There was a problem hiding this comment.
Top-level command discovery only inspects ${warden_dir}/commands/*.cmd, but the CLI itself also supports commands from ${HOME}/.warden/commands and from a project-local .warden/commands (see bin/warden command resolution order). This means custom commands won’t autocomplete; consider merging results from those additional command locations (when present) so completion matches actual command resolution behavior.
| if [[ -n "${warden_dir}" && -d "${warden_dir}/commands" ]]; then | |
| commands="$(ls "${warden_dir}/commands/"*.cmd 2>/dev/null \ | |
| | xargs -I{} basename {} .cmd \ | |
| | grep -v usage)" | |
| fi | |
| # Collect commands from all supported locations: | |
| # - ${warden_dir}/commands | |
| # - ${HOME}/.warden/commands | |
| # - project-local .warden/commands | |
| local cmd_dirs=() | |
| if [[ -n "${warden_dir}" && -d "${warden_dir}/commands" ]]; then | |
| cmd_dirs+=("${warden_dir}/commands") | |
| fi | |
| if [[ -n "${HOME:-}" && -d "${HOME}/.warden/commands" ]]; then | |
| cmd_dirs+=("${HOME}/.warden/commands") | |
| fi | |
| if [[ -d ".warden/commands" ]]; then | |
| cmd_dirs+=(".warden/commands") | |
| fi | |
| if [[ ${#cmd_dirs[@]} -gt 0 ]]; then | |
| local dir | |
| for dir in "${cmd_dirs[@]}"; do | |
| while IFS= read -r cmd_name; do | |
| # Filter out the internal "usage" command | |
| [[ "${cmd_name}" == "usage" ]] && continue | |
| commands+="${cmd_name} " | |
| done < <(ls "${dir}/"*.cmd 2>/dev/null \ | |
| | xargs -I{} basename {} .cmd) | |
| done | |
| fi |
| if [[ -n "${warden_dir}" && -d "${warden_dir}/commands" ]]; then | ||
| commands=(${(f)"$(ls "${warden_dir}/commands/"*.cmd 2>/dev/null \ | ||
| | xargs -I{} basename {} .cmd \ | ||
| | grep -v usage)"}) | ||
| fi |
There was a problem hiding this comment.
Top-level command discovery only checks ${warden_dir}/commands/*.cmd, but warden also loads commands from ${HOME}/.warden/commands and project-local .warden/commands first. As a result, custom/overridden commands won’t autocomplete even though the CLI can execute them; consider incorporating those additional directories into the commands array when they exist.
| if [[ -n "${warden_dir}" && -d "${warden_dir}/commands" ]]; then | |
| commands=(${(f)"$(ls "${warden_dir}/commands/"*.cmd 2>/dev/null \ | |
| | xargs -I{} basename {} .cmd \ | |
| | grep -v usage)"}) | |
| fi | |
| commands=() | |
| # Discover commands from project-local, user, and global warden directories (in that order) | |
| local -a cmd_files | |
| cmd_files=() | |
| if [[ -d "${PWD}/.warden/commands" ]]; then | |
| cmd_files+=("${PWD}"/.warden/commands/*.cmd(N)) | |
| fi | |
| if [[ -n "${HOME}" && -d "${HOME}/.warden/commands" ]]; then | |
| cmd_files+=("${HOME}"/.warden/commands/*.cmd(N)) | |
| fi | |
| if [[ -n "${warden_dir}" && -d "${warden_dir}/commands" ]]; then | |
| cmd_files+=("${warden_dir}"/commands/*.cmd(N)) | |
| fi | |
| if (( ${#cmd_files} > 0 )); then | |
| commands=(${(u)${(f)"$(printf '%s\n' "${cmd_files[@]}" \ | |
| | xargs -I{} basename {} .cmd \ | |
| | grep -v usage)"}}) | |
| fi |
| ZSHRC="${HOME}/.zshrc" | ||
| if [[ -f "${ZSHRC}" || "$OSTYPE" == "darwin"* ]] && ! grep -q 'warden/completions' "${ZSHRC}" 2>/dev/null; then | ||
| echo "==> Adding warden zsh completion to ${ZSHRC}" | ||
| printf '\n# Warden CLI zsh completion\nfpath=("%s" $fpath)\nautoload -Uz compinit && compinit\n' \ |
There was a problem hiding this comment.
This appends autoload -Uz compinit && compinit into the user’s .zshrc. If the user already initializes completion elsewhere (common), this will run compinit twice on every shell startup, which can slow startup and may trigger additional warnings. Consider only adding the fpath+=(...) line (and optionally a short note telling users to run compinit if needed), or guard the compinit call so it only runs when completion hasn’t been initialized yet.
| printf '\n# Warden CLI zsh completion\nfpath=("%s" $fpath)\nautoload -Uz compinit && compinit\n' \ | |
| printf '\n# Warden CLI zsh completion\n# Ensure that zsh completion (compinit) is initialized in your shell config.\nfpath=("%s" $fpath)\n' \ |
| local warden_dir="" | ||
| if [[ -n "${warden_bin}" ]]; then | ||
| local real_bin | ||
| real_bin="$(readlink "${warden_bin}" 2>/dev/null || echo "${warden_bin}")" |
There was a problem hiding this comment.
readlink may return a relative symlink target (common on macOS/Homebrew), in which case dirname "${real_bin}" is relative to the current working directory and warden_dir resolution can fail. The main bin/warden script handles relative symlinks explicitly; consider doing the same here (resolve relative targets against dirname "${warden_bin}", or otherwise canonicalize to an absolute path) so dynamic command/env discovery works reliably.
| real_bin="$(readlink "${warden_bin}" 2>/dev/null || echo "${warden_bin}")" | |
| real_bin="$(readlink "${warden_bin}" 2>/dev/null || echo "${warden_bin}")" | |
| # If readlink returned a relative path, resolve it against the warden_bin directory | |
| if [[ "${real_bin}" != /* ]]; then | |
| local warden_bin_dir | |
| warden_bin_dir="$(cd "$(dirname "${warden_bin}")" 2>/dev/null && pwd)" | |
| if [[ -n "${warden_bin_dir}" ]]; then | |
| real_bin="${warden_bin_dir}/${real_bin}" | |
| fi | |
| fi |
| _describe 'sync subcommand' sync_cmds | ||
| fi | ||
| ;; | ||
| env|svc) |
There was a problem hiding this comment.
Both of these just pipe what comes next into docker compose (or more specifically the command stored in ${DOCKER_COMPOSE_COMMAND}) - Is there any way to use this built in flag to use the autocomplete for that at this stage? That could give us a much better autocomplete experience here
There was a problem hiding this comment.
I tried to do this one with docker compose --help command, try yourself, works perfectly for me so far.
…r subcommands and `docker compose <subcmd> --help` for flags.
Check List
Is your feature request related to a problem? Please describe.
Warden CLI has no shell autocompletion, so users must remember all commands, or create aliases manually, which slows down the workflow.
Describe the solution you've submitted
Added bash and zsh completion scripts that dynamically discover available commands from commands/*.cmd files. Completions cover top-level commands, subcommands for db, sync, env, svc, and environment types for env-init. The warden install command now automatically sets up completions by symlinking scripts into ~/.warden/completions/ and updating shell rc files.
Describe alternatives you've considered
Static hardcoded command lists - rejected in favor of dynamic discovery.
Additional context
Tested on macOS with zsh and bash. Completions fall back to a hardcoded list if the commands directory is unavailable.
Note: top-level commands are discovered dynamically from *.cmd files, but subcommands (e.g. db connect|import|dump|upgrade, sync start|stop|list|...) are hardcoded in the completion scripts because they are
defined inside the command files themselves, not as separate discoverable entities. This means the completion scripts will need to be updated manually whenever new subcommands are added to existing commands.