#!/bin/bash set -e # Source diagnostic logger if available if [ -f "/usr/local/bin/diagnostic-logger.sh" ]; then . /usr/local/bin/diagnostic-logger.sh fi # Helper function to log messages log() { local timestamp=$(date '+%Y-%m-%d %H:%M:%S') echo "[${timestamp}] $*" >&2 } # Sanitize environment variables by removing invisible Unicode characters # This fixes the Portainer deployment issue where U+200E and similar characters # cause "unexpected character" errors in variable names sanitize_env_vars() { log "Sanitizing environment variables..." # Create a secure temporary file local temp_env temp_env=$(mktemp /tmp/sanitized_env.XXXXXX) # Export current environment to a file, then clean it export -p > "$temp_env" # Remove common invisible Unicode characters in a single sed command: # - U+200E (Left-to-Right Mark) - E2 80 8E # - U+200F (Right-to-Left Mark) - E2 80 8F # - U+200B (Zero Width Space) - E2 80 8B # - U+FEFF (Zero Width No-Break Space / BOM) - EF BB BF # - U+202A-202E (Directional formatting characters) sed -i \ -e 's/\xE2\x80\x8E//g' \ -e 's/\xE2\x80\x8F//g' \ -e 's/\xE2\x80\x8B//g' \ -e 's/\xEF\xBB\xBF//g' \ -e 's/\xE2\x80\xAA//g' \ -e 's/\xE2\x80\xAB//g' \ -e 's/\xE2\x80\xAC//g' \ -e 's/\xE2\x80\xAD//g' \ -e 's/\xE2\x80\xAE//g' \ "$temp_env" 2>/dev/null # Source the sanitized environment # If sourcing fails, log a warning but continue (environment may be partially set) if ! source "$temp_env" 2>/dev/null; then log "WARNING: Failed to source sanitized environment. Some variables may not be set correctly." fi # Clean up temporary file rm -f "$temp_env" log "Environment variables sanitized successfully" } # Sanitize environment variables on startup log "=== STARTUP INITIALIZATION ===" log "Container ID: $(hostname 2>/dev/null || 'unknown')" log "Timestamp: $(date '+%Y-%m-%d %H:%M:%S %Z')" # Initialize diagnostic logging only if file exists and we're in debug mode if [ -f "/usr/local/bin/diagnostic-logger.sh" ] && [ "${DEBUG_LOGGING:-false}" = "true" ]; then . /usr/local/bin/diagnostic-logger.sh log_system_info validate_environment rotate_logs fi sanitize_env_vars log "=== ENVIRONMENT SANITIZATION COMPLETE ===" # Repository configuration REPO_URL="${REPO_URL:-}" REPO_DIR="/home/web/data" REPO_BRANCH="${REPO_BRANCH:-main}" GITHUB_USERNAME="${GITHUB_USERNAME:-}" GITHUB_PAT="${GITHUB_PAT:-}" # Helper function to get authenticated repository URL get_auth_url() { local url="$1" if [ -n "$GITHUB_USERNAME" ] && [ -n "$GITHUB_PAT" ]; then # Extract the repository path from the URL local repo_path=$(echo "$url" | sed 's|https://github.com/||') echo "https://${GITHUB_USERNAME}:${GITHUB_PAT}@github.com/${repo_path}" else echo "$url" fi } log "Initializing Shopify AI App Builder..." # Log repository configuration details log "Repository Configuration:" log " URL: ${REPO_URL:-'NOT SET'}" log " Branch: ${REPO_BRANCH:-'NOT SET'}" log " Directory: ${REPO_DIR}" log " GitHub Username: ${GITHUB_USERNAME:-'NOT SET'}" log " GitHub PAT: ${GITHUB_PAT:+SET (hidden)}${GITHUB_PAT:-NOT SET}" # Only clone/pull if REPO_URL is set if [ -n "$REPO_URL" ]; then log "Repository URL: $REPO_URL" log "Repository directory: $REPO_DIR" log "Default branch: $REPO_BRANCH" # Check if authentication credentials are available if [ -n "$GITHUB_USERNAME" ] && [ -n "$GITHUB_PAT" ]; then log "GitHub authentication credentials found for user: $GITHUB_USERNAME" else log "WARNING: No GitHub authentication credentials found. Private repository access may fail." fi # Check if git is available if ! command -v git &> /dev/null; then log "ERROR: git is not available in the container" exit 1 fi # Check if the repository directory is empty or doesn't have .git if [ ! -d "$REPO_DIR/.git" ]; then # Directory doesn't exist or .git is missing - need to clone if [ -d "$REPO_DIR" ] && [ "$(ls -A "$REPO_DIR")" ]; then # Directory exists but is not a git repo - back it up log "WARNING: $REPO_DIR exists but is not a git repository. Backing up to ${REPO_DIR}.backup" mv "$REPO_DIR" "${REPO_DIR}.backup" fi log "Repository not found. Cloning $REPO_URL into $REPO_DIR..." auth_url=$(get_auth_url "$REPO_URL") git clone "$auth_url" "$REPO_DIR" cd "$REPO_DIR" log "Successfully cloned repository" else # Repository exists, pull latest changes cd "$REPO_DIR" log "Repository found at $REPO_DIR. Pulling latest changes from $REPO_BRANCH..." # Update remote URL to use authentication if credentials are available if [ -n "$GITHUB_USERNAME" ] && [ -n "$GITHUB_PAT" ]; then auth_url=$(get_auth_url "$REPO_URL") log "Updating remote URL with authentication credentials" git remote set-url origin "$auth_url" fi # Check if we're on a detached HEAD or have uncommitted changes if git diff --quiet && git diff --cached --quiet; then git fetch origin # Use plain git pull per policy (do not force origin/branch explicitly here) git pull || { log "WARNING: Failed to pull from $REPO_BRANCH, attempting to pull from any available branch" git pull || log "WARNING: Pull operation failed, continuing anyway" } log "Successfully pulled latest changes" else log "WARNING: Repository has uncommitted changes. Skipping pull to avoid conflicts." log "Run 'git status' to see changes, then 'git pull' manually if desired" fi fi log "Repository is ready at $REPO_DIR" else log "No REPO_URL set - starting with empty workspace" mkdir -p "$REPO_DIR" fi log "Starting Shopify AI App Builder service and ttyd..." # Use /opt/webchat directly as it contains the actual server.js with node_modules # /opt/webchat_v2 is just a wrapper and causes module loading failures CHAT_APP_DIR="${CHAT_APP_DIR:-/opt/webchat}" CHAT_APP_FALLBACK="/opt/webchat_v2" CHAT_PORT="${CHAT_PORT:-4000}" CHAT_HOST="${CHAT_HOST:-0.0.0.0}" ACCESS_PASSWORD="${ACCESS_PASSWORD:-}" # Persist opencode installation & user config # Move/Copy /root/.opencode to $REPO_DIR/.opencode if not already present # Then symlink /root/.opencode -> $REPO_DIR/.opencode so opencode state (connections, providers) # are persisted on container restarts while keeping the install accessible for runtime. PERSISTED_OPENCODE_DIR="$REPO_DIR/.opencode" OPENCODE_INSTALL_DIR="/root/.opencode" if [ -d "$OPENCODE_INSTALL_DIR" ]; then # If persisted dir does not exist, copy initial files so the app continues to work if [ ! -d "$PERSISTED_OPENCODE_DIR" ] || [ -z "$(ls -A $PERSISTED_OPENCODE_DIR 2>/dev/null)" ]; then log "Persisting opencode into $PERSISTED_OPENCODE_DIR" mkdir -p "$PERSISTED_OPENCODE_DIR" # Copy installed files to the persisted folder to preserve both the binary and state cp -a "$OPENCODE_INSTALL_DIR/." "$PERSISTED_OPENCODE_DIR/" || true chown -R root:root "$PERSISTED_OPENCODE_DIR" || true fi # Replace the install dir with a symlink to the persisted directory (only if not a symlink already) if [ -e "$OPENCODE_INSTALL_DIR" ] && [ ! -L "$OPENCODE_INSTALL_DIR" ]; then log "Symlinking $OPENCODE_INSTALL_DIR -> $PERSISTED_OPENCODE_DIR" rm -rf "$OPENCODE_INSTALL_DIR" ln -s "$PERSISTED_OPENCODE_DIR" "$OPENCODE_INSTALL_DIR" elif [ -L "$OPENCODE_INSTALL_DIR" ]; then log "$OPENCODE_INSTALL_DIR already symlinked; skipping" fi fi # Only ensure opencode command exists - qwen and gemini commands should not be aliased to opencode ensure_cli_wrappers() { local bin_dir="$PERSISTED_OPENCODE_DIR/bin" mkdir -p "$bin_dir" # Only create symlink for opencode command itself if [ ! -L "/usr/local/bin/opencode" ]; then ln -sf "$bin_dir/opencode" "/usr/local/bin/opencode" fi } ensure_cli_wrappers # Ensure a root-level opencode.json exists so OpenCode can discover the configured # Ollama/OpenAI-compatible model. This file lives in the persisted storage root # (/home/web/data) and is overwritten on every container start. ensure_root_opencode_config() { local config_path="$REPO_DIR/opencode.json" # Allow overrides while defaulting to the PluginCompass Ollama gateway + qwen3 model export OPENCODE_OLLAMA_BASE_URL="${OPENCODE_OLLAMA_BASE_URL:-https://ollama.plugincompass.com}" export OPENCODE_OLLAMA_MODEL="${OPENCODE_OLLAMA_MODEL:-qwen3:0.6b}" # Prefer an explicit Ollama key, fall back to OPENCODE_API_KEY (existing env var) export OPENCODE_OLLAMA_API_KEY="${OPENCODE_OLLAMA_API_KEY:-${OPENCODE_API_KEY:-}}" mkdir -p "$(dirname "$config_path")" log "Writing OpenCode config: ${config_path} (baseURL=${OPENCODE_OLLAMA_BASE_URL}, model=${OPENCODE_OLLAMA_MODEL})" python3 - <<'PY' > "$config_path" import json, os base_url = (os.environ.get("OPENCODE_OLLAMA_BASE_URL") or "https://ollama.plugincompass.com").rstrip("/") model_id = os.environ.get("OPENCODE_OLLAMA_MODEL") or "qwen3:0.6b" api_key = (os.environ.get("OPENCODE_OLLAMA_API_KEY") or "").strip() provider_name = os.environ.get("OPENCODE_OLLAMA_PROVIDER") or "ollama" provider_cfg = { "options": { "baseURL": base_url, }, "models": { model_id: { "id": model_id, "name": model_id, "tool_call": True, "temperature": True, } }, } if api_key: provider_cfg["options"]["apiKey"] = api_key cfg = { "$schema": "https://opencode.ai/config.json", "model": f"{provider_name}/{model_id}", "small_model": f"{provider_name}/{model_id}", "provider": { provider_name: provider_cfg, }, } print(json.dumps(cfg, indent=2)) PY chmod 600 "$config_path" 2>/dev/null || true } ensure_root_opencode_config # Set up signal handlers to properly clean up background processes cleanup() { local signal="$1" log "=== SIGNAL RECEIVED: ${signal} ===" log "Initiating graceful shutdown..." # Log final resource snapshot before shutdown if type monitor_resources &>/dev/null; then log "Final resource snapshot:" monitor_resources fi if [ -n "$MONITOR_PID" ] && kill -0 "$MONITOR_PID" 2>/dev/null; then log "Terminating monitor process (PID: $MONITOR_PID)" kill "$MONITOR_PID" 2>/dev/null || true wait "$MONITOR_PID" 2>/dev/null || true log "Monitor process terminated" fi if [ -n "$CHAT_PID" ] && kill -0 "$CHAT_PID" 2>/dev/null; then log "Terminating chat service (PID: $CHAT_PID) - giving it time for graceful shutdown" kill "$CHAT_PID" 2>/dev/null || true # Wait up to 25 seconds for graceful shutdown (Docker stop_grace_period is 30s) for i in $(seq 1 25); do if ! kill -0 "$CHAT_PID" 2>/dev/null; then log "Chat service terminated gracefully (${i} seconds)" if type diag_log &>/dev/null; then diag_log "INFO" "Chat service shutdown complete" fi exit 0 fi sleep 1 done log "WARNING: Chat service did not terminate gracefully, forcing exit" kill -9 "$CHAT_PID" 2>/dev/null || true fi log "=== SHUTDOWN COMPLETE ===" exit 0 } # Set up traps for common signals trap cleanup SIGTERM SIGINT SIGQUIT SIGHUP if [ -f "$CHAT_APP_DIR/server.js" ]; then log "Launching chat service on ${CHAT_HOST}:${CHAT_PORT} from $CHAT_APP_DIR" log "Environment: CHAT_PORT=${CHAT_PORT} CHAT_HOST=${CHAT_HOST} CHAT_DATA_ROOT=${REPO_DIR} CHAT_REPO_ROOT=${REPO_DIR}" CHAT_PORT=$CHAT_PORT CHAT_HOST=$CHAT_HOST CHAT_DATA_ROOT=$REPO_DIR CHAT_REPO_ROOT=$REPO_DIR node "$CHAT_APP_DIR/server.js" 2>&1 & CHAT_PID=$! log "Chat service started with PID: $CHAT_PID" elif [ -f "$CHAT_APP_FALLBACK/server.js" ]; then log "Primary chat service not found at $CHAT_APP_DIR, trying fallback at $CHAT_APP_FALLBACK" log "Launching chat service on ${CHAT_HOST}:${CHAT_PORT} from $CHAT_APP_FALLBACK" log "Environment: CHAT_PORT=${CHAT_PORT} CHAT_HOST=${CHAT_HOST} CHAT_DATA_ROOT=${REPO_DIR} CHAT_REPO_ROOT=${REPO_DIR}" CHAT_PORT=$CHAT_PORT CHAT_HOST=$CHAT_HOST CHAT_DATA_ROOT=$REPO_DIR CHAT_REPO_ROOT=$REPO_DIR node "$CHAT_APP_FALLBACK/server.js" 2>&1 & CHAT_PID=$! log "Chat service started with PID: $CHAT_PID" else log "ERROR: Chat service not found at $CHAT_APP_DIR or $CHAT_APP_FALLBACK; skipping startup" exit 1 fi # Log initial service status after startup sleep 2 if type check_service_status &>/dev/null; then check_service_status "chat" "$CHAT_PORT" "$CHAT_PID" fi # Monitor chat service health if [ -n "$CHAT_PID" ]; then ( # Initial check sleep 5 if type check_service_status &>/dev/null; then check_service_status "chat" "$CHAT_PORT" "$CHAT_PID" fi # Periodic monitoring check_count=0 while kill -0 "$CHAT_PID" 2>/dev/null; do sleep 30 check_count=$((check_count + 1)) # Every 2 minutes (4 checks), do resource monitoring if [ $((check_count % 4)) -eq 0 ] && type monitor_resources &>/dev/null; then monitor_resources fi # Every 5 minutes (10 checks), do service status check if [ $((check_count % 10)) -eq 0 ] && type check_service_status &>/dev/null; then check_service_status "chat" "$CHAT_PORT" "$CHAT_PID" fi done log "ERROR: Chat service (PID: $CHAT_PID) has exited unexpectedly" if type diag_log &>/dev/null; then diag_log "ERROR" "Chat service exited unexpectedly after ${check_count} checks" fi ) & MONITOR_PID=$! log "Health monitor started with PID: $MONITOR_PID" fi # Start ttyd proxy instead of ttyd directly (on-demand activation) log "Starting ttyd proxy (on-demand mode)" log "ttyd will only run when port 4001 is accessed" log "Idle timeout: 5 minutes of inactivity" exec node /usr/local/bin/ttyd-proxy.js