# bash completion for ai-sandbox. # # Install (either): # mkdir -p ~/.local/share/bash-completion/completions # cp contrib/ai-sandbox.bash ~/.local/share/bash-completion/completions/ai-sandbox # (it also registers ai-sandbox-autophase, which shares this file) # or source it from ~/.bashrc -- put the `source` line *after* the alias, since # bash needs `complete` registered under the alias name (`alias pi=ai-sandbox`). # # The flag list mirrors parse_args() in bin/ai-sandbox -- keep them in sync. # compgen prints one candidate per line; reading them instead of dropping them through an # unquoted $(...) keeps a candidate that contains a space (a file name, a phase directory) # in one piece. Process substitution, not a pipe: COMPREPLY has to survive the call. _aisb_fill() { local w; while IFS= read -r w; do COMPREPLY+=("$w"); done; } _ai_sandbox_completions() { local cur prev word first="" sub="" i if type _get_comp_words_by_ref >/dev/null 2>&1; then _get_comp_words_by_ref -n : cur prev else cur="${COMP_WORDS[COMP_CWORD]}"; prev="${COMP_WORDS[COMP_CWORD-1]:-}" fi local subs="build update doctor gate ps clean inspect version help" # flags that consume the next word, so their values are not mistaken for # positional (harness) arguments. local value_flags=" --net --allow --allow-file --proxy --mount --workdir --agents-dir --env --env-file --base-image --install --harness --pi-token-env --cpus --memory --pids --tmpfs-size --name --label " # Walk the words before the cursor to find the subcommand, skipping option values. local skip=0 for ((i = 1; i < COMP_CWORD; i++)); do word="${COMP_WORDS[i]}" if (( skip )); then skip=0; continue; fi if [[ "$word" == --* && "$value_flags" == *" $word "* ]]; then skip=1; continue; fi [[ "$word" == -* ]] && continue [[ -z "$first" ]] && first="$word" done [[ " $subs " == *" $first "* ]] && sub="$first" case "$prev" in # values of the flag just typed --net) COMPREPLY=(); _aisb_fill < <(compgen -W "none allow full" -- "$cur"); return ;; --harness) COMPREPLY=(); _aisb_fill < <(compgen -W "pi codex claude none" -- "$cur"); return ;; --env) COMPREPLY=(); _aisb_fill < <(compgen -A export -- "$cur"); return ;; --allow) # hosts already in the allow files; no DNS, no network local hosts hosts="$(sed 's/#.*//' "$HOME/.config/ai-sandbox/allow" ./.ai-sandbox/allow 2>/dev/null | tr ',[:space:]' '\n\n')" COMPREPLY=(); _aisb_fill < <(compgen -W "$hosts" -- "$cur") (( ${#COMPREPLY[@]} )) || COMPREPLY=( "$cur" ) # don't fall back to filenames return ;; --proxy) COMPREPLY=( "$cur" ); return ;; --allow-file|--env-file) COMPREPLY=(); _aisb_fill < <(compgen -f -- "$cur"); return ;; --workdir) COMPREPLY=(); _aisb_fill < <(compgen -W "auto" -- "$cur") # 'auto' mirrors the host path (( ${#COMPREPLY[@]} )) || _aisb_fill < <(compgen -d -- "$cur"); return ;; --mount|--agents-dir) COMPREPLY=(); _aisb_fill < <(compgen -d -- "$cur"); return ;; esac # A non-flag word before the cursor that is not a subcommand means the rest goes # to the harness: ai-sandbox stops parsing there, so offer nothing, not sandbox flags. [[ -n "$first" && -z "$sub" ]] && return 1 [[ -z "$first" && "$cur" != -* ]] && { COMPREPLY=(); _aisb_fill < <(compgen -W "$subs" -- "$cur"); return; } # subcommand-specific flags (each subcommand dies on anything else); # `inspect` takes the full policy flag set, so it falls through to the list below. case "$sub" in build) COMPREPLY=(); _aisb_fill < <(compgen -W "--rebuild -f --refresh-base -B --gate" -- "$cur"); return ;; update) COMPREPLY=(); _aisb_fill < <(compgen -W "--check --no-gate" -- "$cur"); return ;; doctor) COMPREPLY=(); _aisb_fill < <(compgen -W "--deep -d" -- "$cur"); return ;; clean) COMPREPLY=(); _aisb_fill < <(compgen -W "--all -a --volumes -V" -- "$cur"); return ;; gate) COMPREPLY=(); _aisb_fill < <(compgen -W "up down status logs" -- "$cur"); return ;; ps|version) return 1 ;; esac COMPREPLY=() _aisb_fill < <(compgen -W \ "--net --allow --allow-file --proxy --no-gate --mount --no-cwd --workdir --home-cache --ephemeral-home --read-only --agents --no-agents --agents-dir --env --env-file --base-image --install --harness --pi-token-env --no-pi-token-env --no-pi-config --cpus --memory --pids --tmpfs-size --shell --interactive --no-interactive --no-tty --keep --name --label --no-build --dry-run --print-policy --print-entrypoint --print-containerfile --allow-unenforced --insecure-keep-root --strict-args -h --help -V" \ -- "$cur") } # ai-sandbox-autophase: the pipeline driver. Its own flag list, mirroring its parser. _ai_sandbox_autophase_completions() { local cur if type _get_comp_words_by_ref >/dev/null 2>&1; then _get_comp_words_by_ref -n : cur else cur="${COMP_WORDS[COMP_CWORD]}" fi case "$cur" in --validate-in=*) COMPREPLY=(); _aisb_fill < <(compgen -W "sandbox host" -- "${cur#*=}"); return ;; esac COMPREPLY=() _aisb_fill < <(compgen -W \ "--pi-cmd --rc-files --lib --skill-dir --sandbox-args --validate-in --no-push --no-preflight --no-notes --trust-git-changes --unit --phase --max-units --status --dry-run -n -h --help -V" \ -- "$cur") # --unit and --phase name pending work: offer the phase directories first, then the # ordinary file completion the caller would otherwise have got. local todo="${PWD}/.agents/phases/todo" case "${COMP_WORDS[COMP_CWORD-1]}" in --unit|--phase) if [[ -d "$todo" ]]; then local d for d in "$todo"/"$cur"*; do [[ -e "$d" ]] && COMPREPLY+=("${d#"$todo"/}") # named as the driver wants them done _aisb_fill < <(compgen -f -- "$cur") fi ;; esac } complete -o default -F _ai_sandbox_completions ai-sandbox 2>/dev/null || true complete -o default -F _ai_sandbox_autophase_completions ai-sandbox-autophase 2>/dev/null || true if [[ $- == *i* ]]; then # also complete the harness alias for _aisb in $(compgen -A alias 2>/dev/null); do if alias "$_aisb" 2>/dev/null | grep -q 'ai-sandbox'; then complete -o default -F _ai_sandbox_completions "$_aisb" 2>/dev/null || true fi done unset _aisb fi