Add prompt_reinstall_mode helper and document the update/fresh/cancel convention

Replaces the y/n "update in place?" prompt in asterisk.sh/asterisk-do.sh
with an explicit r/f/c choice — (r)einstall in place, (f)ull install,
(c)ancel — defaulting to cancel on a bare Enter (or Ctrl-D) instead of
falling through to a destructive full reinstall.

Adds prompt_reinstall_mode to lib/common.sh (plus matching standalone
stubs in both asterisk scripts for when they run without the full repo)
and documents the convention in CLAUDE.md: any service with a persistent
install directory should offer this choice on rerun instead of re-asking
every prompt just to pick up a script fix.
This commit is contained in:
Claude
2026-07-20 01:23:22 +00:00
parent 0d719a5cad
commit 8ee35e1995
4 changed files with 189 additions and 61 deletions
+49 -1
View File
@@ -125,9 +125,12 @@ log_error "message" # red [ERROR]
```bash
prompt_yn "Question? (y/n):" "default_y_or_n" VARNAME
prompt_text "Question? [default]:" "default" VARNAME
prompt_reinstall_mode VARNAME # sets VARNAME to: update | fresh | cancel
```
When `UNATTENDED=true` both functions skip the prompt and use the default.
When `UNATTENDED=true` all three skip the prompt; `prompt_yn`/`prompt_text` use
their given default, `prompt_reinstall_mode` always resolves to `cancel`. See
**Update vs. fresh reinstall on rerun** below for how to use the latter.
### Pre-flight
@@ -262,6 +265,51 @@ fi
Put the check early — after any pure-display output (banners, info text)
but before the first write.
## Update vs. fresh reinstall on rerun
Every service should detect an existing install at the top of its
`install_<name>()` — after the `DRY_RUN` check, before any prompts — and
offer `prompt_reinstall_mode` instead of silently re-running every prompt
(domain, secrets, firewall, Authelia, extras...) from scratch. What counts
as "already installed" is service-specific: usually `docker-compose.yml` and
`.env` both existing in the service's `$DOCKER_DIR/<name>` directory.
```bash
if [[ -f "$DIR/docker-compose.yml" && -f "$DIR/.env" ]]; then
local MODE=""
prompt_reinstall_mode MODE
case "$MODE" in
update)
# Refresh vendor files / config templates, rebuild, done.
# Do NOT touch .env, firewall rules, or Caddy/Authelia config.
...
return 0
;;
cancel)
log_info "Leaving the existing install as-is."
return 0
;;
fresh) ;; # fall through to the full install flow below
esac
fi
```
`update` should be genuinely non-destructive: refresh whatever the service
vendors or templates (Docker image sources, config templates,
`docker-compose.yml`) and rebuild/restart, but never touch `.env`, firewall
rules, or reverse-proxy/SSO config that's already in place. If the
vendor-copy or `docker-compose.yml`-generation logic is more than a few
lines, factor it into a helper function so the fresh-install path and the
update path share one copy instead of drifting apart — see
`_asterisk_do_refresh_vendor_files`/`_asterisk_do_write_compose` in
`services/asterisk-do.sh` (and their `_asterisk_*` counterparts in
`services/asterisk.sh`) for the reference pattern.
`cancel` must leave the install completely untouched — it's the default for
a reason (a stray Enter on a service you're just checking on shouldn't
trigger anything). `fresh` runs the exact same flow a first-time install
would, prompts included.
## .env files and secrets
Generate passwords with `generate_password` (never hardcode them).
+26
View File
@@ -362,6 +362,32 @@ prompt_text() {
eval "$varname='${response:-$default}'"
}
# Prompt for how to handle a service that's already installed, honoring
# unattended. prompt_reinstall_mode VARNAME
# Sets VARNAME to one of: update | fresh | cancel
# Enter (no input) and any unrecognized input both resolve to "cancel" — this
# guards a destructive full reinstall behind a deliberate keypress instead of
# a stray Enter. Unattended mode always resolves to "cancel" too: never
# silently touch an existing install when nobody's watching the prompt.
prompt_reinstall_mode() {
local varname="$1" response
if [ "$UNATTENDED" = true ]; then
eval "$varname='cancel'"
echo "Existing install detected — leaving it as-is [auto: cancel, unattended mode]"
return
fi
echo " Existing install detected. Choose:"
echo " r) Reinstall in place — refresh vendor files/config, keep existing settings"
echo " f) Full install — re-run every prompt from scratch"
echo " c) Cancel — leave everything as-is [default]"
read -p " Choice [r/f/c, Enter=cancel]: " response
case "${response,,}" in
r) eval "$varname='update'" ;;
f) eval "$varname='fresh'" ;;
*) eval "$varname='cancel'" ;;
esac
}
# ── Per-service README generation ────────────────────────────────────────────
# Write <dir>/README.md from stdin (markdown). Every module is encouraged to
# call this so each ~/docker/<service>/ folder is self-documenting.
+57 -30
View File
@@ -61,6 +61,25 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
eval "$_var='${_r:-$_def}'"
}
prompt_reinstall_mode() {
local _var="$1" _r
if [[ "${UNATTENDED:-false}" == "true" ]]; then
eval "$_var='cancel'"
echo "Existing install detected — leaving it as-is [auto: cancel, unattended mode]"
return
fi
echo " Existing install detected. Choose:"
echo " r) Reinstall in place — refresh vendor files/config, keep existing settings"
echo " f) Full install — re-run every prompt from scratch"
echo " c) Cancel — leave everything as-is [default]"
read -r -p " Choice [r/f/c, Enter=cancel]: " _r
case "${_r,,}" in
r) eval "$_var='update'" ;;
f) eval "$_var='fresh'" ;;
*) eval "$_var='cancel'" ;;
esac
}
configure_caddy_for_service() {
local _name="$1" _upstream="$2" _subdomain="$3" _extra="${4:-}"
local _caddy_dir="$DOCKER_DIR/caddy"
@@ -353,39 +372,47 @@ install_asterisk-do() {
if [[ -f "$EA_DIR/docker-compose.yml" && -f "$EA_DIR/.env" ]]; then
echo ""
log_info "Existing install found at $EA_DIR."
local UPDATE_INPLACE=""
prompt_yn "Update in place (refresh vendor files + docker-compose.yml, keep your existing domain/firewall/Authelia settings) instead of a full fresh reinstall? (y/n):" "y" UPDATE_INPLACE
if [[ "$UPDATE_INPLACE" =~ ^[Yy]$ ]]; then
mkdir -p "$EA_DIR/config/asterisk" "$EA_DIR/config/easy-asterisk" \
"$EA_DIR/logs" "$EA_DIR/spool" "$EA_DIR/lib" "$EA_DIR/exports"
ensure_docker_dir_ownership "$EA_DIR"
cd "$EA_DIR" || return 1
local REINSTALL_MODE=""
prompt_reinstall_mode REINSTALL_MODE
case "$REINSTALL_MODE" in
update)
mkdir -p "$EA_DIR/config/asterisk" "$EA_DIR/config/easy-asterisk" \
"$EA_DIR/logs" "$EA_DIR/spool" "$EA_DIR/lib" "$EA_DIR/exports"
ensure_docker_dir_ownership "$EA_DIR"
cd "$EA_DIR" || return 1
_asterisk_do_refresh_vendor_files
_asterisk_do_write_compose
_asterisk_do_refresh_vendor_files
_asterisk_do_write_compose
log_info "Rebuilding and restarting containers..."
if docker compose up -d --build --force-recreate; then
log_success "Update complete — vendor files and docker-compose.yml refreshed."
else
log_warning "docker compose up failed — check: docker compose -f $EA_DIR/docker-compose.yml logs"
fi
log_info "Rebuilding and restarting containers..."
if docker compose up -d --build --force-recreate; then
log_success "Update complete — vendor files and docker-compose.yml refreshed."
else
log_warning "docker compose up failed — check: docker compose -f $EA_DIR/docker-compose.yml logs"
fi
local _EXISTING_DOMAIN _EXISTING_PORT
_EXISTING_DOMAIN="$(grep -E '^DOMAIN_NAME=' .env | cut -d= -f2-)"
_EXISTING_PORT="$(grep -E '^WEB_ADMIN_PORT=' .env | cut -d= -f2-)"
echo ""
log_success "Existing .env, UFW rules, Cloud Firewall, and Caddy/Authelia config were left untouched."
if [[ -n "$_EXISTING_DOMAIN" ]]; then
echo " Web admin: https://${_EXISTING_DOMAIN}/"
else
echo " Web admin: http://<droplet-ip>:${_EXISTING_PORT:-8081}"
fi
echo " Logs: docker compose -f $EA_DIR/docker-compose.yml logs -f"
echo ""
return 0
fi
log_info "Proceeding with a full fresh reinstall — existing config will be reused where prompts match, everything else re-asked."
local _EXISTING_DOMAIN _EXISTING_PORT
_EXISTING_DOMAIN="$(grep -E '^DOMAIN_NAME=' .env | cut -d= -f2-)"
_EXISTING_PORT="$(grep -E '^WEB_ADMIN_PORT=' .env | cut -d= -f2-)"
echo ""
log_success "Existing .env, UFW rules, Cloud Firewall, and Caddy/Authelia config were left untouched."
if [[ -n "$_EXISTING_DOMAIN" ]]; then
echo " Web admin: https://${_EXISTING_DOMAIN}/"
else
echo " Web admin: http://<droplet-ip>:${_EXISTING_PORT:-8081}"
fi
echo " Logs: docker compose -f $EA_DIR/docker-compose.yml logs -f"
echo ""
return 0
;;
cancel)
log_info "Leaving the existing install as-is — nothing changed."
return 0
;;
fresh)
log_info "Proceeding with a full fresh reinstall — every prompt below runs from scratch."
;;
esac
fi
# ── Bring in base first, if this is a genuinely fresh box ─────────────────
+57 -30
View File
@@ -59,6 +59,25 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
eval "$_var='${_r:-$_def}'"
}
prompt_reinstall_mode() {
local _var="$1" _r
if [[ "${UNATTENDED:-false}" == "true" ]]; then
eval "$_var='cancel'"
echo "Existing install detected — leaving it as-is [auto: cancel, unattended mode]"
return
fi
echo " Existing install detected. Choose:"
echo " r) Reinstall in place — refresh vendor files/config, keep existing settings"
echo " f) Full install — re-run every prompt from scratch"
echo " c) Cancel — leave everything as-is [default]"
read -r -p " Choice [r/f/c, Enter=cancel]: " _r
case "${_r,,}" in
r) eval "$_var='update'" ;;
f) eval "$_var='fresh'" ;;
*) eval "$_var='cancel'" ;;
esac
}
configure_caddy_for_service() {
local _name="$1" _upstream="$2" _subdomain="$3" _extra="${4:-}"
local _caddy_dir="$DOCKER_DIR/caddy"
@@ -333,39 +352,47 @@ install_asterisk() {
if [[ -f "$EA_DIR/docker-compose.yml" && -f "$EA_DIR/.env" ]]; then
echo ""
log_info "Existing install found at $EA_DIR."
local UPDATE_INPLACE=""
prompt_yn "Update in place (refresh vendor files + docker-compose.yml, keep your existing domain/VLAN/Authelia settings) instead of a full fresh reinstall? (y/n):" "y" UPDATE_INPLACE
if [[ "$UPDATE_INPLACE" =~ ^[Yy]$ ]]; then
mkdir -p "$EA_DIR/config/asterisk" "$EA_DIR/config/easy-asterisk" \
"$EA_DIR/logs" "$EA_DIR/spool" "$EA_DIR/lib" "$EA_DIR/exports"
ensure_docker_dir_ownership "$EA_DIR"
cd "$EA_DIR" || return 1
local REINSTALL_MODE=""
prompt_reinstall_mode REINSTALL_MODE
case "$REINSTALL_MODE" in
update)
mkdir -p "$EA_DIR/config/asterisk" "$EA_DIR/config/easy-asterisk" \
"$EA_DIR/logs" "$EA_DIR/spool" "$EA_DIR/lib" "$EA_DIR/exports"
ensure_docker_dir_ownership "$EA_DIR"
cd "$EA_DIR" || return 1
_asterisk_refresh_vendor_files
_asterisk_write_compose
_asterisk_refresh_vendor_files
_asterisk_write_compose
log_info "Rebuilding and restarting containers..."
if docker compose up -d --build --force-recreate; then
log_success "Update complete — vendor files and docker-compose.yml refreshed."
else
log_warning "docker compose up failed — check: docker compose -f $EA_DIR/docker-compose.yml logs"
fi
log_info "Rebuilding and restarting containers..."
if docker compose up -d --build --force-recreate; then
log_success "Update complete — vendor files and docker-compose.yml refreshed."
else
log_warning "docker compose up failed — check: docker compose -f $EA_DIR/docker-compose.yml logs"
fi
local _EXISTING_DOMAIN _EXISTING_PORT
_EXISTING_DOMAIN="$(grep -E '^DOMAIN_NAME=' .env | cut -d= -f2-)"
_EXISTING_PORT="$(grep -E '^WEB_ADMIN_PORT=' .env | cut -d= -f2-)"
echo ""
log_success "Existing .env, UFW rules, and Caddy/Authelia config were left untouched."
if [[ -n "$_EXISTING_DOMAIN" ]]; then
echo " Web admin: https://${_EXISTING_DOMAIN}/"
else
echo " Web admin: http://$(hostname -I 2>/dev/null | awk '{print $1}' || echo localhost):${_EXISTING_PORT:-8081}"
fi
echo " Logs: docker compose -f $EA_DIR/docker-compose.yml logs -f"
echo ""
return 0
fi
log_info "Proceeding with a full fresh reinstall — existing config will be reused where prompts match, everything else re-asked."
local _EXISTING_DOMAIN _EXISTING_PORT
_EXISTING_DOMAIN="$(grep -E '^DOMAIN_NAME=' .env | cut -d= -f2-)"
_EXISTING_PORT="$(grep -E '^WEB_ADMIN_PORT=' .env | cut -d= -f2-)"
echo ""
log_success "Existing .env, UFW rules, and Caddy/Authelia config were left untouched."
if [[ -n "$_EXISTING_DOMAIN" ]]; then
echo " Web admin: https://${_EXISTING_DOMAIN}/"
else
echo " Web admin: http://$(hostname -I 2>/dev/null | awk '{print $1}' || echo localhost):${_EXISTING_PORT:-8081}"
fi
echo " Logs: docker compose -f $EA_DIR/docker-compose.yml logs -f"
echo ""
return 0
;;
cancel)
log_info "Leaving the existing install as-is — nothing changed."
return 0
;;
fresh)
log_info "Proceeding with a full fresh reinstall — every prompt below runs from scratch."
;;
esac
fi
mkdir -p "$EA_DIR"