diff --git a/compose.d/.gitkeep b/compose.d/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/compose.d/00-goclaw.yml b/compose.d/00-goclaw.yml deleted file mode 120000 index 5c8318efc..000000000 --- a/compose.d/00-goclaw.yml +++ /dev/null @@ -1 +0,0 @@ -../docker-compose.yml \ No newline at end of file diff --git a/compose.options/11-postgres.yml b/compose.options/11-postgres.yml deleted file mode 120000 index 618529b52..000000000 --- a/compose.options/11-postgres.yml +++ /dev/null @@ -1 +0,0 @@ -../docker-compose.postgres.yml \ No newline at end of file diff --git a/compose.options/12-selfservice.yml b/compose.options/12-selfservice.yml deleted file mode 120000 index 6c90b043b..000000000 --- a/compose.options/12-selfservice.yml +++ /dev/null @@ -1 +0,0 @@ -../docker-compose.selfservice.yml \ No newline at end of file diff --git a/compose.options/13-upgrade.yml b/compose.options/13-upgrade.yml deleted file mode 120000 index cfcde769d..000000000 --- a/compose.options/13-upgrade.yml +++ /dev/null @@ -1 +0,0 @@ -../docker-compose.upgrade.yml \ No newline at end of file diff --git a/compose.options/14-browser.yml b/compose.options/14-browser.yml deleted file mode 120000 index eff2597a2..000000000 --- a/compose.options/14-browser.yml +++ /dev/null @@ -1 +0,0 @@ -../docker-compose.browser.yml \ No newline at end of file diff --git a/compose.options/15-otel.yml b/compose.options/15-otel.yml deleted file mode 120000 index 91959568e..000000000 --- a/compose.options/15-otel.yml +++ /dev/null @@ -1 +0,0 @@ -../docker-compose.otel.yml \ No newline at end of file diff --git a/compose.options/16-redis.yml b/compose.options/16-redis.yml deleted file mode 120000 index ff53cce53..000000000 --- a/compose.options/16-redis.yml +++ /dev/null @@ -1 +0,0 @@ -../docker-compose.redis.yml \ No newline at end of file diff --git a/compose.options/17-sandbox.yml b/compose.options/17-sandbox.yml deleted file mode 120000 index 671aa3300..000000000 --- a/compose.options/17-sandbox.yml +++ /dev/null @@ -1 +0,0 @@ -../docker-compose.sandbox.yml \ No newline at end of file diff --git a/compose.options/18-tailscale.yml b/compose.options/18-tailscale.yml deleted file mode 120000 index 945eb09b4..000000000 --- a/compose.options/18-tailscale.yml +++ /dev/null @@ -1 +0,0 @@ -../docker-compose.tailscale.yml \ No newline at end of file diff --git a/options/postgres/postgres-low-cpu.yml b/options/postgres/postgres+low-cpu.yml similarity index 100% rename from options/postgres/postgres-low-cpu.yml rename to options/postgres/postgres+low-cpu.yml diff --git a/prepare-compose.sh b/prepare-compose.sh index c3ff50dfe..34ce4eb8a 100755 --- a/prepare-compose.sh +++ b/prepare-compose.sh @@ -1,59 +1,52 @@ #!/usr/bin/env bash -# Generates COMPOSE_FILE from compose.d/*.yml +# Generates COMPOSE_FILE from .env-compose selection, writes to .env set -euo pipefail SCRIPT="${BASH_SOURCE[0]}" SCRIPT_DIR="$(cd "$(dirname "${SCRIPT}")" && pwd)" -ENV_FILE="${GOCLAW_ENV_FILE:-$SCRIPT_DIR/.env}" +ENV_FILE="$SCRIPT_DIR/.env" +ENV_COMPOSE="$SCRIPT_DIR/.env-compose" +EDITOR="${EDITOR:-${VISUAL:-nano}}" loud() { [[ "${QUIET:-false}" != true ]] && echo "$@" true } -# Show help -if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then - echo "Usage: $SCRIPT [--quiet] [--skip-validation]" - echo "" - echo " --quiet Suppress normal output" - echo " --skip-validation Skip docker compose config validation" - echo "" - echo " Generates COMPOSE_FILE from compose.d/*.yml files (sorted)" - echo " Updates .env with the resulting COMPOSE_FILE value" - echo "" - echo "Note: docker-compose reads .env automatically" - echo " for podman-compose: source .env first" - exit 0 -fi - -# Parse flags -SKIP_VALIDATION=false -for arg in "$@"; do - case "$arg" in - --quiet) QUIET=true ;; - --skip-validation) SKIP_VALIDATION=true ;; - esac -done - -cd "$SCRIPT_DIR" >/dev/null 2>&1 +# Find all compose yml files in root dir and options/ subdirectory +find_compose_files() { + local dir="$1" + ( + find "$dir" -maxdepth 1 -name "*.yml" 2>/dev/null + if [[ -d "$dir/options" ]]; then + find "$dir/options" -maxdepth 2 -name "*.yml" 2>/dev/null + fi + ) | sort +} -[[ ! -f "docker-compose.yml" ]] && echo "docker-compose.yml not found" && exit 1 +# Check if a file is a compose file by content +is_compose_file() { + [[ "$1" == *.yml ]] || return 1 + grep -q "^services:\|^networks:\|^volumes:" "$1" 2>/dev/null +} -# Build COMPOSE_FILE from compose.d files (sorted) -COMPOSE_FILE="" -for f in compose.d/*.yml; do - [[ -e "$f" ]] && COMPOSE_FILE="$COMPOSE_FILE${COMPOSE_FILE:+:}$f" -done -export COMPOSE_FILE +# Categorize a compose file by filename only +# . = service, + = overlay, otherwise = root +categorize_compose() { + local name="${1%.yml}" + [[ "$name" == *+* ]] && echo "overlay" && return + [[ "$name" == *.* ]] && echo "service" && return + echo "root" +} -# Validate compose files -if [[ "$SKIP_VALIDATION" != true ]]; then - DOCKER_CMD="${DOCKER_CMD:-docker}" - $DOCKER_CMD compose config > /dev/null 2>&1 || { echo "Compose config validation failed"; exit 1; } - loud "Compose config valid" -fi +# Read current COMPOSE_FILE from .env, return colon-separated list +read_compose_file() { + if [[ -f "$ENV_FILE" ]]; then + grep "^COMPOSE_FILE=" "$ENV_FILE" 2>/dev/null | head -1 | sed 's/^COMPOSE_FILE=//' | tr -d "'" + fi +} -# Update a key=value line in .env safely (via temp file) +# Update a key=value line in .env safely update_env() { local key="$1" value="$2" if grep -q "^${key}=" "$ENV_FILE" 2>/dev/null; then @@ -63,14 +56,220 @@ update_env() { fi } -# Update .env with COMPOSE_FILE and GOCLAW_DIR -if [[ -f "$ENV_FILE" ]]; then - update_env "COMPOSE_FILE" "$COMPOSE_FILE" - update_env "GOCLAW_DIR" "$SCRIPT_DIR" - loud "COMPOSE_FILE updated in $ENV_FILE" - loud " COMPOSE_FILE=$COMPOSE_FILE" -else - loud "File not found: $ENV_FILE" +# Write COMPOSE_FILE to .env +write_compose_file() { + update_env "COMPOSE_FILE" "$1" + loud "COMPOSE_FILE='$1'" +} + +# Generate .env-compose from available files and current selection +do_generate() { + local current_compose="${1:-}" + local line + + echo "# Docker Compose file picker" + echo "# Lines starting with # are disabled" + echo "# Remove # to enable a file" + echo "" + + local roots="" services="" overlays="" + + while IFS= read -r line; do + [[ -z "$line" ]] && continue + if is_compose_file "$line"; then + local cat=$(categorize_compose "$line") + local rel="${line#$SCRIPT_DIR/}" + local enabled="# " + if [[ -n "$current_compose" && "$current_compose" == *"$rel"* ]]; then + enabled="" + fi + case "$cat" in + root) roots="${roots}${roots:+$'\n'}${enabled}${rel}" ;; + service) services="${services}${services:+$'\n'}${enabled}${rel}" ;; + overlay) overlays="${overlays}${overlays:+$'\n'}${enabled}${rel}" ;; + esac + fi + done < <(find_compose_files "$SCRIPT_DIR") + + if [[ -n "$roots" ]]; then + echo "# === ROOT (required) ===" + echo "$roots" + echo "" + fi + if [[ -n "$services" ]]; then + echo "# === SERVICE (optional) ===" + echo "$services" + echo "" + fi + if [[ -n "$overlays" ]]; then + echo "# === OVERLAY (optional) ===" + echo "$overlays" + fi +} + +# Parse enabled files from .env-compose (uncommented, non-empty lines) +do_parse() { + local result="" + local line + + while IFS= read -r line || [[ -n "$line" ]]; do + line="${line#"${line%%[![:space:]]*}"}" # trim leading whitespace + [[ -z "$line" || "$line" == \#* ]] && continue + line="${line#\# }" # remove "# " prefix + [[ -z "$line" || "$line" == \#* ]] && continue + [[ -n "$result" ]] && result="${result}:" + result="${result}${line}" + done < "$ENV_COMPOSE" + + echo "$result" +} + +# Apply selection from .env-compose to .env +do_update() { + if [[ ! -f "$ENV_COMPOSE" ]]; then + echo "No .env-compose found. Run '$SCRIPT --generate' first." + exit 1 + fi + + local selection + selection=$(do_parse) + + if [[ -z "$selection" ]]; then + echo "No compose files selected in .env-compose" + fi + + write_compose_file "$selection" + loud "Done. COMPOSE_FILE=$selection" +} + +# Validate compose files with docker/podman compose config +do_check() { + if [[ ! -f "$ENV_COMPOSE" ]]; then + echo "No .env-compose found" + exit 1 + fi + + # Source .env to get COMPOSE_FILE + set -a + source "$ENV_FILE" 2>/dev/null || true + set +a + + local engine="${DOCKER_CMD:-docker}" + if ! command -v "$engine" &>/dev/null; then + echo "$engine not found" + exit 1 + fi + + if "$engine" compose config >/dev/null 2>&1; then + echo "✓ Compose config valid" + exit 0 + else + echo "✗ Compose config invalid" + "$engine" compose config 2>&1 | head -5 + exit 1 + fi +} + +# Open editor, then apply +do_edit() { + if [[ ! -f "$ENV_COMPOSE" ]]; then + loud "Regenerating $ENV_COMPOSE..." + local current + current=$(read_compose_file) + do_generate "$current" > "$ENV_COMPOSE" + fi + + if ! "$EDITOR" "$ENV_COMPOSE"; then + echo "Editor failed (EDITOR=$EDITOR)" + exit 1 + fi + + do_update + do_check +} + +# Show help +show_help() { + cat << EOF +Usage: $SCRIPT [--quiet] [--generate] [--update] [--edit] [--check] [--file ] + + --generate Create/replace .env-compose from available compose files + --edit Open .env-compose in \$EDITOR, then apply to .env + --update Apply current .env-compose to .env + --check Validate compose config using \$DOCKER_CMD (default: docker) + --file Copy to .env-compose (-f also works) + --quiet Suppress normal output + + Finds all compose *.yml files under this directory. + Reads .env-compose for file selection (uncommented lines = enabled). + Writes resulting COMPOSE_FILE to .env + + .env-compose format: + docker-compose.yml # enabled + # docker-compose.postgres.yml # disabled (commented) +EOF + exit 0 +} + +# Main +QUIET=false +GENERATE=false +UPDATE=false +EDIT=false +CHECK=false +NEXT_FILE="" + +for arg in "$@"; do + if [[ "$NEXT_FILE" ]]; then + cp "$arg" "$ENV_COMPOSE" + echo "Copied $arg to $ENV_COMPOSE" + NEXT_FILE="" + UPDATE=true + CHECK=true + else + case "$arg" in + --quiet) QUIET=true ;; + --generate) GENERATE=true ;; + --update) UPDATE=true ;; + --edit) EDIT=true ;; + --check) CHECK=true ;; + --help|-h) show_help ;; + --file|-f) NEXT_FILE="yes" ;; + --file=*|-f=*) + src="${arg#--file=}" + src="${src#-f=}" + cp "$src" "$ENV_COMPOSE" + echo "Copied $src to $ENV_COMPOSE" + UPDATE=true + CHECK=true + ;; + *) echo "Unknown: $arg" ;; + esac + fi +done + +cd "$SCRIPT_DIR" >/dev/null 2>&1 + +# No args = help (unless FILE was set, which auto-sets UPDATE/CHECK) +if [[ "$GENERATE" == false && "$UPDATE" == false && "$EDIT" == false && "$CHECK" == false ]]; then + show_help fi -loud "(run '${SCRIPT} --help' for help)" +if [[ "$GENERATE" == true ]]; then + loud "Generating $ENV_COMPOSE..." + current=$(read_compose_file) + do_generate "$current" > "$ENV_COMPOSE" + loud "Generated $ENV_COMPOSE" +fi + +if [[ "$EDIT" == true ]]; then + do_edit +fi + +if [[ "$UPDATE" == true ]]; then + do_update +fi + +if [[ "$CHECK" == true ]]; then + do_check +fi