Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 62 additions & 22 deletions dream-server/dream-cli
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,60 @@ get_compose_flags() {
fi
}

# Run `docker compose <args...>` with a compact summary on success and a
# surfaced error banner on failure.
#
# Usage: _compose_run_with_summary <verb> <compose_args...>
# <verb> Human-readable gerund phrase, e.g. "Restarting all services"
# <compose_args> Everything that goes after `docker compose`, including
# any `-f` flags resolved by get_compose_flags.
#
# Behavior:
# - Runs `docker compose --progress quiet <compose_args>`, capturing
# stdout+stderr to a mktemp log file.
# - On success: prints "<verb> — 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
#=============================================================================
Expand Down Expand Up @@ -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
}

Expand All @@ -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
}

Expand All @@ -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
}
Expand Down Expand Up @@ -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

Expand Down
13 changes: 12 additions & 1 deletion dream-server/lib/service-registry.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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-<sid>` (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}"
}

Expand Down
Loading