Skip to content

Add shell autocompletion support#915

Open
MaximLogic wants to merge 2 commits intowardenenv:mainfrom
MaximLogic:main
Open

Add shell autocompletion support#915
MaximLogic wants to merge 2 commits intowardenenv:mainfrom
MaximLogic:main

Conversation

@MaximLogic
Copy link

Check List

  • Matching PR in the documentation repo (replace text with link when it exists)
  • Entry in CHANGELOG.md

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.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 install to 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}")"
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/.

Suggested change
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

Copilot uses AI. Check for mistakes.

_warden() {
local cur prev words cword
_init_completion || return
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
_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

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +27
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
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +19
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
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
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' \
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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' \

Copilot uses AI. Check for mistakes.
local warden_dir=""
if [[ -n "${warden_bin}" ]]; then
local real_bin
real_bin="$(readlink "${warden_bin}" 2>/dev/null || echo "${warden_bin}")"
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
@navarr navarr moved this to 🏗 In progress in Warden Feb 16, 2026
@navarr navarr added this to the Warden 0.17 milestone Feb 16, 2026
@navarr navarr added the enhancement New feature or request label Feb 16, 2026
_describe 'sync subcommand' sync_cmds
fi
;;
env|svc)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

Status: 🏗 In progress

Development

Successfully merging this pull request may close these issues.

3 participants