#!/usr/bin/env bash __caddy_command_list() { printf '%s\n' up down restart reload status logs edit fmt validate version raw help } __caddy_read_helper_value() { local helper_env="$1" local key="$2" [[ -f "${helper_env}" ]] || return 1 sed -n "s/^${key}=\\\"\\(.*\\)\\\"$/\\1/p" "${helper_env}" | head -n 1 } __caddy_find_helper_env_in_tree() { local search_dir="${PWD}" while [[ -n "${search_dir}" && "${search_dir}" != "/" ]]; do if [[ -f "${search_dir}/.helper.env" ]]; then printf '%s\n' "${search_dir}/.helper.env" return 0 fi search_dir="$(dirname "${search_dir}")" done if [[ -f "/.helper.env" ]]; then printf '%s\n' "/.helper.env" return 0 fi return 1 } __caddy_active_helper_env() { if [[ -n "${CADDY_HELPER_ENV:-}" && -f "${CADDY_HELPER_ENV}" ]]; then printf '%s\n' "${CADDY_HELPER_ENV}" return 0 fi __caddy_find_helper_env_in_tree && return 0 if [[ -f "$(__caddy_binary_helper_env)" ]]; then printf '%s\n' "$(__caddy_binary_helper_env)" return 0 fi local default_docker_root default_docker_root="$HOME/projects/caddy" if [[ -f "${default_docker_root}/.helper.env" ]]; then printf '%s\n' "${default_docker_root}/.helper.env" return 0 fi return 1 } __caddy_detect_mode() { if [[ -n "${CADDY_MODE:-}" ]]; then printf '%s\n' "${CADDY_MODE}" return 0 fi local active_helper_env active_helper_env="$(__caddy_active_helper_env)" || true if [[ -n "${active_helper_env}" ]]; then __caddy_read_helper_value "${active_helper_env}" "CADDY_MODE" && return 0 fi printf 'auto\n' } __caddy_binary_config() { printf '%s\n' "${CADDY_BINARY_CONFIG:-/etc/caddy/Caddyfile}" } __caddy_binary_exec() { printf '%s\n' "${CADDY_BINARY_EXEC:-/usr/local/bin/caddy}" } __caddy_binary_helper_env() { printf '%s\n' "${CADDY_BINARY_HELPER_ENV:-/etc/caddy/.helper.env}" } __caddy_docker_root() { if [[ -n "${CADDY_DOCKER_ROOT:-}" ]]; then printf '%s\n' "${CADDY_DOCKER_ROOT}" return 0 fi local active_helper_env active_helper_env="$(__caddy_active_helper_env)" || true if [[ -n "${active_helper_env}" ]]; then local configured_docker_root configured_docker_root="$(__caddy_read_helper_value "${active_helper_env}" "CADDY_DOCKER_ROOT")" if [[ -n "${configured_docker_root}" ]]; then printf '%s\n' "${configured_docker_root}" return 0 fi fi printf '%s\n' "$HOME/projects/caddy" } __caddy_docker_helper_env() { printf '%s\n' "$(__caddy_docker_root)/.helper.env" } __caddy_editor() { if [[ -n "${CADDY_EDITOR:-}" ]]; then printf '%s\n' "${CADDY_EDITOR}" return 0 fi local resolved_mode resolved_mode="$(__caddy_resolved_mode)" local active_helper_env active_helper_env="$(__caddy_active_helper_env)" || true if [[ -n "${active_helper_env}" ]]; then __caddy_read_helper_value "${active_helper_env}" "CADDY_EDITOR" && return 0 fi printf 'vim\n' } __caddy_resolved_mode() { local mode mode="$(__caddy_detect_mode)" if [[ "${mode}" != "auto" ]]; then printf '%s\n' "${mode}" return fi if [[ -f "$(__caddy_docker_root)/compose.yml" ]]; then printf 'docker\n' return fi printf 'binary\n' } __caddy_as_root() { if [[ "${EUID}" -eq 0 ]]; then "$@" return fi sudo "$@" } __caddy_print_step() { printf '[caddy] %s\n' "$1" } __caddy_print_reload_complete() { printf '[caddy] Reload completed.\n' } __caddy_print_version_header() { local mode="$1" printf 'Mode: %s\n' "${mode}" } __caddy_print_key_value() { local key="$1" local value="$2" printf '%-18s %s\n' "${key}:" "${value}" } __caddy_path_status() { local target_path="$1" if [[ -e "${target_path}" ]]; then printf 'present' return fi printf 'missing' } __caddy_binary_static_site_status() { if [[ -d "/etc/caddy/site" ]]; then printf 'enabled (/etc/caddy/site)' return fi printf 'disabled' } __caddy_docker_static_site_status() { local docker_root docker_root="$(__caddy_docker_root)" if [[ -d "${docker_root}/site" ]]; then printf 'enabled (%s/site -> /srv)\n' "${docker_root}" return fi printf 'disabled\n' } __caddy_print_binary_version_details() { local helper_env helper_env="$(__caddy_binary_helper_env)" __caddy_print_key_value "Command" "caddy version" __caddy_print_key_value "Binary" "$(__caddy_binary_exec)" __caddy_print_key_value "Config root" "/etc/caddy" __caddy_print_key_value "Caddyfile" "$(__caddy_binary_config)" __caddy_print_key_value "Log dir" "/etc/caddy/logs" __caddy_print_key_value "State dir" "/etc/caddy/state" __caddy_print_key_value "Static site" "$(__caddy_binary_static_site_status)" __caddy_print_key_value "Helper script" "/etc/caddy/.bash_caddy" __caddy_print_key_value "Helper env" "${helper_env}" __caddy_print_key_value "Shell hook rc" "${HOME}/.$(basename "${SHELL:-bash}")rc" __caddy_print_key_value "Systemd unit" "/etc/systemd/system/caddy.service" printf '\nGenerated paths:\n' __caddy_print_key_value "binary" "/usr/local/bin/caddy [$(__caddy_path_status "/usr/local/bin/caddy")]" __caddy_print_key_value "config root" "/etc/caddy [$(__caddy_path_status "/etc/caddy")]" __caddy_print_key_value "caddyfile" "/etc/caddy/Caddyfile [$(__caddy_path_status "/etc/caddy/Caddyfile")]" __caddy_print_key_value "helper" "/etc/caddy/.bash_caddy [$(__caddy_path_status "/etc/caddy/.bash_caddy")]" __caddy_print_key_value "helper env" "/etc/caddy/.helper.env [$(__caddy_path_status "/etc/caddy/.helper.env")]" __caddy_print_key_value "logs" "/etc/caddy/logs [$(__caddy_path_status "/etc/caddy/logs")]" __caddy_print_key_value "state" "/etc/caddy/state [$(__caddy_path_status "/etc/caddy/state")]" __caddy_print_key_value "static site" "/etc/caddy/site [$(__caddy_path_status "/etc/caddy/site")]" __caddy_print_key_value "service" "/etc/systemd/system/caddy.service [$(__caddy_path_status "/etc/systemd/system/caddy.service")]" printf '\n' } __caddy_print_docker_version_details() { local docker_root docker_root="$(__caddy_docker_root)" __caddy_print_key_value "Command" "caddy version" __caddy_print_key_value "Docker root" "${docker_root}" __caddy_print_key_value "Compose file" "${docker_root}/compose.yml" __caddy_print_key_value "Config dir" "${docker_root}/conf" __caddy_print_key_value "Caddyfile" "${docker_root}/conf/Caddyfile" __caddy_print_key_value "Log dir" "${docker_root}/logs" __caddy_print_key_value "Data dir" "${docker_root}/data" __caddy_print_key_value "Config state dir" "${docker_root}/config" __caddy_print_key_value "Static site" "$(__caddy_docker_static_site_status)" __caddy_print_key_value "Helper script" "${docker_root}/.bash_caddy" __caddy_print_key_value "Helper env" "${docker_root}/.helper.env" __caddy_print_key_value "Image source" "\${CADDY_DOCKER_IMAGE:-caddy:latest}" printf '\nGenerated paths:\n' __caddy_print_key_value "root" "${docker_root} [$(__caddy_path_status "${docker_root}")]" __caddy_print_key_value "compose" "${docker_root}/compose.yml [$(__caddy_path_status "${docker_root}/compose.yml")]" __caddy_print_key_value "conf" "${docker_root}/conf [$(__caddy_path_status "${docker_root}/conf")]" __caddy_print_key_value "caddyfile" "${docker_root}/conf/Caddyfile [$(__caddy_path_status "${docker_root}/conf/Caddyfile")]" __caddy_print_key_value "helper" "${docker_root}/.bash_caddy [$(__caddy_path_status "${docker_root}/.bash_caddy")]" __caddy_print_key_value "helper env" "${docker_root}/.helper.env [$(__caddy_path_status "${docker_root}/.helper.env")]" __caddy_print_key_value "logs" "${docker_root}/logs [$(__caddy_path_status "${docker_root}/logs")]" __caddy_print_key_value "data" "${docker_root}/data [$(__caddy_path_status "${docker_root}/data")]" __caddy_print_key_value "config" "${docker_root}/config [$(__caddy_path_status "${docker_root}/config")]" __caddy_print_key_value "site" "${docker_root}/site [$(__caddy_path_status "${docker_root}/site")]" printf '\n' } __caddy_run_as_caddy() { if [[ "${EUID}" -eq 0 ]]; then sudo -u caddy "$@" return fi sudo -u caddy "$@" } __caddy_binary_fix_writable_paths() { __caddy_as_root install -d -m 0755 /etc/caddy/logs /etc/caddy/state __caddy_as_root chown -R caddy:caddy /etc/caddy/logs /etc/caddy/state } __caddy_tail_logs() { local follow="${1:-0}" shift local -a files=() local file_path for file_path in "$@"; do [[ -f "${file_path}" ]] && files+=("${file_path}") done if [[ "${#files[@]}" -eq 0 ]]; then return 1 fi if [[ "${follow}" == "1" ]]; then tail -n 100 -F "${files[@]}" return fi tail -n 100 "${files[@]}" } __caddy_parse_logs_args() { local log_type="${1:-}" local follow_flag="${2:-}" case "${log_type}" in runtime|access) ;; *) printf 'Usage: caddy logs runtime [-f] | caddy logs access [-f]\n' >&2 return 1 ;; esac case "${follow_flag}" in "" ) printf '%s %s\n' "${log_type}" "0" ;; -f) printf '%s %s\n' "${log_type}" "1" ;; *) printf 'Usage: caddy logs runtime [-f] | caddy logs access [-f]\n' >&2 return 1 ;; esac } __caddy_binary_logs() { local log_type="$1" local follow="${2:-0}" local log_path="/etc/caddy/logs/${log_type}.log" if [[ -f "${log_path}" ]]; then if [[ "${follow}" == "1" ]]; then __caddy_as_root tail -n 100 -F "${log_path}" return 0 fi __caddy_as_root tail -n 100 "${log_path}" return 0 fi if [[ "${log_type}" == "runtime" ]]; then if [[ "${follow}" == "1" ]]; then __caddy_as_root journalctl -u caddy -n 100 -f return 0 fi __caddy_as_root journalctl -u caddy -n 100 return 0 fi printf 'Access log file not found: %s\n' "${log_path}" >&2 return 1 } __caddy_docker_logs() { local log_type="$1" local follow="${2:-0}" local docker_root docker_root="$(__caddy_docker_root)" local log_path="${docker_root}/logs/${log_type}.log" __caddy_tail_logs "${follow}" "${log_path}" && return if [[ "${log_type}" == "runtime" ]]; then ( cd "${docker_root}" && if [[ "${follow}" == "1" ]]; then docker compose logs caddy -n 100 -f else docker compose logs caddy -n 100 fi ) return 0 fi printf 'Access log file not found: %s\n' "${log_path}" >&2 return 1 } __caddy_binary_control() { local action="$1" shift || true case "${action}" in up) __caddy_binary_fix_writable_paths __caddy_as_root systemctl start caddy ;; down) __caddy_as_root systemctl stop caddy ;; restart) __caddy_binary_fix_writable_paths __caddy_as_root systemctl restart caddy ;; status) __caddy_as_root systemctl "${action}" caddy ;; reload) __caddy_binary_fix_writable_paths __caddy_print_step "Formatting Caddyfile" __caddy_as_root "$(__caddy_binary_exec)" fmt --overwrite "$(__caddy_binary_config)" __caddy_print_step "Reloading systemd service" __caddy_as_root systemctl reload caddy __caddy_print_reload_complete ;; fmt) __caddy_as_root "$(__caddy_binary_exec)" fmt --overwrite "$(__caddy_binary_config)" ;; validate) __caddy_binary_fix_writable_paths __caddy_run_as_caddy "$(__caddy_binary_exec)" validate --config "$(__caddy_binary_config)" --adapter caddyfile ;; version) __caddy_print_version_header "binary" __caddy_print_binary_version_details "$(__caddy_binary_exec)" version ;; logs) local log_type local follow local parsed_logs parsed_logs="$(__caddy_parse_logs_args "${1:-}" "${2:-}")" || return 1 read -r log_type follow <<< "${parsed_logs}" __caddy_binary_logs "${log_type}" "${follow}" ;; edit) __caddy_as_root "$(__caddy_editor)" "$(__caddy_binary_config)" ;; raw) "$(__caddy_binary_exec)" "$@" ;; *) "$(__caddy_binary_exec)" "${action}" "$@" ;; esac } __caddy_docker_control() { local action="$1" shift || true local docker_root docker_root="$(__caddy_docker_root)" case "${action}" in up) (cd "${docker_root}" && docker compose up -d) ;; down) (cd "${docker_root}" && docker compose stop) ;; restart) (cd "${docker_root}" && docker compose restart) ;; status) (cd "${docker_root}" && docker compose ps) ;; reload) ( cd "${docker_root}" && __caddy_print_step "Formatting Caddyfile in container" && docker compose exec -w /etc/caddy caddy caddy fmt --overwrite /etc/caddy/Caddyfile && __caddy_print_step "Reloading container config" && docker compose exec -w /etc/caddy caddy caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile ) __caddy_print_reload_complete ;; fmt) (cd "${docker_root}" && docker compose exec -w /etc/caddy caddy caddy fmt --overwrite /etc/caddy/Caddyfile) ;; validate) (cd "${docker_root}" && docker compose exec -w /etc/caddy caddy caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile) ;; version) __caddy_print_version_header "docker" __caddy_print_docker_version_details (cd "${docker_root}" && docker compose exec caddy caddy version) ;; logs) local log_type local follow local parsed_logs parsed_logs="$(__caddy_parse_logs_args "${1:-}" "${2:-}")" || return 1 read -r log_type follow <<< "${parsed_logs}" __caddy_docker_logs "${log_type}" "${follow}" ;; edit) "$(__caddy_editor)" "${docker_root}/conf/Caddyfile" ;; raw) (cd "${docker_root}" && docker compose exec -w /etc/caddy caddy caddy "$@") ;; *) (cd "${docker_root}" && docker compose exec -w /etc/caddy caddy caddy "${action}" "$@") ;; esac } caddy() { local action="${1:-help}" if [[ $# -gt 0 ]]; then shift fi case "${action}" in start|stop) printf 'Use caddy up or caddy down.\n' >&2 return 1 ;; help|-h|--help) cat <<'EOF' Usage: caddy up|down|restart|reload|status|edit|fmt|validate|version caddy logs runtime [-f] caddy logs access [-f] caddy raw Environment: CADDY_MODE=binary|docker|auto CADDY_DOCKER_ROOT=$HOME/projects/caddy CADDY_EDITOR=vim EOF return 0 ;; esac case "$(__caddy_resolved_mode)" in binary) __caddy_binary_control "${action}" "$@" ;; docker) __caddy_docker_control "${action}" "$@" ;; *) printf 'Unsupported caddy mode\n' >&2 return 1 ;; esac } __caddy_complete() { local current_word="${COMP_WORDS[COMP_CWORD]}" COMPREPLY=($(compgen -W "$(__caddy_command_list)" -- "${current_word}")) } __caddy_complete_zsh() { local -a commands commands=("${(@f)$(__caddy_command_list)}") compadd -- "${commands[@]}" } if command -v complete >/dev/null 2>&1; then complete -F __caddy_complete caddy fi if [[ -n "${ZSH_VERSION:-}" ]]; then if ! typeset -f compdef >/dev/null 2>&1; then autoload -Uz compinit 2>/dev/null || true compinit -i >/dev/null 2>&1 || true fi if typeset -f compdef >/dev/null 2>&1 && [[ -z "${_comps[caddy]:-}" ]]; then compdef __caddy_complete_zsh caddy fi fi