diff --git a/dream-server/dream-cli b/dream-server/dream-cli index 6e160f648..8e659b8c9 100755 --- a/dream-server/dream-cli +++ b/dream-server/dream-cli @@ -510,6 +510,60 @@ get_compose_flags() { fi } +# Run `docker compose ` with a compact summary on success and a +# surfaced error banner on failure. +# +# Usage: _compose_run_with_summary +# Human-readable gerund phrase, e.g. "Restarting all services" +# Everything that goes after `docker compose`, including +# any `-f` flags resolved by get_compose_flags. +# +# Behavior: +# - Runs `docker compose --progress quiet `, capturing +# stdout+stderr to a mktemp log file. +# - On success: prints " — done" and removes the log file. +# - On failure: prints an error banner, surfaces up to 20 lines matching +# /error|unhealthy|failed|dependency/, preserves the full +# log file for inspection, and returns the compose exit +# code so the caller (under `set -e`) aborts with it. +# +# The summary grep pipeline can legitimately produce zero matches if the +# failing compose output has no error-keyword hits. `upstream/main` today +# runs under `set -e` only (no `pipefail`), so grep's exit 1 is absorbed +# by the pipeline's final-stage exit and the function continues to the +# log-path surface below. When pipefail is eventually adopted (sibling +# nounset/exit-code audit change), grep's no-match or a SIGPIPE from +# `head -20` on >20 matches would abort this function before the caller +# sees the compose log path or the compose exit code. `|| warn "..."` +# is the project-blessed form (per CLAUDE.md) for "tolerate this specific +# non-match and log why the summary is empty" — it costs nothing today +# and keeps the function correct under future pipefail. +_compose_run_with_summary() { + local _verb="$1"; shift + log "${_verb}..." + + local _compose_log + _compose_log=$(mktemp) + + local _rc=0 + docker compose --progress quiet "$@" >"$_compose_log" 2>&1 || _rc=$? + + if (( _rc == 0 )); then + success "${_verb} — done" + rm -f "$_compose_log" + return 0 + fi + + log_error "${_verb} failed:" + grep -iE 'error|unhealthy|failed|dependency' "$_compose_log" \ + | sed 's/^/ /' \ + | head -20 \ + || warn "(no error keywords matched in compose log)" + echo "" + log "Full compose output: $_compose_log" + return "$_rc" +} + #============================================================================= # Commands #============================================================================= @@ -759,14 +813,10 @@ cmd_restart() { read -ra flags <<< "$flags_str" if [[ -z "$service" ]]; then - log "Restarting all services..." - docker compose "${flags[@]}" up -d - success "All services restarted" + _compose_run_with_summary "Restarting all services" "${flags[@]}" up -d else service=$(resolve_service "$service") - log "Restarting $service..." - docker compose "${flags[@]}" up -d "$service" - success "$service restarted" + _compose_run_with_summary "Restarting $service" "${flags[@]}" up -d "$service" fi } @@ -782,14 +832,10 @@ cmd_stop() { read -ra flags <<< "$flags_str" if [[ -z "$service" ]]; then - log "Stopping all services..." - docker compose "${flags[@]}" down - success "All services stopped" + _compose_run_with_summary "Stopping all services" "${flags[@]}" down else service=$(resolve_service "$service") - log "Stopping $service..." - docker compose "${flags[@]}" stop "$service" - success "$service stopped" + _compose_run_with_summary "Stopping $service" "${flags[@]}" stop "$service" fi } @@ -807,15 +853,11 @@ cmd_start() { read -ra flags <<< "$flags_str" if [[ -z "$service" ]]; then - log "Starting all services..." - docker compose "${flags[@]}" up -d - success "All services started" + _compose_run_with_summary "Starting all services" "${flags[@]}" up -d else service=$(resolve_service "$service") _run_hook "$service" "pre_start" - log "Starting $service..." - docker compose "${flags[@]}" up -d "$service" - success "$service started" + _compose_run_with_summary "Starting $service" "${flags[@]}" up -d "$service" _run_hook "$service" "post_start" fi } @@ -1021,13 +1063,11 @@ cmd_update() { warn "dream-update.sh and dream-backup.sh not found; skipping pre-update snapshot." fi - log "Pulling latest images..." - if ! docker compose "${flags[@]}" pull; then + if ! _compose_run_with_summary "Pulling latest images" "${flags[@]}" pull; then error "Failed to pull latest images" fi - log "Recreating containers with new images..." - if ! docker compose "${flags[@]}" up -d --force-recreate; then + if ! _compose_run_with_summary "Recreating containers with new images" "${flags[@]}" up -d --force-recreate; then error "Failed to recreate containers. Run 'dream rollback' to restore previous state." fi diff --git a/dream-server/lib/service-registry.sh b/dream-server/lib/service-registry.sh index 71fd8049e..70a12715b 100755 --- a/dream-server/lib/service-registry.sh +++ b/dream-server/lib/service-registry.sh @@ -228,10 +228,21 @@ sr_resolve_ports() { fi } -# Resolve a user-provided name to a compose service ID +# Resolve a user-provided name to a compose service ID. +# +# Users copy container names (e.g. `dream-token-spy`) from `docker ps` and +# expect them to work as arguments to `dream restart|stop|start|update`. The +# registry loader names every container `dream-` (or the manifest's +# explicit `container_name`, which by convention follows the same pattern), +# so stripping a leading `dream-` recovers the alias key when the literal +# input doesn't match an alias. sr_resolve() { sr_load local input="$1" + if [[ -z "${SERVICE_ALIASES[$input]:-}" && "$input" == dream-* ]]; then + local _stripped="${input#dream-}" + [[ -n "${SERVICE_ALIASES[$_stripped]:-}" ]] && input="$_stripped" + fi echo "${SERVICE_ALIASES[$input]:-$input}" }