Skip to content
Open
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
Empty file removed compose.d/.gitkeep
Empty file.
1 change: 0 additions & 1 deletion compose.d/00-goclaw.yml

This file was deleted.

1 change: 0 additions & 1 deletion compose.options/11-postgres.yml

This file was deleted.

1 change: 0 additions & 1 deletion compose.options/12-selfservice.yml

This file was deleted.

1 change: 0 additions & 1 deletion compose.options/13-upgrade.yml

This file was deleted.

1 change: 0 additions & 1 deletion compose.options/14-browser.yml

This file was deleted.

1 change: 0 additions & 1 deletion compose.options/15-otel.yml

This file was deleted.

1 change: 0 additions & 1 deletion compose.options/16-redis.yml

This file was deleted.

1 change: 0 additions & 1 deletion compose.options/17-sandbox.yml

This file was deleted.

1 change: 0 additions & 1 deletion compose.options/18-tailscale.yml

This file was deleted.

299 changes: 249 additions & 50 deletions prepare-compose.sh
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 <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 <f> Copy <f> 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
Loading