mirror of
https://github.com/Heretek-AI/heretek-openclaw-cli.git
synced 2026-07-01 19:54:16 -04:00
P6: Complete 8 initiatives - Agent files, deployment options, CLI, dashboards, plugins
P6-7: Agent File Completion (34 files - 11 agents × 3 files + guides) - Added BOOTSTRAP.md, IDENTITY.md, TOOLS.md for all 11 agents - Created AGENT_CREATION_GUIDE.md P6-2: Per-Agent Model Configuration (9 files) - Agent model router and config library - YAML configs for arbiter, coder agents - Configuration documentation P6-3: Health Check Dashboard (20+ files) - Complete frontend React application - API endpoints, WebSocket server - Collectors for agents, resources, services - Alert management and configuration P6-4: LiteLLM Observability Integration (10 files) - LiteLLM metrics collector and API - Frontend components for model/budget tracking - Integration documentation P6-1: Non-Docker Deployment (16 files) - Bare metal and VM deployment docs - Systemd service files - Installation scripts for Ubuntu/RHEL - Migration guide and troubleshooting P6-6: Cloud-Native Deployments (45+ files) - AWS, Azure, GCP Terraform configurations - Kubernetes base deployments with Kustomize overlays - Cloud deployment documentation P6-5: Unified Deployment CLI (28 files) - Complete CLI with 12 commands - Deployers for Docker, Kubernetes, cloud, baremetal - Health checker, backup manager, config manager P6-8: Plugin Installation Guide (15 files) - Plugin development and installation guides - Plugin CLI documentation and registry - Templates for basic, skill, and tool plugins
This commit is contained in:
@@ -0,0 +1,434 @@
|
||||
# ==============================================================================
|
||||
# Heretek OpenClaw - Bare Metal Environment Configuration v2.0
|
||||
# ==============================================================================
|
||||
# Copy this file to /etc/openclaw/.env and update with your values
|
||||
# Usage: cp .env.bare-metal.example /etc/openclaw/.env
|
||||
#
|
||||
# Configuration: Bare Metal Deployment (Non-Docker)
|
||||
# All services run on localhost with direct system access
|
||||
#
|
||||
# Generated: 2026-03-31
|
||||
# ==============================================================================
|
||||
|
||||
# ==============================================================================
|
||||
# LITEELM GATEWAY CONFIGURATION
|
||||
# ==============================================================================
|
||||
|
||||
# LiteLLM Master Key (REQUIRED - change in production!)
|
||||
# Generate with: openssl rand -hex 32
|
||||
LITELLM_MASTER_KEY=heretek-master-key-change-me
|
||||
|
||||
# LiteLLM Salt Key (used for encryption)
|
||||
# Generate with: openssl rand -hex 32
|
||||
LITELLM_SALT_KEY=heretek-salt-change-me
|
||||
|
||||
# LiteLLM Port
|
||||
LITELLM_PORT=4000
|
||||
|
||||
# LiteLLM UI Credentials
|
||||
LITELLM_UI_USERNAME=admin
|
||||
LITELLM_UI_PASSWORD=heretek-admin-change-me
|
||||
|
||||
# LiteLLM Host (for external access)
|
||||
LITELLM_HOST=http://localhost:4000
|
||||
|
||||
# ==============================================================================
|
||||
# PROVIDER API KEYS
|
||||
# ==============================================================================
|
||||
# See docs/configuration/PROVIDER_SETUP.md for detailed setup instructions
|
||||
# See config/providers/ for pre-configured provider templates
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# MiniMax API (PRIMARY - All Agents Default)
|
||||
# ------------------------------------------------------------------------------
|
||||
# Get your key from: https://platform.minimaxi.ai
|
||||
MINIMAX_API_KEY=your-minimax-key-here
|
||||
MINIMAX_API_BASE=https://api.minimaxi.chat/v1
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# z.ai Coding API (FAILOVER - GLM-5)
|
||||
# ------------------------------------------------------------------------------
|
||||
# Endpoint: https://api.z.ai/api/coding/paas/v4
|
||||
ZAI_API_KEY=your-zai-key-here
|
||||
ZAI_API_BASE=https://api.z.ai/api/coding/paas/v4
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# OpenAI API (OPTIONAL)
|
||||
# ------------------------------------------------------------------------------
|
||||
# Get your key from: https://platform.openai.com/api-keys
|
||||
# Models: GPT-4, GPT-4-Turbo, GPT-3.5-Turbo, o1
|
||||
OPENAI_API_KEY=sk-your-openai-key-here
|
||||
OPENAI_API_BASE=https://api.openai.com/v1
|
||||
OPENAI_ORGANIZATION=
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Anthropic API (OPTIONAL)
|
||||
# ------------------------------------------------------------------------------
|
||||
# Get your key from: https://console.anthropic.com/
|
||||
# Models: Claude-3-Opus, Claude-3-Sonnet, Claude-3-Haiku, Claude-3.5-Sonnet
|
||||
ANTHROPIC_API_KEY=sk-ant-your-anthropic-key-here
|
||||
ANTHROPIC_API_BASE=https://api.anthropic.com
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Google API (OPTIONAL)
|
||||
# ------------------------------------------------------------------------------
|
||||
# Get your key from: https://makersuite.google.com/app/apikey
|
||||
# Models: Gemini-Pro, Gemini-Ultra, Gemini-Flash
|
||||
GOOGLE_API_KEY=your-google-api-key-here
|
||||
GOOGLE_VERTEX_PROJECT_ID=your-gcp-project-id
|
||||
GOOGLE_VERTEX_LOCATION=us-central1
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Azure OpenAI API (OPTIONAL)
|
||||
# ------------------------------------------------------------------------------
|
||||
# Create resource at: https://portal.azure.com
|
||||
# Models: Azure-hosted GPT-4, GPT-35-Turbo
|
||||
AZURE_API_KEY=your-azure-openai-key-here
|
||||
AZURE_API_BASE=https://your-resource.openai.azure.com/
|
||||
AZURE_API_VERSION=2024-02-15-preview
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# xAI API (OPTIONAL)
|
||||
# ------------------------------------------------------------------------------
|
||||
# Get your key from: https://console.x.ai/
|
||||
# Models: Grok-Beta, Grok-Vision, Grok-2
|
||||
XAI_API_KEY=your-xai-key-here
|
||||
XAI_API_BASE=https://api.x.ai
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Ollama (Local Models - No API key required)
|
||||
# ------------------------------------------------------------------------------
|
||||
OLLAMA_API_KEY=not-required
|
||||
OLLAMA_HOST=http://localhost:11434
|
||||
|
||||
# ==============================================================================
|
||||
# DATABASE CONFIGURATION (PostgreSQL)
|
||||
# ==============================================================================
|
||||
# PostgreSQL runs on localhost for bare metal deployment
|
||||
# pgvector extension required for vector embeddings
|
||||
# ==============================================================================
|
||||
|
||||
POSTGRES_USER=openclaw
|
||||
POSTGRES_PASSWORD=heretek-secure-password-change-me
|
||||
POSTGRES_DB=openclaw
|
||||
POSTGRES_HOST=localhost
|
||||
POSTGRES_PORT=5432
|
||||
DATABASE_URL=postgresql://openclaw:heretek-secure-password-change-me@localhost:5432/openclaw
|
||||
|
||||
# PostgreSQL connection pool settings
|
||||
DATABASE_POOL_SIZE=10
|
||||
DATABASE_MAX_OVERFLOW=20
|
||||
DATABASE_POOL_TIMEOUT=30
|
||||
|
||||
# ==============================================================================
|
||||
# REDIS CONFIGURATION
|
||||
# ==============================================================================
|
||||
# Redis runs on localhost for bare metal deployment
|
||||
# Used for caching, rate limiting, and session storage
|
||||
# ==============================================================================
|
||||
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_URL=redis://localhost:6379/0
|
||||
# If password is enabled:
|
||||
# REDIS_URL=redis://:your-redis-password@localhost:6379/0
|
||||
|
||||
# Redis connection settings
|
||||
REDIS_DB=0
|
||||
REDIS_PASSWORD=
|
||||
REDIS_SSL=false
|
||||
|
||||
# ==============================================================================
|
||||
# OLLAMA CONFIGURATION (Local LLM Runtime)
|
||||
# ==============================================================================
|
||||
# Ollama runs on localhost for bare metal deployment
|
||||
# Supports AMD ROCm and NVIDIA CUDA GPUs
|
||||
# ==============================================================================
|
||||
|
||||
# GPU Mode: cpu, amd, nvidia
|
||||
OLLAMA_GPU_MODE=cpu
|
||||
|
||||
# Ollama host binding
|
||||
OLLAMA_HOST_BINDING=127.0.0.1
|
||||
OLLAMA_PORT=11434
|
||||
|
||||
# Embedding model (nomic-embed-text-v2-moe recommended for 768 dimensions)
|
||||
OLLAMA_EMBEDDING_MODEL=nomic-embed-text-v2-moe
|
||||
|
||||
# Pre-pull models on startup (comma-separated)
|
||||
# These models will be pulled when Ollama starts
|
||||
OLLAMA_MODELS=nomic-embed-text-v2-moe,qwen3-embedding:8b
|
||||
|
||||
# AMD ROCm Settings (if using AMD GPU)
|
||||
# HSA_OVERRIDE_GFX_VERSION=10.3.0
|
||||
|
||||
# NVIDIA CUDA Settings (if using NVIDIA GPU)
|
||||
# CUDA_VISIBLE_DEVICES=0
|
||||
|
||||
# ==============================================================================
|
||||
# AGENT MODEL ASSIGNMENTS
|
||||
# ==============================================================================
|
||||
# These are virtual model names in LiteLLM. Each agent uses its passthrough
|
||||
# endpoint (agent/steward, agent/alpha, etc.) which defaults to minimax/M2.7
|
||||
# Users can reassign models via LiteLLM WebUI without changing this file.
|
||||
# ==============================================================================
|
||||
|
||||
# Default model for all agent passthrough endpoints
|
||||
DEFAULT_AGENT_MODEL=minimax/MiniMax-M2.7
|
||||
|
||||
# Failover model when primary is unavailable
|
||||
FAILOVER_AGENT_MODEL=zai/glm-5-1
|
||||
|
||||
# Individual agent model overrides (optional - leave empty to use default)
|
||||
# Uncomment and set to override the default model for specific agents
|
||||
# AGENT_STEWARD_MODEL=minimax/MiniMax-M2.7
|
||||
# AGENT_ALPHA_MODEL=minimax/MiniMax-M2.7
|
||||
# AGENT_BETA_MODEL=minimax/MiniMax-M2.7
|
||||
# AGENT_CHARLIE_MODEL=minimax/MiniMax-M2.7
|
||||
# AGENT_EXAMINER_MODEL=minimax/MiniMax-M2.7
|
||||
# AGENT_EXPLORER_MODEL=minimax/MiniMax-M2.7
|
||||
# AGENT_SENTINEL_MODEL=minimax/MiniMax-M2.7
|
||||
# AGENT_CODER_MODEL=zai/glm-5-1
|
||||
# AGENT_DREAMER_MODEL=minimax/MiniMax-M2.7
|
||||
# AGENT_EMPATH_MODEL=minimax/MiniMax-M2.7
|
||||
# AGENT_HISTORIAN_MODEL=minimax/MiniMax-M2.7
|
||||
|
||||
# ==============================================================================
|
||||
# LITEELM A2A AGENT CONFIGURATION
|
||||
# ==============================================================================
|
||||
|
||||
# Current agent name (steward, alpha, beta, charlie, examiner, explorer, sentinel, coder, dreamer, empath, historian)
|
||||
AGENT_NAME=steward
|
||||
|
||||
# Agent configuration JSON
|
||||
# Each agent has: role, session (unique workspace identifier), port
|
||||
AGENTS='{
|
||||
"steward": {
|
||||
"role": "orchestrator",
|
||||
"session": "agent:heretek:steward",
|
||||
"port": 8001
|
||||
},
|
||||
"alpha": {
|
||||
"role": "triad",
|
||||
"session": "agent:heretek:alpha",
|
||||
"port": 8002
|
||||
},
|
||||
"beta": {
|
||||
"role": "triad",
|
||||
"session": "agent:heretek:beta",
|
||||
"port": 8003
|
||||
},
|
||||
"charlie": {
|
||||
"role": "triad",
|
||||
"session": "agent:heretek:charlie",
|
||||
"port": 8004
|
||||
},
|
||||
"examiner": {
|
||||
"role": "interrogator",
|
||||
"session": "agent:heretek:examiner",
|
||||
"port": 8005
|
||||
},
|
||||
"explorer": {
|
||||
"role": "scout",
|
||||
"session": "agent:heretek:explorer",
|
||||
"port": 8006
|
||||
},
|
||||
"sentinel": {
|
||||
"role": "guardian",
|
||||
"session": "agent:heretek:sentinel",
|
||||
"port": 8007
|
||||
},
|
||||
"coder": {
|
||||
"role": "artisan",
|
||||
"session": "agent:heretek:coder",
|
||||
"port": 8008
|
||||
},
|
||||
"dreamer": {
|
||||
"role": "visionary",
|
||||
"session": "agent:heretek:dreamer",
|
||||
"port": 8009
|
||||
},
|
||||
"empath": {
|
||||
"role": "diplomat",
|
||||
"session": "agent:heretek:empath",
|
||||
"port": 8010
|
||||
},
|
||||
"historian": {
|
||||
"role": "archivist",
|
||||
"session": "agent:heretek:historian",
|
||||
"port": 8011
|
||||
}
|
||||
}'
|
||||
|
||||
# ==============================================================================
|
||||
# OPENCLAW SPECIFIC SETTINGS
|
||||
# ==============================================================================
|
||||
|
||||
# OpenClaw data directory
|
||||
OPENCLAW_DATA_DIR=/root/.openclaw/data
|
||||
|
||||
# OpenClaw workspace directory (agent workspaces)
|
||||
OPENCLAW_WORKSPACE=/root/.openclaw/agents
|
||||
|
||||
# OpenClaw logs directory
|
||||
OPENCLAW_LOG_DIR=/var/log/openclaw
|
||||
|
||||
# Collective memory directory
|
||||
COLLECTIVE_MEMORY_DIR=/root/.openclaw/memory
|
||||
|
||||
# Skills directory
|
||||
SKILLS_DIR=/root/heretek/heretek-openclaw/skills
|
||||
|
||||
# Plugins directory
|
||||
PLUGINS_DIR=/root/heretek/heretek-openclaw/plugins
|
||||
|
||||
# ==============================================================================
|
||||
# RATE LIMITING & CACHING
|
||||
# ==============================================================================
|
||||
|
||||
# Rate limit settings (requests per minute)
|
||||
RATE_LIMIT_ENABLED=true
|
||||
RATE_LIMIT_REQUESTS_PER_MINUTE=60
|
||||
|
||||
# Cache settings
|
||||
CACHE_ENABLED=true
|
||||
CACHE_TTL_SECONDS=3600
|
||||
|
||||
# ==============================================================================
|
||||
# LOGGING & MONITORING
|
||||
# ==============================================================================
|
||||
|
||||
# Log level: DEBUG, INFO, WARNING, ERROR
|
||||
LOG_LEVEL=INFO
|
||||
|
||||
# Enable detailed request logging
|
||||
LITELLM_REQUEST_LOGGING=true
|
||||
|
||||
# Cost tracking
|
||||
LITELLM_COST_TRACKING_ENABLED=true
|
||||
|
||||
# Performance metrics
|
||||
LITELLM_METRICS_ENABLED=true
|
||||
|
||||
# ==============================================================================
|
||||
# A2A PROTOCOL SETTINGS
|
||||
# ==============================================================================
|
||||
|
||||
# A2A streaming support
|
||||
LITELLM_STREAMING_ENABLED=true
|
||||
|
||||
# Agent discovery
|
||||
LITELLM_AGENT_DISCOVERY_ENABLED=true
|
||||
|
||||
# Task handoff timeout (seconds)
|
||||
A2A_TASK_HANDOFF_TIMEOUT=60
|
||||
|
||||
# Agent heartbeat interval (seconds)
|
||||
A2A_HEARTBEAT_INTERVAL=30
|
||||
|
||||
# ==============================================================================
|
||||
# WEBSOCKET CONFIGURATION
|
||||
# ==============================================================================
|
||||
|
||||
# WebSocket URL for real-time A2A message streaming
|
||||
VITE_WS_URL=ws://localhost:18789
|
||||
WS_PORT=18789
|
||||
|
||||
# ==============================================================================
|
||||
# FAILOVER CONFIGURATION
|
||||
# ==============================================================================
|
||||
|
||||
# Priority-based fallback enabled
|
||||
LITELLM_PRIORITY_FALLBACK_ENABLED=true
|
||||
|
||||
# Health check enabled
|
||||
LITELLM_HEALTH_CHECK_ENABLED=true
|
||||
|
||||
# Health check interval (seconds)
|
||||
LITELLM_HEALTH_CHECK_INTERVAL=30
|
||||
|
||||
# Unhealthy threshold before fallback
|
||||
LITELLM_UNHEALTHY_THRESHOLD=2
|
||||
|
||||
# ==============================================================================
|
||||
# OBSERVABILITY - LANGFUSE & OPENTELEMETRY
|
||||
# ==============================================================================
|
||||
|
||||
# LangFuse Configuration
|
||||
# Get your keys from: https://cloud.langfuse.com
|
||||
LANGFUSE_ENABLED=false
|
||||
LANGFUSE_PUBLIC_KEY=pk-lf-your-public-key-here
|
||||
LANGFUSE_SECRET_KEY=sk-lf-your-secret-key-here
|
||||
LANGFUSE_HOST=https://cloud.langfuse.com
|
||||
|
||||
# OpenTelemetry Configuration
|
||||
OTEL_ENABLED=false
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/traces
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
|
||||
OTEL_EXPORTER_TYPE=console
|
||||
OTEL_SERVICE_NAME=heretek-openclaw
|
||||
|
||||
# ==============================================================================
|
||||
# SECURITY
|
||||
# ==============================================================================
|
||||
|
||||
# CORS allowed origins (comma-separated)
|
||||
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173
|
||||
|
||||
# Admin emails for alerts
|
||||
# ADMIN_EMAILS=admin@heretek.local
|
||||
|
||||
# API rate limiting
|
||||
API_RATE_LIMIT_ENABLED=true
|
||||
API_RATE_LIMIT_REQUESTS_PER_MINUTE=100
|
||||
|
||||
# ==============================================================================
|
||||
# BACKUP & RECOVERY
|
||||
# ==============================================================================
|
||||
|
||||
# Enable automatic backup
|
||||
AUTO_BACKUP_ENABLED=true
|
||||
|
||||
# Backup interval (hours)
|
||||
BACKUP_INTERVAL_HOURS=24
|
||||
|
||||
# Backup retention (days)
|
||||
BACKUP_RETENTION_DAYS=7
|
||||
|
||||
# Backup directory
|
||||
BACKUP_DIR=/var/backups/openclaw
|
||||
|
||||
# ==============================================================================
|
||||
# SYSTEM PATHS (Bare Metal Specific)
|
||||
# ==============================================================================
|
||||
|
||||
# PostgreSQL data directory
|
||||
POSTGRES_DATA_DIR=/var/lib/postgresql/15/main
|
||||
|
||||
# Redis data directory
|
||||
REDIS_DATA_DIR=/var/lib/redis
|
||||
|
||||
# Ollama models directory
|
||||
OLLAMA_DATA_DIR=/var/lib/ollama
|
||||
|
||||
# LiteLLM config directory
|
||||
LITELLM_CONFIG_DIR=/etc/litellm
|
||||
|
||||
# OpenClaw config directory
|
||||
OPENCLAW_CONFIG_DIR=/etc/openclaw
|
||||
|
||||
# ==============================================================================
|
||||
# SERVICE MANAGEMENT
|
||||
# ==============================================================================
|
||||
|
||||
# Systemd service names (for monitoring and restart scripts)
|
||||
POSTGRES_SERVICE_NAME=postgresql
|
||||
REDIS_SERVICE_NAME=redis
|
||||
OLLAMA_SERVICE_NAME=ollama
|
||||
LITELLM_SERVICE_NAME=litellm
|
||||
OPENCLAW_SERVICE_NAME=openclaw-gateway
|
||||
|
||||
# ==============================================================================
|
||||
# END OF ENVIRONMENT CONFIGURATION
|
||||
# ==============================================================================
|
||||
+379
@@ -0,0 +1,379 @@
|
||||
# ==============================================================================
|
||||
# Heretek OpenClaw - VM Environment Configuration v2.0
|
||||
# ==============================================================================
|
||||
# Copy this file to /etc/openclaw/.env and update with your values
|
||||
# Usage: cp .env.vm.example /etc/openclaw/.env
|
||||
#
|
||||
# Configuration: VM Deployment (AWS EC2, GCP Compute, Azure VM, etc.)
|
||||
# Optimized for cloud VM environments with security group considerations
|
||||
#
|
||||
# Generated: 2026-03-31
|
||||
# ==============================================================================
|
||||
|
||||
# ==============================================================================
|
||||
# LITEELM GATEWAY CONFIGURATION
|
||||
# ==============================================================================
|
||||
|
||||
# LiteLLM Master Key (REQUIRED - change in production!)
|
||||
# Generate with: openssl rand -hex 32
|
||||
LITELLM_MASTER_KEY=heretek-master-key-change-me
|
||||
|
||||
# LiteLLM Salt Key (used for encryption)
|
||||
# Generate with: openssl rand -hex 32
|
||||
LITELLM_SALT_KEY=heretek-salt-change-me
|
||||
|
||||
# LiteLLM Port (bind to 0.0.0.0 for external access)
|
||||
LITELLM_PORT=4000
|
||||
LITELLM_HOST=0.0.0.0
|
||||
|
||||
# LiteLLM UI Credentials
|
||||
LITELLM_UI_USERNAME=admin
|
||||
LITELLM_UI_PASSWORD=heretek-admin-change-me
|
||||
|
||||
# External URL for VM access (update with your VM's public IP or domain)
|
||||
LITELLM_EXTERNAL_URL=http://YOUR_VM_IP:4000
|
||||
|
||||
# ==============================================================================
|
||||
# PROVIDER API KEYS
|
||||
# ==============================================================================
|
||||
# See docs/configuration/PROVIDER_SETUP.md for detailed setup instructions
|
||||
# See config/providers/ for pre-configured provider templates
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# MiniMax API (PRIMARY - All Agents Default)
|
||||
# ------------------------------------------------------------------------------
|
||||
# Get your key from: https://platform.minimaxi.ai
|
||||
MINIMAX_API_KEY=your-minimax-key-here
|
||||
MINIMAX_API_BASE=https://api.minimaxi.chat/v1
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# z.ai Coding API (FAILOVER - GLM-5)
|
||||
# ------------------------------------------------------------------------------
|
||||
# Endpoint: https://api.z.ai/api/coding/paas/v4
|
||||
ZAI_API_KEY=your-zai-key-here
|
||||
ZAI_API_BASE=https://api.z.ai/api/coding/paas/v4
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# OpenAI API (OPTIONAL)
|
||||
# ------------------------------------------------------------------------------
|
||||
# Get your key from: https://platform.openai.com/api-keys
|
||||
OPENAI_API_KEY=sk-your-openai-key-here
|
||||
OPENAI_API_BASE=https://api.openai.com/v1
|
||||
OPENAI_ORGANIZATION=
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Anthropic API (OPTIONAL)
|
||||
# ------------------------------------------------------------------------------
|
||||
# Get your key from: https://console.anthropic.com/
|
||||
ANTHROPIC_API_KEY=sk-ant-your-anthropic-key-here
|
||||
ANTHROPIC_API_BASE=https://api.anthropic.com
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Google API (OPTIONAL)
|
||||
# ------------------------------------------------------------------------------
|
||||
# Get your key from: https://makersuite.google.com/app/apikey
|
||||
GOOGLE_API_KEY=your-google-api-key-here
|
||||
GOOGLE_VERTEX_PROJECT_ID=your-gcp-project-id
|
||||
GOOGLE_VERTEX_LOCATION=us-central1
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Azure OpenAI API (OPTIONAL)
|
||||
# ------------------------------------------------------------------------------
|
||||
# Create resource at: https://portal.azure.com
|
||||
AZURE_API_KEY=your-azure-openai-key-here
|
||||
AZURE_API_BASE=https://your-resource.openai.azure.com/
|
||||
AZURE_API_VERSION=2024-02-15-preview
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# xAI API (OPTIONAL)
|
||||
# ------------------------------------------------------------------------------
|
||||
# Get your key from: https://console.x.ai/
|
||||
XAI_API_KEY=your-xai-key-here
|
||||
XAI_API_BASE=https://api.x.ai
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Ollama (Local Models - No API key required)
|
||||
# ------------------------------------------------------------------------------
|
||||
OLLAMA_API_KEY=not-required
|
||||
OLLAMA_HOST=http://localhost:11434
|
||||
|
||||
# ==============================================================================
|
||||
# DATABASE CONFIGURATION (PostgreSQL)
|
||||
# ==============================================================================
|
||||
# PostgreSQL runs on localhost for VM deployment
|
||||
# Bind to localhost only for security (use SSH tunnel for remote access)
|
||||
# pgvector extension required for vector embeddings
|
||||
# ==============================================================================
|
||||
|
||||
POSTGRES_USER=openclaw
|
||||
POSTGRES_PASSWORD=heretek-secure-password-change-me
|
||||
POSTGRES_DB=openclaw
|
||||
POSTGRES_HOST=localhost
|
||||
POSTGRES_PORT=5432
|
||||
DATABASE_URL=postgresql://openclaw:heretek-secure-password-change-me@localhost:5432/openclaw
|
||||
|
||||
# PostgreSQL connection pool settings (adjusted for VM resources)
|
||||
DATABASE_POOL_SIZE=5
|
||||
DATABASE_MAX_OVERFLOW=10
|
||||
DATABASE_POOL_TIMEOUT=30
|
||||
|
||||
# ==============================================================================
|
||||
# REDIS CONFIGURATION
|
||||
# ==============================================================================
|
||||
# Redis runs on localhost for VM deployment
|
||||
# Bind to localhost only for security
|
||||
# ==============================================================================
|
||||
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_URL=redis://localhost:6379/0
|
||||
# If password is enabled (recommended for VM):
|
||||
# REDIS_URL=redis://:your-redis-password@localhost:6379/0
|
||||
|
||||
# Redis connection settings
|
||||
REDIS_DB=0
|
||||
REDIS_PASSWORD=
|
||||
REDIS_SSL=false
|
||||
|
||||
# ==============================================================================
|
||||
# OLLAMA CONFIGURATION (Local LLM Runtime)
|
||||
# ==============================================================================
|
||||
# Ollama runs on localhost for VM deployment
|
||||
# GPU support depends on VM instance type
|
||||
# ==============================================================================
|
||||
|
||||
# GPU Mode: cpu, amd, nvidia, auto
|
||||
# For GPU-enabled VMs, set appropriately:
|
||||
# - AWS g5 instances: nvidia
|
||||
# - GCP g2 instances: nvidia
|
||||
# - Azure NC series: nvidia
|
||||
OLLAMA_GPU_MODE=auto
|
||||
|
||||
# Ollama host binding (localhost for security)
|
||||
OLLAMA_HOST_BINDING=127.0.0.1
|
||||
OLLAMA_PORT=11434
|
||||
|
||||
# Embedding model
|
||||
OLLAMA_EMBEDDING_MODEL=nomic-embed-text-v2-moe
|
||||
|
||||
# Pre-pull models on startup
|
||||
OLLAMA_MODELS=nomic-embed-text-v2-moe
|
||||
|
||||
# AMD ROCm Settings (for AMD GPU VMs)
|
||||
# HSA_OVERRIDE_GFX_VERSION=10.3.0
|
||||
|
||||
# NVIDIA CUDA Settings (for NVIDIA GPU VMs)
|
||||
# CUDA_VISIBLE_DEVICES=0
|
||||
|
||||
# ==============================================================================
|
||||
# AGENT MODEL ASSIGNMENTS
|
||||
# ==============================================================================
|
||||
|
||||
# Default model for all agent passthrough endpoints
|
||||
DEFAULT_AGENT_MODEL=minimax/MiniMax-M2.7
|
||||
|
||||
# Failover model when primary is unavailable
|
||||
FAILOVER_AGENT_MODEL=zai/glm-5-1
|
||||
|
||||
# Individual agent model overrides (optional)
|
||||
# AGENT_CODER_MODEL=zai/glm-5-1
|
||||
|
||||
# ==============================================================================
|
||||
# LITEELM A2A AGENT CONFIGURATION
|
||||
# ==============================================================================
|
||||
|
||||
# Current agent name
|
||||
AGENT_NAME=steward
|
||||
|
||||
# Agent configuration JSON
|
||||
AGENTS='{
|
||||
"steward": {"role": "orchestrator", "session": "agent:heretek:steward", "port": 8001},
|
||||
"alpha": {"role": "triad", "session": "agent:heretek:alpha", "port": 8002},
|
||||
"beta": {"role": "triad", "session": "agent:heretek:beta", "port": 8003},
|
||||
"charlie": {"role": "triad", "session": "agent:heretek:charlie", "port": 8004},
|
||||
"examiner": {"role": "interrogator", "session": "agent:heretek:examiner", "port": 8005},
|
||||
"explorer": {"role": "scout", "session": "agent:heretek:explorer", "port": 8006},
|
||||
"sentinel": {"role": "guardian", "session": "agent:heretek:sentinel", "port": 8007},
|
||||
"coder": {"role": "artisan", "session": "agent:heretek:coder", "port": 8008},
|
||||
"dreamer": {"role": "visionary", "session": "agent:heretek:dreamer", "port": 8009},
|
||||
"empath": {"role": "diplomat", "session": "agent:heretek:empath", "port": 8010},
|
||||
"historian": {"role": "archivist", "session": "agent:heretek:historian", "port": 8011}
|
||||
}'
|
||||
|
||||
# ==============================================================================
|
||||
# OPENCLAW SPECIFIC SETTINGS
|
||||
# ==============================================================================
|
||||
|
||||
# OpenClaw directories
|
||||
OPENCLAW_DATA_DIR=/root/.openclaw/data
|
||||
OPENCLAW_WORKSPACE=/root/.openclaw/agents
|
||||
OPENCLAW_LOG_DIR=/var/log/openclaw
|
||||
COLLECTIVE_MEMORY_DIR=/root/.openclaw/memory
|
||||
SKILLS_DIR=/root/heretek/heretek-openclaw/skills
|
||||
PLUGINS_DIR=/root/heretek/heretek-openclaw/plugins
|
||||
|
||||
# ==============================================================================
|
||||
# RATE LIMITING & CACHING
|
||||
# ==============================================================================
|
||||
|
||||
# Rate limit settings (adjusted for VM deployment)
|
||||
RATE_LIMIT_ENABLED=true
|
||||
RATE_LIMIT_REQUESTS_PER_MINUTE=60
|
||||
|
||||
# Cache settings
|
||||
CACHE_ENABLED=true
|
||||
CACHE_TTL_SECONDS=3600
|
||||
|
||||
# ==============================================================================
|
||||
# LOGGING & MONITORING
|
||||
# ==============================================================================
|
||||
|
||||
# Log level
|
||||
LOG_LEVEL=INFO
|
||||
|
||||
# Enable detailed request logging
|
||||
LITELLM_REQUEST_LOGGING=true
|
||||
|
||||
# Cost tracking
|
||||
LITELLM_COST_TRACKING_ENABLED=true
|
||||
|
||||
# Performance metrics
|
||||
LITELLM_METRICS_ENABLED=true
|
||||
|
||||
# ==============================================================================
|
||||
# A2A PROTOCOL SETTINGS
|
||||
# ==============================================================================
|
||||
|
||||
LITELLM_STREAMING_ENABLED=true
|
||||
LITELLM_AGENT_DISCOVERY_ENABLED=true
|
||||
A2A_TASK_HANDOFF_TIMEOUT=60
|
||||
A2A_HEARTBEAT_INTERVAL=30
|
||||
|
||||
# ==============================================================================
|
||||
# WEBSOCKET CONFIGURATION
|
||||
# ==============================================================================
|
||||
|
||||
# WebSocket URL for external access (update with your VM's public IP)
|
||||
VITE_WS_URL=ws://YOUR_VM_IP:18789
|
||||
WS_PORT=18789
|
||||
|
||||
# ==============================================================================
|
||||
# FAILOVER CONFIGURATION
|
||||
# ==============================================================================
|
||||
|
||||
LITELLM_PRIORITY_FALLBACK_ENABLED=true
|
||||
LITELLM_HEALTH_CHECK_ENABLED=true
|
||||
LITELLM_HEALTH_CHECK_INTERVAL=30
|
||||
LITELLM_UNHEALTHY_THRESHOLD=2
|
||||
|
||||
# ==============================================================================
|
||||
# OBSERVABILITY - LANGFUSE & OPENTELEMETRY
|
||||
# ==============================================================================
|
||||
|
||||
# LangFuse Configuration
|
||||
LANGFUSE_ENABLED=false
|
||||
LANGFUSE_PUBLIC_KEY=pk-lf-your-public-key-here
|
||||
LANGFUSE_SECRET_KEY=sk-lf-your-secret-key-here
|
||||
LANGFUSE_HOST=https://cloud.langfuse.com
|
||||
|
||||
# OpenTelemetry Configuration
|
||||
OTEL_ENABLED=false
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/traces
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
|
||||
OTEL_EXPORTER_TYPE=console
|
||||
OTEL_SERVICE_NAME=heretek-openclaw
|
||||
|
||||
# ==============================================================================
|
||||
# SECURITY (VM-Specific)
|
||||
# ==============================================================================
|
||||
|
||||
# CORS allowed origins (update with your VM's public IP or domain)
|
||||
CORS_ALLOWED_ORIGINS=http://YOUR_VM_IP:3000,http://YOUR_VM_IP:5173
|
||||
|
||||
# Admin emails for alerts
|
||||
# ADMIN_EMAILS=admin@heretek.local
|
||||
|
||||
# API rate limiting (stricter for public VMs)
|
||||
API_RATE_LIMIT_ENABLED=true
|
||||
API_RATE_LIMIT_REQUESTS_PER_MINUTE=100
|
||||
|
||||
# Bind addresses (localhost for internal services)
|
||||
POSTGRES_BIND_ADDRESS=127.0.0.1
|
||||
REDIS_BIND_ADDRESS=127.0.0.1
|
||||
OLLAMA_BIND_ADDRESS=127.0.0.1
|
||||
|
||||
# Public bind addresses (for external access)
|
||||
LITELLM_BIND_ADDRESS=0.0.0.0
|
||||
OPENCLAW_BIND_ADDRESS=0.0.0.0
|
||||
|
||||
# ==============================================================================
|
||||
# BACKUP & RECOVERY (VM-Specific)
|
||||
# ==============================================================================
|
||||
|
||||
# Enable automatic backup
|
||||
AUTO_BACKUP_ENABLED=true
|
||||
|
||||
# Backup interval (hours)
|
||||
BACKUP_INTERVAL_HOURS=24
|
||||
|
||||
# Backup retention (days)
|
||||
BACKUP_RETENTION_DAYS=7
|
||||
|
||||
# Backup directory
|
||||
BACKUP_DIR=/var/backups/openclaw
|
||||
|
||||
# Cloud backup integration (optional)
|
||||
# AWS S3
|
||||
# AWS_BACKUP_BUCKET=your-backup-bucket
|
||||
# AWS_BACKUP_REGION=us-east-1
|
||||
# AWS_ACCESS_KEY_ID=your-aws-key
|
||||
# AWS_SECRET_ACCESS_KEY=your-aws-secret
|
||||
|
||||
# GCP Cloud Storage
|
||||
# GCP_BACKUP_BUCKET=your-backup-bucket
|
||||
# GCP_PROJECT_ID=your-project-id
|
||||
|
||||
# Azure Blob Storage
|
||||
# AZURE_BACKUP_CONTAINER=your-backup-container
|
||||
# AZURE_STORAGE_ACCOUNT=your-storage-account
|
||||
# AZURE_STORAGE_KEY=your-storage-key
|
||||
|
||||
# ==============================================================================
|
||||
# SYSTEM PATHS (VM Specific)
|
||||
# ==============================================================================
|
||||
|
||||
# Data directories
|
||||
POSTGRES_DATA_DIR=/var/lib/postgresql/15/main
|
||||
REDIS_DATA_DIR=/var/lib/redis
|
||||
OLLAMA_DATA_DIR=/var/lib/ollama
|
||||
LITELLM_CONFIG_DIR=/etc/litellm
|
||||
OPENCLAW_CONFIG_DIR=/etc/openclaw
|
||||
|
||||
# ==============================================================================
|
||||
# SERVICE MANAGEMENT
|
||||
# ==============================================================================
|
||||
|
||||
# Systemd service names
|
||||
POSTGRES_SERVICE_NAME=postgresql
|
||||
REDIS_SERVICE_NAME=redis
|
||||
OLLAMA_SERVICE_NAME=ollama
|
||||
LITELLM_SERVICE_NAME=litellm
|
||||
OPENCLAW_SERVICE_NAME=openclaw-gateway
|
||||
|
||||
# ==============================================================================
|
||||
# CLOUD-SPECIFIC SETTINGS
|
||||
# ==============================================================================
|
||||
|
||||
# Cloud provider detection (auto-detected by vm-install.sh)
|
||||
# Options: aws, gcp, azure, digitalocean, linode, bare-metal
|
||||
CLOUD_PROVIDER=auto
|
||||
|
||||
# Instance metadata (auto-populated by vm-install.sh)
|
||||
# INSTANCE_TYPE=auto
|
||||
# INSTANCE_ID=auto
|
||||
# REGION=auto
|
||||
|
||||
# ==============================================================================
|
||||
# END OF ENVIRONMENT CONFIGURATION
|
||||
# ==============================================================================
|
||||
+384
@@ -0,0 +1,384 @@
|
||||
# OpenClaw CLI
|
||||
|
||||
Unified command-line interface for Heretek OpenClaw deployment and management.
|
||||
|
||||
## Installation
|
||||
|
||||
### From Source
|
||||
|
||||
```bash
|
||||
cd cli
|
||||
npm install
|
||||
npm link
|
||||
```
|
||||
|
||||
### Global Installation
|
||||
|
||||
```bash
|
||||
npm install -g @heretek/openclaw-cli
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Initialize OpenClaw
|
||||
openclaw init
|
||||
|
||||
# Deploy
|
||||
openclaw deploy
|
||||
|
||||
# Check status
|
||||
openclaw status
|
||||
|
||||
# View health
|
||||
openclaw health check
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### Core Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `openclaw init` | Initialize deployment configuration |
|
||||
| `openclaw deploy` | Deploy OpenClaw |
|
||||
| `openclaw status` | Check deployment status |
|
||||
| `openclaw logs` | View logs |
|
||||
| `openclaw stop` | Stop deployment |
|
||||
|
||||
### Management Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `openclaw backup` | Manage backups |
|
||||
| `openclaw config` | Manage configuration |
|
||||
| `openclaw update` | Update OpenClaw |
|
||||
| `openclaw agents` | Manage agents |
|
||||
| `openclaw health` | Run health checks |
|
||||
|
||||
## Command Reference
|
||||
|
||||
### `openclaw init`
|
||||
|
||||
Initialize deployment configuration with interactive setup wizard.
|
||||
|
||||
```bash
|
||||
# Interactive mode
|
||||
openclaw init
|
||||
|
||||
# Non-interactive mode
|
||||
openclaw init --type docker --non-interactive
|
||||
|
||||
# Specify output directory
|
||||
openclaw init --output /path/to/config
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `-t, --type <type>` - Deployment type (docker, bare-metal, kubernetes, aws, gcp, azure)
|
||||
- `-o, --output <path>` - Output directory for configuration
|
||||
- `-n, --non-interactive` - Non-interactive mode (use defaults)
|
||||
- `--skip-validation` - Skip configuration validation
|
||||
|
||||
### `openclaw deploy`
|
||||
|
||||
Deploy OpenClaw using the configured deployment type.
|
||||
|
||||
```bash
|
||||
# Interactive deployment
|
||||
openclaw deploy
|
||||
|
||||
# Deploy to Docker
|
||||
openclaw deploy docker
|
||||
|
||||
# Deploy to Kubernetes with Helm
|
||||
openclaw deploy kubernetes --method helm
|
||||
|
||||
# Deploy with build
|
||||
openclaw deploy --build --force-recreate
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `-c, --config <path>` - Configuration file path
|
||||
- `--build` - Build images before deployment
|
||||
- `--force-recreate` - Force recreate containers
|
||||
- `--pull` - Pull latest images
|
||||
- `--method <method>` - Deployment method (helm, kustomize) for Kubernetes
|
||||
- `--auto-approve` - Auto-approve Terraform changes
|
||||
- `-y, --yes` - Skip confirmation prompts
|
||||
|
||||
### `openclaw status`
|
||||
|
||||
Check deployment status and display service health.
|
||||
|
||||
```bash
|
||||
# Full status
|
||||
openclaw status
|
||||
|
||||
# Service status only
|
||||
openclaw status --services
|
||||
|
||||
# Agent status only
|
||||
openclaw status --agents
|
||||
|
||||
# JSON output
|
||||
openclaw status --json
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `-t, --type <type>` - Deployment type
|
||||
- `--services` - Show service status only
|
||||
- `--agents` - Show agent status only
|
||||
- `--resources` - Show resource usage
|
||||
- `--json` - Output as JSON
|
||||
|
||||
### `openclaw logs`
|
||||
|
||||
View logs from OpenClaw services.
|
||||
|
||||
```bash
|
||||
# View all logs
|
||||
openclaw logs
|
||||
|
||||
# View specific service logs
|
||||
openclaw logs gateway
|
||||
|
||||
# Follow logs
|
||||
openclaw logs -f
|
||||
|
||||
# Show last 200 lines
|
||||
openclaw logs --tail 200
|
||||
|
||||
# Filter by pattern
|
||||
openclaw logs --grep "error"
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `-f, --follow` - Follow log output
|
||||
- `-n, --tail <lines>` - Number of lines to show
|
||||
- `--since <time>` - Show logs since timestamp
|
||||
- `--until <time>` - Show logs until timestamp
|
||||
- `--timestamps` - Show timestamps
|
||||
- `--grep <pattern>` - Filter logs by pattern
|
||||
|
||||
### `openclaw stop`
|
||||
|
||||
Stop OpenClaw deployment gracefully.
|
||||
|
||||
```bash
|
||||
# Stop with confirmation
|
||||
openclaw stop
|
||||
|
||||
# Force stop
|
||||
openclaw stop --force
|
||||
|
||||
# Stop and create backup
|
||||
openclaw stop --backup
|
||||
|
||||
# Skip confirmation
|
||||
openclaw stop -y
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `-f, --force` - Force stop
|
||||
- `--volumes` - Remove volumes (Docker only)
|
||||
- `--backup` - Create backup before stopping
|
||||
- `-y, --yes` - Skip confirmation
|
||||
|
||||
### `openclaw backup`
|
||||
|
||||
Manage backups.
|
||||
|
||||
```bash
|
||||
# Create backup
|
||||
openclaw backup create
|
||||
|
||||
# Create full backup with verification
|
||||
openclaw backup create --type full --verify
|
||||
|
||||
# List backups
|
||||
openclaw backup list
|
||||
|
||||
# Restore from backup
|
||||
openclaw backup restore backup-name
|
||||
|
||||
# Delete backup
|
||||
openclaw backup delete backup-name
|
||||
|
||||
# Verify backup
|
||||
openclaw backup verify backup-name
|
||||
|
||||
# Rotate old backups
|
||||
openclaw backup rotate --days 30
|
||||
```
|
||||
|
||||
### `openclaw config`
|
||||
|
||||
Manage configuration.
|
||||
|
||||
```bash
|
||||
# Show configuration
|
||||
openclaw config show
|
||||
|
||||
# Show specific path
|
||||
openclaw config show --path model_routing.default
|
||||
|
||||
# Validate configuration
|
||||
openclaw config validate
|
||||
|
||||
# Reset to defaults
|
||||
openclaw config reset
|
||||
|
||||
# Get value
|
||||
openclaw config get model_routing.default
|
||||
|
||||
# Set value
|
||||
openclaw config set model_routing.default openai/gpt-4o
|
||||
```
|
||||
|
||||
### `openclaw update`
|
||||
|
||||
Update OpenClaw.
|
||||
|
||||
```bash
|
||||
# Check for updates
|
||||
openclaw update check
|
||||
|
||||
# Apply update
|
||||
openclaw update apply
|
||||
|
||||
# Dry run
|
||||
openclaw update apply --dry-run
|
||||
|
||||
# Rollback
|
||||
openclaw update rollback
|
||||
|
||||
# Show history
|
||||
openclaw update history
|
||||
```
|
||||
|
||||
### `openclaw agents`
|
||||
|
||||
Manage agents.
|
||||
|
||||
```bash
|
||||
# List agents
|
||||
openclaw agents list
|
||||
|
||||
# Start agent
|
||||
openclaw agents start steward
|
||||
|
||||
# Stop agent
|
||||
openclaw agents stop steward
|
||||
|
||||
# Agent status
|
||||
openclaw agents status steward
|
||||
|
||||
# Configure agent model
|
||||
openclaw agents configure steward --model openai/gpt-4o
|
||||
|
||||
# List available models
|
||||
openclaw agents models
|
||||
```
|
||||
|
||||
### `openclaw health`
|
||||
|
||||
Run health checks.
|
||||
|
||||
```bash
|
||||
# Run all checks
|
||||
openclaw health check
|
||||
|
||||
# Check specific service
|
||||
openclaw health check --service postgres
|
||||
|
||||
# Continuous monitoring
|
||||
openclaw health watch --interval 60
|
||||
|
||||
# Generate report
|
||||
openclaw health report --output report.json
|
||||
```
|
||||
|
||||
## Deployment Types
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
openclaw init --type docker
|
||||
openclaw deploy
|
||||
```
|
||||
|
||||
### Bare Metal
|
||||
|
||||
```bash
|
||||
openclaw init --type bare-metal
|
||||
openclaw deploy
|
||||
```
|
||||
|
||||
### Kubernetes
|
||||
|
||||
```bash
|
||||
openclaw init --type kubernetes
|
||||
openclaw deploy --method helm
|
||||
```
|
||||
|
||||
### Cloud (AWS/GCP/Azure)
|
||||
|
||||
```bash
|
||||
openclaw init --type aws
|
||||
openclaw deploy --auto-approve
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `GATEWAY_URL` | Gateway endpoint | `http://localhost:18789` |
|
||||
| `LITELLM_URL` | LiteLLM endpoint | `http://localhost:4000` |
|
||||
| `LITELLM_MASTER_KEY` | LiteLLM API key | - |
|
||||
| `POSTGRES_HOST` | PostgreSQL host | `localhost` |
|
||||
| `POSTGRES_PORT` | PostgreSQL port | `5432` |
|
||||
| `REDIS_HOST` | Redis host | `localhost` |
|
||||
| `REDIS_PORT` | Redis port | `6379` |
|
||||
|
||||
## Configuration Files
|
||||
|
||||
- `openclaw.json` - Main configuration file
|
||||
- `.env` - Environment variables
|
||||
- `~/.openclaw/openclaw.json` - User configuration
|
||||
- `cli/openclaw.config.js` - CLI settings
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Command not found
|
||||
|
||||
```bash
|
||||
# Ensure CLI is installed
|
||||
npm install -g @heretek/openclaw-cli
|
||||
|
||||
# Or run directly
|
||||
node cli/bin/openclaw.js
|
||||
```
|
||||
|
||||
### Permission denied
|
||||
|
||||
```bash
|
||||
# Fix npm global permissions
|
||||
sudo chown -R $(whoami) $(npm config get prefix)/{lib/node_modules,bin,share}
|
||||
```
|
||||
|
||||
### Docker connection failed
|
||||
|
||||
```bash
|
||||
# Ensure Docker is running
|
||||
docker ps
|
||||
|
||||
# Check Docker socket permissions
|
||||
sudo usermod -aG docker $USER
|
||||
```
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [CLI Documentation](../docs/cli/README.md)
|
||||
- [Command Reference](../docs/cli/COMMANDS.md)
|
||||
- [Configuration Guide](../docs/cli/CONFIGURATION.md)
|
||||
- [Usage Examples](../docs/cli/EXAMPLES.md)
|
||||
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Heretek OpenClaw CLI - Main Entry Point
|
||||
*
|
||||
* Unified command-line interface for OpenClaw deployment and management.
|
||||
*
|
||||
* Usage:
|
||||
* openclaw <command> [options]
|
||||
*
|
||||
* Commands:
|
||||
* init Initialize deployment configuration
|
||||
* deploy Deploy OpenClaw
|
||||
* status Check deployment status
|
||||
* logs View logs
|
||||
* stop Stop deployment
|
||||
* backup Manage backups
|
||||
* config Manage configuration
|
||||
* update Update OpenClaw
|
||||
* agents Manage agents
|
||||
* health Run health checks
|
||||
*/
|
||||
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
import { createRequire } from 'module';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
// Get CLI version from package.json
|
||||
const pkg = require('../package.json');
|
||||
|
||||
// Import Commander
|
||||
import { program } from 'commander';
|
||||
|
||||
// Configure program
|
||||
program
|
||||
.name('openclaw')
|
||||
.description('Heretek OpenClaw - Unified Deployment CLI')
|
||||
.version(pkg.version, '-v, --version', 'Display CLI version')
|
||||
.helpOption('-h, --help', 'Display help for command')
|
||||
.addHelpText('before', `
|
||||
╔══════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ Heretek OpenClaw CLI v${pkg.version}
|
||||
║ Unified deployment and management tool ║
|
||||
║ ║
|
||||
╚══════════════════════════════════════════════════════════╝
|
||||
`);
|
||||
|
||||
// Register commands
|
||||
const commands = [
|
||||
{ name: 'init', description: 'Initialize deployment configuration', file: '../src/commands/init.js' },
|
||||
{ name: 'deploy', description: 'Deploy OpenClaw', file: '../src/commands/deploy.js' },
|
||||
{ name: 'status', description: 'Check deployment status', file: '../src/commands/status.js' },
|
||||
{ name: 'logs', description: 'View logs', file: '../src/commands/logs.js' },
|
||||
{ name: 'stop', description: 'Stop deployment', file: '../src/commands/stop.js' },
|
||||
{ name: 'backup', description: 'Manage backups', file: '../src/commands/backup.js' },
|
||||
{ name: 'config', description: 'Manage configuration', file: '../src/commands/config.js' },
|
||||
{ name: 'update', description: 'Update OpenClaw', file: '../src/commands/update.js' },
|
||||
{ name: 'agents', description: 'Manage agents', file: '../src/commands/agents.js' },
|
||||
{ name: 'health', description: 'Run health checks', file: '../src/commands/health.js' },
|
||||
];
|
||||
|
||||
// Add commands to program
|
||||
commands.forEach(({ name, description, file }) => {
|
||||
try {
|
||||
const commandModule = require(file);
|
||||
if (commandModule.default) {
|
||||
program.addCommand(commandModule.default);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to load command '${name}': ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle unknown commands
|
||||
program.on('command:*', () => {
|
||||
console.error(`Error: Unknown command '${program.args.join(' ')}'`);
|
||||
console.error('Run "openclaw --help" to see available commands.');
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Parse arguments
|
||||
program.parse(process.argv);
|
||||
|
||||
// Show help if no command provided
|
||||
if (!process.argv.slice(2).length) {
|
||||
program.outputHelp();
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* OpenClaw CLI Configuration
|
||||
*
|
||||
* This file contains configuration options for the OpenClaw CLI tool.
|
||||
*/
|
||||
|
||||
export default {
|
||||
/**
|
||||
* CLI settings
|
||||
*/
|
||||
cli: {
|
||||
name: 'openclaw',
|
||||
version: '1.0.0',
|
||||
description: 'Heretek OpenClaw - Unified Deployment CLI',
|
||||
},
|
||||
|
||||
/**
|
||||
* Default paths
|
||||
*/
|
||||
paths: {
|
||||
// OpenClaw installation directory
|
||||
openclawDir: '~/.openclaw',
|
||||
|
||||
// Workspace directory for agents
|
||||
workspaceDir: '~/.openclaw/workspace',
|
||||
|
||||
// Agents directory
|
||||
agentsDir: '~/.openclaw/agents',
|
||||
|
||||
// Backups directory
|
||||
backupsDir: '~/.openclaw/backups',
|
||||
|
||||
// Logs directory
|
||||
logsDir: '~/.openclaw/logs',
|
||||
|
||||
// Cache directory
|
||||
cacheDir: '~/.openclaw/cache',
|
||||
},
|
||||
|
||||
/**
|
||||
* Default deployment settings
|
||||
*/
|
||||
deployment: {
|
||||
// Default deployment type
|
||||
defaultType: 'docker',
|
||||
|
||||
// Docker settings
|
||||
docker: {
|
||||
composeFile: 'docker-compose.yml',
|
||||
projectName: 'openclaw',
|
||||
},
|
||||
|
||||
// Kubernetes settings
|
||||
kubernetes: {
|
||||
namespace: 'openclaw',
|
||||
releaseName: 'openclaw',
|
||||
chartDir: './charts/openclaw',
|
||||
},
|
||||
|
||||
// Cloud settings
|
||||
cloud: {
|
||||
terraformDir: './terraform',
|
||||
autoApprove: false,
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Health check settings
|
||||
*/
|
||||
health: {
|
||||
// Default timeout for health checks (ms)
|
||||
timeout: 5000,
|
||||
|
||||
// Watch interval (seconds)
|
||||
watchInterval: 30,
|
||||
|
||||
// Service endpoints
|
||||
endpoints: {
|
||||
gateway: 'http://localhost:18789',
|
||||
litellm: 'http://localhost:4000',
|
||||
postgres: 'localhost:5432',
|
||||
redis: 'localhost:6379',
|
||||
ollama: 'http://localhost:11434',
|
||||
langfuse: 'http://localhost:3000',
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Backup settings
|
||||
*/
|
||||
backup: {
|
||||
// Default backup directory
|
||||
directory: '~/.openclaw/backups',
|
||||
|
||||
// Retention period (days)
|
||||
retentionDays: 30,
|
||||
|
||||
// Compression enabled
|
||||
compress: true,
|
||||
|
||||
// Backup schedule
|
||||
schedule: {
|
||||
full: '0 2 * * 0', // Sunday at 2 AM
|
||||
incremental: '0 2 * * 1-6', // Mon-Sat at 2 AM
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Logging settings
|
||||
*/
|
||||
logging: {
|
||||
// Default log level
|
||||
level: 'info',
|
||||
|
||||
// Show timestamps
|
||||
timestamps: true,
|
||||
|
||||
// Color output
|
||||
colors: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* Update settings
|
||||
*/
|
||||
update: {
|
||||
// Check for updates on startup
|
||||
checkOnStartup: false,
|
||||
|
||||
// Auto-update (not recommended for production)
|
||||
autoUpdate: false,
|
||||
|
||||
// Update channel
|
||||
channel: 'stable',
|
||||
},
|
||||
|
||||
/**
|
||||
* Feature flags
|
||||
*/
|
||||
features: {
|
||||
// Enable interactive prompts
|
||||
interactive: true,
|
||||
|
||||
// Enable telemetry (future)
|
||||
telemetry: false,
|
||||
|
||||
// Enable experimental features
|
||||
experimental: false,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "@heretek/openclaw-cli",
|
||||
"version": "1.0.0",
|
||||
"description": "Unified CLI tool for Heretek OpenClaw deployment and management",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"openclaw": "./bin/openclaw.js"
|
||||
},
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"start": "node bin/openclaw.js",
|
||||
"dev": "node --watch bin/openclaw.js",
|
||||
"test": "vitest run",
|
||||
"lint": "eslint src/",
|
||||
"lint:fix": "eslint src/ --fix"
|
||||
},
|
||||
"keywords": [
|
||||
"openclaw",
|
||||
"heretek",
|
||||
"cli",
|
||||
"deployment",
|
||||
"a2a",
|
||||
"agents"
|
||||
],
|
||||
"author": "Heretek",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/heretek/heretek-openclaw.git",
|
||||
"directory": "cli"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": "^12.0.0",
|
||||
"chalk": "^5.3.0",
|
||||
"ora": "^8.0.1",
|
||||
"inquirer": "^9.2.15",
|
||||
"execa": "^8.0.1",
|
||||
"fs-extra": "^11.2.0",
|
||||
"yaml": "^2.4.0",
|
||||
"axios": "^1.6.7",
|
||||
"dotenv": "^16.4.5",
|
||||
"boxen": "^7.1.1",
|
||||
"gradient-string": "^2.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"eslint": "^9.0.0",
|
||||
"vitest": "^1.3.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
/**
|
||||
* Agents Command
|
||||
*
|
||||
* Manage OpenClaw agents including list, start, stop, and configure.
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import axios from 'axios';
|
||||
import log from '../lib/logger.js';
|
||||
import { promptSelect, promptConfirm } from '../lib/prompts.js';
|
||||
|
||||
const command = new Command('agents');
|
||||
|
||||
const GATEWAY_URL = process.env.GATEWAY_URL || 'http://localhost:18789';
|
||||
|
||||
command
|
||||
.description('Manage agents')
|
||||
.addCommand(new Command('list')
|
||||
.description('List all agents')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action((options) => handleAgentsList(options))
|
||||
)
|
||||
.addCommand(new Command('start')
|
||||
.description('Start an agent')
|
||||
.argument('<agent>', 'Agent ID to start')
|
||||
.option('--model <model>', 'Model to assign')
|
||||
.action((agent, options) => handleAgentsStart(agent, options))
|
||||
)
|
||||
.addCommand(new Command('stop')
|
||||
.description('Stop an agent')
|
||||
.argument('<agent>', 'Agent ID to stop')
|
||||
.action((agent) => handleAgentsStop(agent))
|
||||
)
|
||||
.addCommand(new Command('status')
|
||||
.description('Show agent status')
|
||||
.argument('[agent]', 'Specific agent ID (optional)')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action((agent, options) => handleAgentsStatus(agent, options))
|
||||
)
|
||||
.addCommand(new Command('configure')
|
||||
.description('Configure agent model')
|
||||
.argument('<agent>', 'Agent ID to configure')
|
||||
.option('--model <model>', 'Model to assign')
|
||||
.option('--primary <model>', 'Primary model')
|
||||
.option('--failover <model>', 'Failover model')
|
||||
.action((agent, options) => handleAgentsConfigure(agent, options))
|
||||
)
|
||||
.addCommand(new Command('models')
|
||||
.description('List available models')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action((options) => handleAgentsModels(options))
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle agents list
|
||||
*/
|
||||
async function handleAgentsList(options) {
|
||||
log.section('Registered Agents');
|
||||
|
||||
try {
|
||||
const response = await axios.get(`${GATEWAY_URL}/v1/agents`, {
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
const agents = response.data?.agents || [];
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(agents, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
if (agents.length === 0) {
|
||||
console.log(' No agents registered');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(' ┌─────────────────┬─────────────────┬─────────────────┐');
|
||||
console.log(' │ Agent │ Role │ Status │');
|
||||
console.log(' ├─────────────────┼─────────────────┼─────────────────┤');
|
||||
|
||||
for (const agent of agents) {
|
||||
const name = (agent.agent_name || agent.name || 'unknown').substring(0, 15).padEnd(15);
|
||||
const role = (agent.role || 'unknown').substring(0, 15).padEnd(15);
|
||||
const status = (agent.status || 'active').substring(0, 15).padEnd(15);
|
||||
console.log(` │ ${name} │ ${role} │ ${status} │`);
|
||||
}
|
||||
|
||||
console.log(' └─────────────────┴─────────────────┴─────────────────┘');
|
||||
console.log(`\n Total: ${agents.length} agent(s)`);
|
||||
} catch (error) {
|
||||
log.error(`Failed to list agents: ${error.message}`);
|
||||
console.log('\n Make sure the Gateway is running and accessible.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle agents start
|
||||
*/
|
||||
async function handleAgentsStart(agent, options) {
|
||||
log.section(`Starting Agent: ${agent}`);
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
agent_id: agent,
|
||||
model: options.model,
|
||||
};
|
||||
|
||||
await axios.post(`${GATEWAY_URL}/api/v1/agents/${agent}/start`, payload, {
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
log.success(`Agent ${agent} started`);
|
||||
|
||||
if (options.model) {
|
||||
log.info(`Model assigned: ${options.model}`);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Failed to start agent: ${error.message}`);
|
||||
console.log(`
|
||||
To start an agent manually:
|
||||
cd agents/${agent}
|
||||
npm start
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle agents stop
|
||||
*/
|
||||
async function handleAgentsStop(agent) {
|
||||
log.section(`Stopping Agent: ${agent}`);
|
||||
|
||||
try {
|
||||
await axios.post(`${GATEWAY_URL}/api/v1/agents/${agent}/stop`, {}, {
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
log.success(`Agent ${agent} stopped`);
|
||||
} catch (error) {
|
||||
log.error(`Failed to stop agent: ${error.message}`);
|
||||
log.info('You can stop the agent manually by killing its process');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle agents status
|
||||
*/
|
||||
async function handleAgentsStatus(agent, options) {
|
||||
if (agent) {
|
||||
await handleAgentStatusSingle(agent, options);
|
||||
} else {
|
||||
await handleAgentsList(options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle single agent status
|
||||
*/
|
||||
async function handleAgentStatusSingle(agent, options) {
|
||||
log.section(`Agent Status: ${agent}`);
|
||||
|
||||
try {
|
||||
const response = await axios.get(`${GATEWAY_URL}/api/v1/agents/${agent}`, {
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
const agentData = response.data;
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(agentData, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`
|
||||
ID: ${agentData.id || agent}
|
||||
Name: ${agentData.name || 'unknown'}
|
||||
Role: ${agentData.role || 'unknown'}
|
||||
Status: ${agentData.status || 'unknown'}
|
||||
Model: ${agentData.model || 'not assigned'}
|
||||
Port: ${agentData.port || 'N/A'}
|
||||
|
||||
Last Active: ${agentData.lastActive || 'never'}
|
||||
Memory: ${agentData.memory || 'N/A'}
|
||||
`);
|
||||
} catch (error) {
|
||||
log.error(`Failed to get agent status: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle agents configure
|
||||
*/
|
||||
async function handleAgentsConfigure(agent, options) {
|
||||
log.section(`Configuring Agent: ${agent}`);
|
||||
|
||||
if (!options.model && !options.primary && !options.failover) {
|
||||
// Interactive mode
|
||||
const models = await getAvailableModels();
|
||||
|
||||
const selectedModel = await promptSelect(
|
||||
'Select model for this agent:',
|
||||
models.map(m => ({ name: m, value: m }))
|
||||
);
|
||||
|
||||
options.model = selectedModel;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
model: options.model,
|
||||
primary: options.primary,
|
||||
failover: options.failover,
|
||||
};
|
||||
|
||||
await axios.put(`${GATEWAY_URL}/api/v1/agents/${agent}/model`, payload, {
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
log.success(`Agent ${agent} configured`);
|
||||
|
||||
if (options.model) log.info(`Model: ${options.model}`);
|
||||
if (options.primary) log.info(`Primary: ${options.primary}`);
|
||||
if (options.failover) log.info(`Failover: ${options.failover}`);
|
||||
} catch (error) {
|
||||
log.error(`Failed to configure agent: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle agents models
|
||||
*/
|
||||
async function handleAgentsModels(options) {
|
||||
log.section('Available Models');
|
||||
|
||||
try {
|
||||
const models = await getAvailableModels();
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(models, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
if (models.length === 0) {
|
||||
console.log(' No models available');
|
||||
return;
|
||||
}
|
||||
|
||||
// Group by provider
|
||||
const grouped = {};
|
||||
for (const model of models) {
|
||||
const [provider] = model.split('/');
|
||||
if (!grouped[provider]) {
|
||||
grouped[provider] = [];
|
||||
}
|
||||
grouped[provider].push(model);
|
||||
}
|
||||
|
||||
for (const [provider, providerModels] of Object.entries(grouped)) {
|
||||
console.log(`\n ${provider.toUpperCase()}:`);
|
||||
providerModels.forEach(m => console.log(` - ${m}`));
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Failed to list models: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available models from LiteLLM
|
||||
*/
|
||||
async function getAvailableModels() {
|
||||
try {
|
||||
const response = await axios.get(`${process.env.LITELLM_URL || 'http://localhost:4000'}/v1/models`, {
|
||||
timeout: 5000,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${process.env.LITELLM_MASTER_KEY || 'heretek-master-key-change-me'}`,
|
||||
},
|
||||
});
|
||||
|
||||
return response.data?.data?.map(m => m.id) || [];
|
||||
} catch {
|
||||
// Return default models
|
||||
return [
|
||||
'ollama/llama2',
|
||||
'openai/gpt-4o',
|
||||
'anthropic/claude-sonnet-4-20250514',
|
||||
'google/gemini-2.5-pro',
|
||||
'minimax/minimax-abab6.5s',
|
||||
'zai/glm-5-1',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export default command;
|
||||
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* Backup Command
|
||||
*
|
||||
* Manage OpenClaw backups including create, list, restore, and delete.
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import log from '../lib/logger.js';
|
||||
import BackupManager from '../lib/backup-manager.js';
|
||||
import { promptConfirm } from '../lib/prompts.js';
|
||||
|
||||
const command = new Command('backup');
|
||||
|
||||
command
|
||||
.description('Manage backups')
|
||||
.addCommand(new Command('create')
|
||||
.description('Create a new backup')
|
||||
.option('-t, --type <type>', 'Backup type: full, incremental', 'incremental')
|
||||
.option('--verify', 'Verify backup after creation')
|
||||
.option('--compress', 'Compress backup files', true)
|
||||
.action((options) => handleBackupCreate(options))
|
||||
)
|
||||
.addCommand(new Command('list')
|
||||
.description('List available backups')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action((options) => handleBackupList(options))
|
||||
)
|
||||
.addCommand(new Command('restore')
|
||||
.description('Restore from a backup')
|
||||
.argument('<name>', 'Backup name to restore')
|
||||
.option('--components <list>', 'Components to restore (all, postgresql, redis, workspace, config)', 'all')
|
||||
.option('--confirm', 'Skip confirmation prompt')
|
||||
.action((name, options) => handleBackupRestore(name, options))
|
||||
)
|
||||
.addCommand(new Command('delete')
|
||||
.description('Delete a backup')
|
||||
.argument('<name>', 'Backup name to delete')
|
||||
.option('--confirm', 'Skip confirmation prompt')
|
||||
.action((name, options) => handleBackupDelete(name, options))
|
||||
)
|
||||
.addCommand(new Command('verify')
|
||||
.description('Verify a backup')
|
||||
.argument('<name>', 'Backup name to verify')
|
||||
.action((name) => handleBackupVerify(name))
|
||||
)
|
||||
.addCommand(new Command('rotate')
|
||||
.description('Rotate old backups based on retention policy')
|
||||
.option('--days <days>', 'Retention period in days', '30')
|
||||
.action((options) => handleBackupRotate(options))
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle backup create
|
||||
*/
|
||||
async function handleBackupCreate(options) {
|
||||
log.section('Creating Backup');
|
||||
|
||||
const backupManager = new BackupManager();
|
||||
|
||||
try {
|
||||
const result = await backupManager.create({
|
||||
type: options.type,
|
||||
verify: options.verify,
|
||||
compress: options.compress,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
log.success(`Backup created: ${result.name}`);
|
||||
} else {
|
||||
log.warn(`Backup completed with errors:`);
|
||||
result.errors.forEach(e => log.warn(` - ${e.component}: ${e.error}`));
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Backup failed: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle backup list
|
||||
*/
|
||||
async function handleBackupList(options) {
|
||||
const backupManager = new BackupManager();
|
||||
|
||||
try {
|
||||
const backups = await backupManager.list();
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(backups, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
log.section('Available Backups');
|
||||
|
||||
if (backups.length === 0) {
|
||||
console.log(' No backups found');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(' ┌─────────────────────────────────────────────────────────────┐');
|
||||
console.log(' │ Name │ Type │ Size │');
|
||||
console.log(' ├─────────────────────────────────────────────────────────────┤');
|
||||
|
||||
for (const backup of backups) {
|
||||
const name = backup.name.substring(0, 29).padEnd(29);
|
||||
const type = backup.type.padEnd(12);
|
||||
const size = formatSize(backup.size).padEnd(12);
|
||||
console.log(` │ ${name} │ ${type} │ ${size} │`);
|
||||
}
|
||||
|
||||
console.log(' └─────────────────────────────────────────────────────────────┘');
|
||||
console.log(`\n Total: ${backups.length} backup(s)`);
|
||||
} catch (error) {
|
||||
log.error(`Failed to list backups: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle backup restore
|
||||
*/
|
||||
async function handleBackupRestore(name, options) {
|
||||
log.section('Restoring Backup');
|
||||
|
||||
if (!options.confirm) {
|
||||
const confirmed = await promptConfirm(
|
||||
`Restore from backup "${name}"? This will overwrite existing data.`,
|
||||
{ default: false }
|
||||
);
|
||||
|
||||
if (!confirmed) {
|
||||
log.info('Restore cancelled');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const backupManager = new BackupManager();
|
||||
|
||||
try {
|
||||
const components = options.components === 'all'
|
||||
? ['all']
|
||||
: options.components.split(',');
|
||||
|
||||
const result = await backupManager.restore(name, {
|
||||
components,
|
||||
confirm: true,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
log.success(`Backup restored: ${name}`);
|
||||
} else {
|
||||
log.warn(`Restore completed with errors:`);
|
||||
result.errors.forEach(e => log.warn(` - ${e.component}: ${e.error}`));
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Restore failed: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle backup delete
|
||||
*/
|
||||
async function handleBackupDelete(name, options) {
|
||||
log.section('Deleting Backup');
|
||||
|
||||
if (!options.confirm) {
|
||||
const confirmed = await promptConfirm(
|
||||
`Delete backup "${name}"? This action cannot be undone.`,
|
||||
{ default: false }
|
||||
);
|
||||
|
||||
if (!confirmed) {
|
||||
log.info('Delete cancelled');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const backupManager = new BackupManager();
|
||||
|
||||
try {
|
||||
const success = await backupManager.delete(name);
|
||||
|
||||
if (success) {
|
||||
log.success(`Backup deleted: ${name}`);
|
||||
} else {
|
||||
log.error('Failed to delete backup');
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Delete failed: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle backup verify
|
||||
*/
|
||||
async function handleBackupVerify(name) {
|
||||
log.section('Verifying Backup');
|
||||
|
||||
const backupManager = new BackupManager();
|
||||
|
||||
try {
|
||||
const result = await backupManager.verify(name);
|
||||
|
||||
if (result.valid) {
|
||||
log.success(`Backup verification passed: ${name}`);
|
||||
|
||||
result.checks.forEach(check => {
|
||||
if (check.valid) {
|
||||
log.success(` ✓ ${check.name}`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
log.error(`Backup verification failed: ${name}`);
|
||||
|
||||
result.checks.forEach(check => {
|
||||
if (!check.valid) {
|
||||
log.error(` ✗ ${check.name}: ${check.error}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Verification failed: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle backup rotate
|
||||
*/
|
||||
async function handleBackupRotate(options) {
|
||||
log.section('Rotating Backups');
|
||||
|
||||
const backupManager = new BackupManager({
|
||||
retentionDays: parseInt(options.days, 10),
|
||||
});
|
||||
|
||||
try {
|
||||
const deleted = await backupManager.rotate();
|
||||
log.success(`Rotation complete: ${deleted} backup(s) deleted`);
|
||||
} catch (error) {
|
||||
log.error(`Rotation failed: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format file size
|
||||
*/
|
||||
function formatSize(bytes) {
|
||||
if (!bytes || bytes === 0) return '0 B';
|
||||
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
|
||||
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
|
||||
}
|
||||
|
||||
export default command;
|
||||
@@ -0,0 +1,249 @@
|
||||
/**
|
||||
* Config Command
|
||||
*
|
||||
* Manage OpenClaw configuration including view, edit, validate, and reset.
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import log from '../lib/logger.js';
|
||||
import ConfigManager from '../lib/config-manager.js';
|
||||
import { promptConfirm, promptEditor } from '../lib/prompts.js';
|
||||
|
||||
const command = new Command('config');
|
||||
|
||||
command
|
||||
.description('Manage configuration')
|
||||
.addCommand(new Command('show')
|
||||
.description('Show current configuration')
|
||||
.option('--json', 'Output as JSON')
|
||||
.option('--path <path>', 'Show specific config path (e.g., models.providers)')
|
||||
.action((options) => handleConfigShow(options))
|
||||
)
|
||||
.addCommand(new Command('edit')
|
||||
.description('Edit configuration')
|
||||
.option('--path <path>', 'Edit specific config path')
|
||||
.action((options) => handleConfigEdit(options))
|
||||
)
|
||||
.addCommand(new Command('validate')
|
||||
.description('Validate configuration')
|
||||
.option('--strict', 'Enable strict validation')
|
||||
.action((options) => handleConfigValidate(options))
|
||||
)
|
||||
.addCommand(new Command('reset')
|
||||
.description('Reset configuration to defaults')
|
||||
.option('--confirm', 'Skip confirmation prompt')
|
||||
.action((options) => handleConfigReset(options))
|
||||
)
|
||||
.addCommand(new Command('get')
|
||||
.description('Get a specific configuration value')
|
||||
.argument('<path>', 'Configuration path (e.g., model_routing.default)')
|
||||
.action((path) => handleConfigGet(path))
|
||||
)
|
||||
.addCommand(new Command('set')
|
||||
.description('Set a configuration value')
|
||||
.argument('<path>', 'Configuration path (e.g., model_routing.default)')
|
||||
.argument('<value>', 'Value to set')
|
||||
.action((path, value) => handleConfigSet(path, value))
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle config show
|
||||
*/
|
||||
async function handleConfigShow(options) {
|
||||
const configManager = new ConfigManager();
|
||||
|
||||
try {
|
||||
await configManager.load();
|
||||
|
||||
if (options.json) {
|
||||
if (options.path) {
|
||||
const value = configManager.get(options.path);
|
||||
console.log(JSON.stringify(value, null, 2));
|
||||
} else {
|
||||
console.log(JSON.stringify(configManager.config, null, 2));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
log.section('OpenClaw Configuration');
|
||||
|
||||
if (options.path) {
|
||||
const value = configManager.get(options.path);
|
||||
console.log(`\n ${options.path}: ${JSON.stringify(value, null, 2)}`);
|
||||
} else {
|
||||
printConfigSummary(configManager.config);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Failed to show configuration: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print configuration summary
|
||||
*/
|
||||
function printConfigSummary(config) {
|
||||
console.log(`
|
||||
Version: ${config.version || 'unknown'}
|
||||
Collective: ${config.collective?.name || 'unknown'}
|
||||
|
||||
Models:
|
||||
Providers: ${Object.keys(config.models?.providers || {}).join(', ') || 'none'}
|
||||
|
||||
Model Routing:
|
||||
Default: ${config.model_routing?.default || 'not set'}
|
||||
Failover: ${config.model_routing?.aliases?.failover || 'not set'}
|
||||
|
||||
Agents: ${config.agents?.length || 0}
|
||||
`);
|
||||
|
||||
if (config.agents?.length > 0) {
|
||||
console.log(' Agent List:');
|
||||
config.agents.forEach(agent => {
|
||||
console.log(` - ${agent.id} (${agent.role}): ${agent.model}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle config edit
|
||||
*/
|
||||
async function handleConfigEdit(options) {
|
||||
const configManager = new ConfigManager();
|
||||
|
||||
try {
|
||||
await configManager.load();
|
||||
|
||||
log.info('Opening configuration editor...');
|
||||
console.log(' Edit the configuration and save to apply changes.\n');
|
||||
|
||||
// In a real implementation, this would open the system editor
|
||||
// For now, we'll show the config file path
|
||||
console.log(` Configuration file: ${configManager.configPath}`);
|
||||
console.log('\n To edit manually:');
|
||||
console.log(` nano ${configManager.configPath}`);
|
||||
console.log(' # or');
|
||||
console.log(` code ${configManager.configPath}`);
|
||||
} catch (error) {
|
||||
log.error(`Failed to edit configuration: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle config validate
|
||||
*/
|
||||
async function handleConfigValidate(options) {
|
||||
const configManager = new ConfigManager();
|
||||
|
||||
try {
|
||||
await configManager.load();
|
||||
|
||||
log.section('Validating Configuration');
|
||||
|
||||
const result = configManager.validate();
|
||||
|
||||
if (result.valid) {
|
||||
log.success('Configuration is valid');
|
||||
} else {
|
||||
log.error('Configuration validation failed:');
|
||||
result.errors.forEach(e => log.error(` ✗ ${e}`));
|
||||
|
||||
if (result.warnings.length > 0) {
|
||||
log.warn('Warnings:');
|
||||
result.warnings.forEach(w => log.warn(` ⚠ ${w}`));
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Validation failed: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle config reset
|
||||
*/
|
||||
async function handleConfigReset(options) {
|
||||
const configManager = new ConfigManager();
|
||||
|
||||
if (!options.confirm) {
|
||||
const confirmed = await promptConfirm(
|
||||
'Reset configuration to defaults? This will overwrite current settings.',
|
||||
{ default: false }
|
||||
);
|
||||
|
||||
if (!confirmed) {
|
||||
log.info('Reset cancelled');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
log.section('Resetting Configuration');
|
||||
|
||||
const defaultConfig = configManager.createDefault();
|
||||
await configManager.save(defaultConfig);
|
||||
|
||||
log.success('Configuration reset to defaults');
|
||||
} catch (error) {
|
||||
log.error(`Reset failed: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle config get
|
||||
*/
|
||||
async function handleConfigGet(path) {
|
||||
const configManager = new ConfigManager();
|
||||
|
||||
try {
|
||||
await configManager.load();
|
||||
|
||||
const value = configManager.get(path);
|
||||
|
||||
if (value === undefined) {
|
||||
log.warn(`Configuration path not found: ${path}`);
|
||||
} else {
|
||||
console.log(typeof value === 'object'
|
||||
? JSON.stringify(value, null, 2)
|
||||
: value);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Failed to get configuration: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle config set
|
||||
*/
|
||||
async function handleConfigSet(path, value) {
|
||||
const configManager = new ConfigManager();
|
||||
|
||||
try {
|
||||
await configManager.load();
|
||||
|
||||
// Parse value type
|
||||
let parsedValue;
|
||||
try {
|
||||
parsedValue = JSON.parse(value);
|
||||
} catch {
|
||||
parsedValue = value;
|
||||
}
|
||||
|
||||
configManager.set(path, parsedValue);
|
||||
await configManager.save();
|
||||
|
||||
log.success(`Set ${path} = ${JSON.stringify(parsedValue)}`);
|
||||
} catch (error) {
|
||||
log.error(`Failed to set configuration: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export default command;
|
||||
@@ -0,0 +1,216 @@
|
||||
/**
|
||||
* Deploy Command
|
||||
*
|
||||
* Deploy OpenClaw using the configured deployment type.
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import log from '../lib/logger.js';
|
||||
import DeploymentManager, { DeploymentType } from '../lib/deployment-manager.js';
|
||||
import { promptSelect, promptConfirm } from '../lib/prompts.js';
|
||||
|
||||
const command = new Command('deploy');
|
||||
|
||||
command
|
||||
.description('Deploy OpenClaw')
|
||||
.argument('[type]', 'Deployment type (docker, bare-metal, kubernetes, aws, gcp, azure)')
|
||||
.option('-c, --config <path>', 'Configuration file path')
|
||||
.option('--build', 'Build images before deployment (Docker)')
|
||||
.option('--force-recreate', 'Force recreate containers (Docker)')
|
||||
.option('--pull', 'Pull latest images before deployment (Docker)')
|
||||
.option('--method <method>', 'Deployment method (helm, kustomize) for Kubernetes')
|
||||
.option('--auto-approve', 'Auto-approve Terraform changes (Cloud)')
|
||||
.option('-y, --yes', 'Skip confirmation prompts')
|
||||
.action(async (type, options) => {
|
||||
await handleDeploy(type, options);
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle deploy command
|
||||
*/
|
||||
async function handleDeploy(type, options) {
|
||||
log.section('Deploying OpenClaw');
|
||||
|
||||
// Determine deployment type
|
||||
let deploymentType = type;
|
||||
|
||||
if (!deploymentType) {
|
||||
// Try to read from config
|
||||
deploymentType = await detectDeploymentType(options.config);
|
||||
|
||||
if (!deploymentType) {
|
||||
// Interactive selection
|
||||
deploymentType = await promptSelect(
|
||||
'Select deployment type:',
|
||||
[
|
||||
{ name: 'Docker Compose', value: DeploymentType.DOCKER },
|
||||
{ name: 'Bare Metal', value: DeploymentType.BARE_METAL },
|
||||
{ name: 'Kubernetes', value: DeploymentType.KUBERNETES },
|
||||
{ name: 'AWS', value: DeploymentType.AWS },
|
||||
{ name: 'GCP', value: DeploymentType.GCP },
|
||||
{ name: 'Azure', value: DeploymentType.AZURE },
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate deployment type
|
||||
if (!Object.values(DeploymentType).includes(deploymentType)) {
|
||||
log.error(`Unknown deployment type: ${deploymentType}`);
|
||||
printUsage();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Confirm deployment
|
||||
if (!options.yes) {
|
||||
const confirmed = await promptConfirm(
|
||||
`Deploy to ${deploymentType}? This may take several minutes.`,
|
||||
{ default: true }
|
||||
);
|
||||
|
||||
if (!confirmed) {
|
||||
log.info('Deployment cancelled');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Create deployment manager
|
||||
const manager = new DeploymentManager({
|
||||
rootDir: process.cwd(),
|
||||
deploymentType,
|
||||
configPath: options.config,
|
||||
});
|
||||
|
||||
// Build deploy options
|
||||
const deployOptions = {
|
||||
build: options.build,
|
||||
forceRecreate: options.forceRecreate,
|
||||
pull: options.pull,
|
||||
method: options.method,
|
||||
autoApprove: options.autoApprove,
|
||||
};
|
||||
|
||||
// Execute deployment
|
||||
try {
|
||||
const success = await manager.deploy(deployOptions);
|
||||
|
||||
if (success) {
|
||||
log.success(`Deployment to ${deploymentType} completed successfully!`);
|
||||
|
||||
// Show status
|
||||
await showPostDeployStatus(manager);
|
||||
} else {
|
||||
log.error('Deployment failed');
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Deployment failed: ${error.message}`);
|
||||
log.debug(error.stack);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect deployment type from config or environment
|
||||
*/
|
||||
async function detectDeploymentType(configPath) {
|
||||
// Check for docker-compose.yml
|
||||
const fs = await import('fs-extra');
|
||||
|
||||
if (await fs.pathExists('docker-compose.yml')) {
|
||||
return DeploymentType.DOCKER;
|
||||
}
|
||||
|
||||
// Check for kubernetes manifests
|
||||
if (await fs.pathExists('charts/openclaw') || await fs.pathExists('deploy/k8s')) {
|
||||
return DeploymentType.KUBERNETES;
|
||||
}
|
||||
|
||||
// Check for terraform configs
|
||||
if (await fs.pathExists('terraform/aws')) {
|
||||
return DeploymentType.AWS;
|
||||
}
|
||||
if (await fs.pathExists('terraform/gcp')) {
|
||||
return DeploymentType.GCP;
|
||||
}
|
||||
if (await fs.pathExists('terraform/azure')) {
|
||||
return DeploymentType.AZURE;
|
||||
}
|
||||
|
||||
// Check config file
|
||||
if (configPath && await fs.pathExists(configPath)) {
|
||||
try {
|
||||
const content = await fs.readFile(configPath, 'utf-8');
|
||||
const config = JSON.parse(content);
|
||||
if (config.deployment?.type) {
|
||||
return config.deployment.type;
|
||||
}
|
||||
} catch {
|
||||
// Ignore parse errors
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show post-deployment status
|
||||
*/
|
||||
async function showPostDeployStatus(manager) {
|
||||
log.subheader('Deployment Status');
|
||||
|
||||
try {
|
||||
const status = await manager.status();
|
||||
const health = await manager.healthCheck();
|
||||
|
||||
if (health.healthy) {
|
||||
log.success('All services are healthy');
|
||||
} else {
|
||||
log.warn('Some services may not be healthy yet');
|
||||
}
|
||||
|
||||
// Print status details based on deployment type
|
||||
if (status.containers) {
|
||||
console.log('\nContainers:');
|
||||
status.containers.forEach(c => {
|
||||
const statusSymbol = c.State?.includes('running') ? '✓' : '⚠';
|
||||
console.log(` ${statusSymbol} ${c.Names}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (status.pods) {
|
||||
console.log('\nPods:');
|
||||
status.pods.forEach(p => {
|
||||
const statusSymbol = p.ready ? '✓' : '⚠';
|
||||
console.log(` ${statusSymbol} ${p.name} (${p.status})`);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
log.debug(`Could not retrieve status: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print usage information
|
||||
*/
|
||||
function printUsage() {
|
||||
console.log(`
|
||||
Usage: openclaw deploy [type] [options]
|
||||
|
||||
Deployment Types:
|
||||
docker Deploy using Docker Compose
|
||||
bare-metal Deploy directly on host system
|
||||
kubernetes Deploy to Kubernetes cluster
|
||||
aws Deploy to AWS using Terraform
|
||||
gcp Deploy to GCP using Terraform
|
||||
azure Deploy to Azure using Terraform
|
||||
|
||||
Examples:
|
||||
openclaw deploy docker
|
||||
openclaw deploy kubernetes --method helm
|
||||
openclaw deploy aws --auto-approve
|
||||
openclaw deploy --build --force-recreate
|
||||
`);
|
||||
}
|
||||
|
||||
export default command;
|
||||
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* Health Command
|
||||
*
|
||||
* Run health checks on OpenClaw services.
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import log from '../lib/logger.js';
|
||||
import HealthChecker from '../lib/health-checker.js';
|
||||
|
||||
const command = new Command('health');
|
||||
|
||||
command
|
||||
.description('Run health checks')
|
||||
.addCommand(new Command('check')
|
||||
.description('Run all health checks')
|
||||
.option('--service <name>', 'Check specific service only')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action((options) => handleHealthCheck(options))
|
||||
)
|
||||
.addCommand(new Command('watch')
|
||||
.description('Continuously monitor health')
|
||||
.option('--interval <seconds>', 'Check interval in seconds', '30')
|
||||
.action((options) => handleHealthWatch(options))
|
||||
)
|
||||
.addCommand(new Command('report')
|
||||
.description('Generate health report')
|
||||
.option('--output <file>', 'Save report to file')
|
||||
.action((options) => handleHealthReport(options))
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle health check
|
||||
*/
|
||||
async function handleHealthCheck(options) {
|
||||
const checker = new HealthChecker();
|
||||
|
||||
try {
|
||||
let results;
|
||||
|
||||
if (options.service) {
|
||||
// Check specific service
|
||||
results = {
|
||||
checks: {
|
||||
[options.service]: await checker.checkService(options.service),
|
||||
},
|
||||
};
|
||||
results.healthy = Object.values(results.checks).every(c => c.healthy);
|
||||
} else {
|
||||
// Check all services
|
||||
results = await checker.checkAll();
|
||||
}
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(results, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
printHealthResults(results);
|
||||
} catch (error) {
|
||||
log.error(`Health check failed: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print health results
|
||||
*/
|
||||
function printHealthResults(results) {
|
||||
log.section('OpenClaw Health Status');
|
||||
|
||||
const overallStatus = results.healthy
|
||||
? chalk.green('● HEALTHY')
|
||||
: chalk.red('● UNHEALTHY');
|
||||
|
||||
console.log(`\n Overall Status: ${overallStatus}`);
|
||||
console.log(` Timestamp: ${results.timestamp || new Date().toISOString()}`);
|
||||
console.log('');
|
||||
|
||||
// Service status table
|
||||
console.log(' ┌─────────────────┬─────────────────┬─────────────────────────────────┐');
|
||||
console.log(' │ Service │ Status │ Details │');
|
||||
console.log(' ├─────────────────┼─────────────────┼─────────────────────────────────┤');
|
||||
|
||||
for (const [service, check] of Object.entries(results.checks || {})) {
|
||||
const serviceName = service.padEnd(15);
|
||||
const statusSymbol = check.healthy ? chalk.green('✓') : chalk.red('✗');
|
||||
const statusText = check.healthy ? 'Healthy' : 'Unhealthy';
|
||||
const status = `${statusSymbol} ${statusText}`.padEnd(15);
|
||||
|
||||
// Build details string
|
||||
let details = [];
|
||||
if (check.modelCount !== undefined) details.push(`Models: ${check.modelCount}`);
|
||||
if (check.agentCount !== undefined) details.push(`Agents: ${check.agentCount}`);
|
||||
if (check.pgvector !== undefined) details.push(`pgvector: ${check.pgvector ? 'yes' : 'no'}`);
|
||||
if (check.memoryUsed !== undefined) details.push(`Memory: ${check.memoryUsed}`);
|
||||
if (check.responseTime !== undefined) details.push(`Response: ${check.responseTime}`);
|
||||
if (check.error) details.push(chalk.red(check.error));
|
||||
|
||||
const detailsText = details.join(', ').substring(0, 31).padEnd(31);
|
||||
|
||||
console.log(` │ ${serviceName} │ ${status} │ ${detailsText} │`);
|
||||
}
|
||||
|
||||
console.log(' └─────────────────┴─────────────────┴─────────────────────────────────┘');
|
||||
|
||||
// Print recommendations for unhealthy services
|
||||
const unhealthyServices = Object.entries(results.checks || {})
|
||||
.filter(([, check]) => !check.healthy);
|
||||
|
||||
if (unhealthyServices.length > 0) {
|
||||
console.log('\n');
|
||||
log.subheader('Recommendations');
|
||||
|
||||
for (const [service, check] of unhealthyServices) {
|
||||
const recommendation = getRecommendation(service, check);
|
||||
console.log(` ${chalk.yellow('→')} ${service.toUpperCase()}: ${recommendation}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Exit with error if unhealthy
|
||||
if (!results.healthy) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recommendation for unhealthy service
|
||||
*/
|
||||
function getRecommendation(service, check) {
|
||||
const recommendations = {
|
||||
gateway: 'Check if Gateway is running on port 18789',
|
||||
litellm: 'Verify LiteLLM is running and API key is correct',
|
||||
postgres: 'Ensure PostgreSQL is running and credentials are correct',
|
||||
redis: 'Ensure Redis is running on port 6379',
|
||||
ollama: 'Start Ollama service: ollama serve',
|
||||
langfuse: 'Check Langfuse deployment and configuration',
|
||||
agents: 'Verify agents are deployed and connected to Gateway',
|
||||
};
|
||||
|
||||
return recommendations[service] || `Check ${service} logs for more information`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle health watch
|
||||
*/
|
||||
async function handleHealthWatch(options) {
|
||||
const interval = parseInt(options.interval, 10) * 1000;
|
||||
const checker = new HealthChecker();
|
||||
|
||||
log.section('Health Monitor');
|
||||
log.info(`Watching services (interval: ${options.interval}s)`);
|
||||
log.info('Press Ctrl+C to stop\n');
|
||||
|
||||
const watch = async () => {
|
||||
try {
|
||||
const results = await checker.checkAll();
|
||||
|
||||
// Clear screen and print status
|
||||
process.stdout.write('\x1Bc');
|
||||
console.log(`Health Monitor (Ctrl+C to stop) - ${new Date().toISOString()}`);
|
||||
console.log('');
|
||||
|
||||
printHealthResults(results);
|
||||
} catch (error) {
|
||||
log.error(`Health check failed: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Initial check
|
||||
await watch();
|
||||
|
||||
// Interval checks
|
||||
setInterval(watch, interval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle health report
|
||||
*/
|
||||
async function handleHealthReport(options) {
|
||||
const checker = new HealthChecker();
|
||||
|
||||
try {
|
||||
const report = await checker.generateReport();
|
||||
|
||||
if (options.output) {
|
||||
const fs = await import('fs-extra');
|
||||
await fs.writeFile(options.output, JSON.stringify(report, null, 2));
|
||||
log.success(`Report saved to: ${options.output}`);
|
||||
} else {
|
||||
printHealthReport(report);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Failed to generate report: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print health report
|
||||
*/
|
||||
function printHealthReport(report) {
|
||||
log.section('Health Report');
|
||||
|
||||
const summary = report.summary;
|
||||
|
||||
console.log(`
|
||||
Generated: ${summary.timestamp}
|
||||
Status: ${summary.healthy ? chalk.green('HEALTHY') : chalk.red('UNHEALTHY')}
|
||||
|
||||
Checks: ${summary.healthyChecks}/${summary.totalChecks} passed
|
||||
`);
|
||||
|
||||
if (report.recommendations.length > 0) {
|
||||
log.subheader('Issues & Recommendations');
|
||||
|
||||
for (const rec of report.recommendations) {
|
||||
console.log(`
|
||||
${chalk.yellow(rec.service.toUpperCase())}
|
||||
Issue: ${rec.issue}
|
||||
Action: ${rec.action}
|
||||
`);
|
||||
}
|
||||
} else {
|
||||
log.success('All services are healthy - no issues found');
|
||||
}
|
||||
}
|
||||
|
||||
export default command;
|
||||
@@ -0,0 +1,327 @@
|
||||
/**
|
||||
* Init Command
|
||||
*
|
||||
* Initialize deployment configuration with interactive setup wizard.
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import log from '../lib/logger.js';
|
||||
import ConfigManager from '../lib/config-manager.js';
|
||||
import { promptSelect, promptConfirm, promptText, promptPassword, promptSequence } from '../lib/prompts.js';
|
||||
|
||||
const command = new Command('init');
|
||||
|
||||
command
|
||||
.description('Initialize deployment configuration')
|
||||
.option('-t, --type <type>', 'Deployment type (docker, bare-metal, kubernetes, aws, gcp, azure)')
|
||||
.option('-o, --output <path>', 'Output directory for configuration')
|
||||
.option('-n, --non-interactive', 'Non-interactive mode (use defaults)')
|
||||
.option('--skip-validation', 'Skip configuration validation')
|
||||
.action(async (options) => {
|
||||
await handleInit(options);
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle init command
|
||||
*/
|
||||
async function handleInit(options) {
|
||||
log.section('OpenClaw Initialization');
|
||||
|
||||
const configManager = new ConfigManager({
|
||||
rootDir: options.output || process.cwd(),
|
||||
});
|
||||
|
||||
// Non-interactive mode
|
||||
if (options.nonInteractive) {
|
||||
return await initNonInteractive(configManager, options);
|
||||
}
|
||||
|
||||
// Interactive mode
|
||||
return await initInteractive(configManager, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interactive initialization
|
||||
*/
|
||||
async function initInteractive(configManager, options) {
|
||||
log.info('Starting interactive setup wizard...\n');
|
||||
|
||||
const config = {
|
||||
version: '2.0.0',
|
||||
collective: {
|
||||
name: 'OpenClaw Collective',
|
||||
description: 'Self-improving autonomous agent collective',
|
||||
version: '2.0.0',
|
||||
},
|
||||
models: {
|
||||
providers: {},
|
||||
},
|
||||
agents: [],
|
||||
model_routing: {
|
||||
default: '',
|
||||
aliases: {
|
||||
failover: '',
|
||||
},
|
||||
},
|
||||
deployment: {
|
||||
type: options.type || 'docker',
|
||||
},
|
||||
};
|
||||
|
||||
// Step 1: Welcome
|
||||
console.log(`
|
||||
${log.symbols.info} Welcome to Heretek OpenClaw!
|
||||
This wizard will guide you through the setup process.
|
||||
`);
|
||||
|
||||
// Step 2: Deployment type selection
|
||||
log.subheader('Step 1: Deployment Type');
|
||||
|
||||
const deploymentType = await promptSelect(
|
||||
'Select deployment type:',
|
||||
[
|
||||
{ name: 'Docker (Recommended for local development)', value: 'docker' },
|
||||
{ name: 'Bare Metal (Direct installation)', value: 'bare-metal' },
|
||||
{ name: 'Kubernetes (Production)', value: 'kubernetes' },
|
||||
{ name: 'AWS Cloud', value: 'aws' },
|
||||
{ name: 'GCP Cloud', value: 'gcp' },
|
||||
{ name: 'Azure Cloud', value: 'azure' },
|
||||
]
|
||||
);
|
||||
|
||||
config.deployment.type = deploymentType;
|
||||
log.success(`Deployment type: ${deploymentType}`);
|
||||
|
||||
// Step 3: AI Provider selection
|
||||
log.subheader('Step 2: AI Provider Configuration');
|
||||
|
||||
const primaryProvider = await promptSelect(
|
||||
'Select primary AI provider:',
|
||||
[
|
||||
{ name: 'MiniMax (Recommended)', value: 'minimax' },
|
||||
{ name: 'z.ai', value: 'zai' },
|
||||
{ name: 'OpenAI', value: 'openai' },
|
||||
{ name: 'Anthropic', value: 'anthropic' },
|
||||
{ name: 'Google', value: 'google' },
|
||||
{ name: 'Ollama (Local/Free)', value: 'ollama' },
|
||||
]
|
||||
);
|
||||
|
||||
log.success(`Primary provider: ${primaryProvider}`);
|
||||
|
||||
// Set default model based on provider
|
||||
const defaultModels = {
|
||||
minimax: 'minimax/minimax-abab6.5s',
|
||||
zai: 'zai/glm-5-1',
|
||||
openai: 'openai/gpt-4o',
|
||||
anthropic: 'anthropic/claude-sonnet-4-20250514',
|
||||
google: 'google/gemini-2.5-pro',
|
||||
ollama: 'ollama/llama2',
|
||||
};
|
||||
|
||||
config.model_routing.default = defaultModels[primaryProvider];
|
||||
config.model_routing.aliases.failover = defaultModels[primaryProvider];
|
||||
|
||||
// Step 4: API Keys
|
||||
log.subheader('Step 3: API Key Configuration');
|
||||
|
||||
const apiKey = await promptPassword(
|
||||
`${primaryProvider.toUpperCase()} API Key:`,
|
||||
{ mask: '*' }
|
||||
);
|
||||
|
||||
// Store in environment variable reference
|
||||
const envVarMap = {
|
||||
minimax: 'MINIMAX_API_KEY',
|
||||
zai: 'ZAI_API_KEY',
|
||||
openai: 'OPENAI_API_KEY',
|
||||
anthropic: 'ANTHROPIC_API_KEY',
|
||||
google: 'GOOGLE_API_KEY',
|
||||
};
|
||||
|
||||
const envVar = envVarMap[primaryProvider];
|
||||
if (envVar) {
|
||||
configManager.setEnv(envVar, apiKey);
|
||||
log.success(`API key configured for ${primaryProvider}`);
|
||||
}
|
||||
|
||||
// Step 5: Agent selection
|
||||
log.subheader('Step 4: Agent Configuration');
|
||||
|
||||
const enableAllAgents = await promptConfirm(
|
||||
'Enable all agents? (recommended for full functionality)',
|
||||
{ default: true }
|
||||
);
|
||||
|
||||
const availableAgents = [
|
||||
{ id: 'steward', name: 'Steward', role: 'Orchestrator' },
|
||||
{ id: 'alpha', name: 'Alpha', role: 'Triad' },
|
||||
{ id: 'beta', name: 'Beta', role: 'Triad' },
|
||||
{ id: 'charlie', name: 'Charlie', role: 'Triad' },
|
||||
{ id: 'examiner', name: 'Examiner', role: 'Interrogator' },
|
||||
{ id: 'explorer', name: 'Explorer', role: 'Scout' },
|
||||
{ id: 'historian', name: 'Historian', role: 'Archivist' },
|
||||
];
|
||||
|
||||
if (enableAllAgents) {
|
||||
config.agents = availableAgents.map((agent, index) => ({
|
||||
id: agent.id,
|
||||
name: agent.name,
|
||||
role: agent.role,
|
||||
model: 'agent/' + agent.id,
|
||||
port: 18790 + index,
|
||||
}));
|
||||
log.success(`All ${config.agents.length} agents enabled`);
|
||||
} else {
|
||||
const selectedAgents = await promptSelect(
|
||||
'Select agents to enable:',
|
||||
[
|
||||
{ name: 'Steward only (minimal)', value: ['steward'] },
|
||||
{ name: 'Core triad (Steward, Alpha, Beta)', value: ['steward', 'alpha', 'beta'] },
|
||||
{ name: 'Full collective', value: availableAgents.map(a => a.id) },
|
||||
]
|
||||
);
|
||||
|
||||
config.agents = selectedAgents.map((id, index) => {
|
||||
const agent = availableAgents.find(a => a.id === id);
|
||||
return {
|
||||
id: agent.id,
|
||||
name: agent.name,
|
||||
role: agent.role,
|
||||
model: 'agent/' + agent.id,
|
||||
port: 18790 + index,
|
||||
};
|
||||
});
|
||||
log.success(`${config.agents.length} agents enabled`);
|
||||
}
|
||||
|
||||
// Step 6: Observability
|
||||
log.subheader('Step 5: Observability');
|
||||
|
||||
const enableLangfuse = await promptConfirm(
|
||||
'Enable Langfuse observability?',
|
||||
{ default: false }
|
||||
);
|
||||
|
||||
if (enableLangfuse) {
|
||||
config.observability = {
|
||||
langfuse: {
|
||||
enabled: true,
|
||||
host: 'http://localhost:3000',
|
||||
},
|
||||
};
|
||||
log.success('Langfuse observability enabled');
|
||||
}
|
||||
|
||||
// Step 7: Review and confirm
|
||||
log.subheader('Step 6: Review Configuration');
|
||||
|
||||
console.log(`
|
||||
Configuration Summary:
|
||||
Deployment Type: ${config.deployment.type}
|
||||
Primary Provider: ${primaryProvider}
|
||||
Default Model: ${config.model_routing.default}
|
||||
Agents: ${config.agents.length}
|
||||
Observability: ${enableLangfuse ? 'Enabled' : 'Disabled'}
|
||||
`);
|
||||
|
||||
const confirm = await promptConfirm('Proceed with this configuration?', { default: true });
|
||||
|
||||
if (!confirm) {
|
||||
log.warn('Setup cancelled');
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 8: Write configuration
|
||||
log.subheader('Writing Configuration');
|
||||
|
||||
// Initialize directories
|
||||
await configManager.initConfigDir();
|
||||
|
||||
// Save openclaw.json
|
||||
await configManager.save(config);
|
||||
|
||||
// Generate .env file
|
||||
await configManager.saveEnv();
|
||||
|
||||
log.success('Configuration files written');
|
||||
|
||||
// Step 9: Next steps
|
||||
log.subheader('Setup Complete!');
|
||||
|
||||
printNextSteps(config.deployment.type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-interactive initialization
|
||||
*/
|
||||
async function initNonInteractive(configManager, options) {
|
||||
log.info('Running non-interactive initialization...');
|
||||
|
||||
const config = configManager.createDefault();
|
||||
config.deployment = {
|
||||
type: options.type || 'docker',
|
||||
};
|
||||
|
||||
// Initialize directories
|
||||
await configManager.initConfigDir();
|
||||
|
||||
// Save configuration
|
||||
await configManager.save(config);
|
||||
|
||||
// Validate if not skipped
|
||||
if (!options.skipValidation) {
|
||||
const validation = configManager.validate(config);
|
||||
if (!validation.valid) {
|
||||
log.error('Configuration validation failed:');
|
||||
validation.errors.forEach(e => log.error(` - ${e}`));
|
||||
throw new Error('Configuration validation failed');
|
||||
}
|
||||
log.success('Configuration validated');
|
||||
}
|
||||
|
||||
log.success('Non-interactive initialization complete');
|
||||
}
|
||||
|
||||
/**
|
||||
* Print next steps
|
||||
*/
|
||||
function printNextSteps(deploymentType) {
|
||||
const steps = {
|
||||
docker: [
|
||||
'Start Docker services: docker compose up -d',
|
||||
'Verify services: docker compose ps',
|
||||
'Install Gateway: curl -fsSL https://openclaw.ai/install.sh | bash',
|
||||
'Initialize Gateway: openclaw onboard --install-daemon',
|
||||
],
|
||||
'bare-metal': [
|
||||
'Run system setup: sudo ./scripts/install/ubuntu-deps.sh',
|
||||
'Install application: npm install --production',
|
||||
'Start services: sudo systemctl start openclaw-gateway',
|
||||
],
|
||||
kubernetes: [
|
||||
'Apply manifests: kubectl apply -f deploy/k8s/',
|
||||
'Or use Helm: helm install openclaw ./charts/openclaw',
|
||||
],
|
||||
};
|
||||
|
||||
console.log(`
|
||||
${log.symbols.info} Next Steps:
|
||||
`);
|
||||
|
||||
const typeSteps = steps[deploymentType] || steps.docker;
|
||||
typeSteps.forEach((step, i) => {
|
||||
console.log(` ${i + 1}. ${step}`);
|
||||
});
|
||||
|
||||
console.log(`
|
||||
Documentation:
|
||||
- Local Deployment: docs/deployment/LOCAL_DEPLOYMENT.md
|
||||
- Setup Wizard: docs/deployment/SETUP_WIZARD.md
|
||||
- Configuration: docs/CONFIGURATION.md
|
||||
`);
|
||||
}
|
||||
|
||||
export default command;
|
||||
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Logs Command
|
||||
*
|
||||
* View and follow logs from OpenClaw services.
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import { execa } from 'execa';
|
||||
import log from '../lib/logger.js';
|
||||
import DeploymentManager from '../lib/deployment-manager.js';
|
||||
|
||||
const command = new Command('logs');
|
||||
|
||||
command
|
||||
.description('View logs from OpenClaw services')
|
||||
.argument('[service]', 'Service name (gateway, litellm, postgres, redis, ollama, etc.)')
|
||||
.option('-t, --type <type>', 'Deployment type (auto-detect if not specified)')
|
||||
.option('-f, --follow', 'Follow log output (tail -f mode)')
|
||||
.option('-n, --tail <lines>', 'Number of lines to show', '100')
|
||||
.option('--since <time>', 'Show logs since timestamp (e.g., 2024-01-01T00:00:00)')
|
||||
.option('--until <time>', 'Show logs until timestamp')
|
||||
.option('--timestamps', 'Show timestamps in logs')
|
||||
.option('--level <level>', 'Filter by log level (error, warn, info, debug)')
|
||||
.option('--grep <pattern>', 'Filter logs by pattern')
|
||||
.option('--json', 'Output as JSON (if supported)')
|
||||
.action(async (service, options) => {
|
||||
await handleLogs(service, options);
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle logs command
|
||||
*/
|
||||
async function handleLogs(service, options) {
|
||||
const manager = new DeploymentManager({
|
||||
rootDir: process.cwd(),
|
||||
deploymentType: options.type,
|
||||
});
|
||||
|
||||
try {
|
||||
// Determine deployment type and get logs
|
||||
const deployer = manager.deployer;
|
||||
|
||||
if (deployer.logs) {
|
||||
// Use deployer's logs method
|
||||
await deployer.logs({
|
||||
services: service ? [service] : [],
|
||||
follow: options.follow,
|
||||
tail: options.tail,
|
||||
timestamps: options.timestamps,
|
||||
});
|
||||
} else {
|
||||
// Fallback to direct log viewing
|
||||
await viewLogsDirect(service, options);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Failed to view logs: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* View logs directly from files or system
|
||||
*/
|
||||
async function viewLogsDirect(service, options) {
|
||||
const logDir = '/var/log/openclaw';
|
||||
const fs = await import('fs-extra');
|
||||
|
||||
// Check if log directory exists
|
||||
if (await fs.pathExists(logDir)) {
|
||||
const logFile = service
|
||||
? `${logDir}/${service}.log`
|
||||
: `${logDir}/openclaw.log`;
|
||||
|
||||
if (await fs.pathExists(logFile)) {
|
||||
if (options.follow) {
|
||||
// Follow mode using tail -f
|
||||
const args = ['-f', logFile];
|
||||
if (options.tail !== 'all') {
|
||||
args.unshift('-n', options.tail);
|
||||
}
|
||||
await execa('tail', args, { stdio: 'inherit' });
|
||||
} else {
|
||||
// Read file directly
|
||||
const content = await fs.readFile(logFile, 'utf-8');
|
||||
console.log(content);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Try journalctl for systemd services
|
||||
if (service) {
|
||||
try {
|
||||
const args = ['--no-pager'];
|
||||
|
||||
if (options.follow) {
|
||||
args.push('-f');
|
||||
}
|
||||
|
||||
if (options.tail && options.tail !== 'all') {
|
||||
args.push('-n', options.tail);
|
||||
}
|
||||
|
||||
if (options.since) {
|
||||
args.push('--since', options.since);
|
||||
}
|
||||
|
||||
args.push('-u', `openclaw-${service}`);
|
||||
|
||||
await execa('journalctl', args, { stdio: 'inherit' });
|
||||
return;
|
||||
} catch {
|
||||
// journalctl not available
|
||||
}
|
||||
}
|
||||
|
||||
log.error(`No logs found for service: ${service}`);
|
||||
console.log(`
|
||||
Available log sources:
|
||||
- Docker: openclaw logs --type docker
|
||||
- Systemd: Check /var/log/openclaw/
|
||||
- Kubernetes: openclaw logs --type kubernetes
|
||||
`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter logs by level
|
||||
*/
|
||||
function filterByLevel(content, level) {
|
||||
if (!level) return content;
|
||||
|
||||
const levelPatterns = {
|
||||
error: /ERROR|error|Error|\[E\]/i,
|
||||
warn: /WARN|warn|Warn|WARNING|warning|\[W\]/i,
|
||||
info: /INFO|info|Info|\[I\]/i,
|
||||
debug: /DEBUG|debug|Debug|\[D\]/i,
|
||||
};
|
||||
|
||||
const pattern = levelPatterns[level];
|
||||
if (!pattern) return content;
|
||||
|
||||
return content.split('\n')
|
||||
.filter(line => pattern.test(line))
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Grep logs by pattern
|
||||
*/
|
||||
function grepLogs(content, pattern) {
|
||||
if (!pattern) return content;
|
||||
|
||||
const regex = new RegExp(pattern, 'i');
|
||||
return content.split('\n')
|
||||
.filter(line => regex.test(line))
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
export default command;
|
||||
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* Status Command
|
||||
*
|
||||
* Check deployment status and display service health.
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import log from '../lib/logger.js';
|
||||
import DeploymentManager from '../lib/deployment-manager.js';
|
||||
import HealthChecker from '../lib/health-checker.js';
|
||||
|
||||
const command = new Command('status');
|
||||
|
||||
command
|
||||
.description('Check deployment status')
|
||||
.option('-t, --type <type>', 'Deployment type (auto-detect if not specified)')
|
||||
.option('--services', 'Show service status only')
|
||||
.option('--agents', 'Show agent status only')
|
||||
.option('--resources', 'Show resource usage')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action(async (options) => {
|
||||
await handleStatus(options);
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle status command
|
||||
*/
|
||||
async function handleStatus(options) {
|
||||
const manager = new DeploymentManager({
|
||||
rootDir: process.cwd(),
|
||||
deploymentType: options.type,
|
||||
});
|
||||
|
||||
try {
|
||||
// Get deployment status
|
||||
const status = await manager.status();
|
||||
const health = await manager.healthCheck();
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify({ status, health }, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
// Print status based on options
|
||||
if (options.services) {
|
||||
printServiceStatus(status);
|
||||
} else if (options.agents) {
|
||||
printAgentStatus(status);
|
||||
} else if (options.resources) {
|
||||
printResourceStatus(status);
|
||||
} else {
|
||||
printFullStatus(status, health);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Failed to get status: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print full status
|
||||
*/
|
||||
function printFullStatus(status, health) {
|
||||
log.section('OpenClaw Status');
|
||||
|
||||
// Overall status
|
||||
const overallHealthy = health?.healthy || false;
|
||||
const statusLine = overallHealthy
|
||||
? chalk.green('● HEALTHY')
|
||||
: chalk.red('● UNHEALTHY');
|
||||
|
||||
console.log(`\n Status: ${statusLine}`);
|
||||
console.log(` Last Check: ${health?.timestamp || new Date().toISOString()}`);
|
||||
|
||||
// Service status
|
||||
printServiceStatus(status);
|
||||
|
||||
// Health check summary
|
||||
if (health?.checks) {
|
||||
console.log('\n');
|
||||
log.subheader('Health Checks');
|
||||
|
||||
for (const [service, check] of Object.entries(health.checks)) {
|
||||
const symbol = check.healthy ? chalk.green('✓') : chalk.red('✗');
|
||||
console.log(` ${symbol} ${service.toUpperCase()}`);
|
||||
|
||||
if (!check.healthy && check.error) {
|
||||
console.log(` Error: ${check.error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print service status
|
||||
*/
|
||||
function printServiceStatus(status) {
|
||||
log.subheader('Services');
|
||||
|
||||
const services = [];
|
||||
|
||||
// Docker containers
|
||||
if (status.containers) {
|
||||
for (const container of status.containers) {
|
||||
services.push({
|
||||
name: container.Names?.replace('openclaw-', '') || container.name,
|
||||
status: container.State || container.status,
|
||||
healthy: container.State?.includes('running') || container.status?.includes('running'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Kubernetes pods
|
||||
if (status.pods) {
|
||||
for (const pod of status.pods) {
|
||||
services.push({
|
||||
name: pod.name.replace('openclaw-', ''),
|
||||
status: pod.status,
|
||||
healthy: pod.ready,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Systemd services
|
||||
if (Array.isArray(status) && status.length > 0) {
|
||||
for (const service of status) {
|
||||
services.push({
|
||||
name: service.name,
|
||||
status: service.status,
|
||||
healthy: service.active,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Print service table
|
||||
if (services.length === 0) {
|
||||
console.log(' No services found');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(' ┌─────────────────┬─────────────────┬──────────┐');
|
||||
console.log(' │ Service │ Status │ Health │');
|
||||
console.log(' ├─────────────────┼─────────────────┼──────────┤');
|
||||
|
||||
for (const service of services) {
|
||||
const name = service.name.padEnd(15);
|
||||
const statusText = (service.status || 'unknown').substring(0, 15).padEnd(15);
|
||||
const health = service.healthy
|
||||
? chalk.green('Healthy'.padEnd(8))
|
||||
: chalk.red('Unhealthy'.padEnd(8));
|
||||
|
||||
console.log(` │ ${name} │ ${statusText} │ ${health} │`);
|
||||
}
|
||||
|
||||
console.log(' └─────────────────┴─────────────────┴──────────┘');
|
||||
}
|
||||
|
||||
/**
|
||||
* Print agent status
|
||||
*/
|
||||
function printAgentStatus(status) {
|
||||
log.subheader('Agents');
|
||||
|
||||
const agents = [];
|
||||
|
||||
// From health checks
|
||||
if (status.checks?.agents?.agents) {
|
||||
for (const agent of status.checks.agents.agents) {
|
||||
agents.push({
|
||||
name: agent.agent_name || agent,
|
||||
status: 'active',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (agents.length === 0) {
|
||||
console.log(' No agents registered');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(' ┌─────────────────┬─────────────────┐');
|
||||
console.log(' │ Agent │ Status │');
|
||||
console.log(' ├─────────────────┼─────────────────┤');
|
||||
|
||||
for (const agent of agents) {
|
||||
const name = agent.name.padEnd(15);
|
||||
const statusText = chalk.green(agent.status.padEnd(15));
|
||||
console.log(` │ ${name} │ ${statusText} │`);
|
||||
}
|
||||
|
||||
console.log(' └─────────────────┴─────────────────┘');
|
||||
}
|
||||
|
||||
/**
|
||||
* Print resource status
|
||||
*/
|
||||
function printResourceStatus(status) {
|
||||
log.subheader('Resource Usage');
|
||||
|
||||
// Docker stats
|
||||
if (status.containers) {
|
||||
console.log('\n Container Resources:');
|
||||
console.log(' ┌─────────────────┬───────────────┬───────────────┐');
|
||||
console.log(' │ Container │ CPU │ Memory │');
|
||||
console.log(' ├─────────────────┼───────────────┼───────────────┤');
|
||||
|
||||
for (const container of status.containers) {
|
||||
// Note: Actual CPU/Memory would require docker stats
|
||||
console.log(` │ ${(container.Names || 'unknown').padEnd(15)} │ N/A │ N/A │`);
|
||||
}
|
||||
|
||||
console.log(' └─────────────────┴───────────────┴───────────────┘');
|
||||
}
|
||||
|
||||
// Kubernetes resources
|
||||
if (status.pods) {
|
||||
console.log('\n Pod Resources:');
|
||||
console.log(' ┌─────────────────┬───────────────┬───────────────┐');
|
||||
console.log(' │ Pod │ CPU │ Memory │');
|
||||
console.log(' ├─────────────────┼───────────────┼───────────────┤');
|
||||
|
||||
for (const pod of status.pods) {
|
||||
console.log(` │ ${pod.name.substring(0, 15).padEnd(15)} │ N/A │ N/A │`);
|
||||
}
|
||||
|
||||
console.log(' └─────────────────┴───────────────┴───────────────┘');
|
||||
}
|
||||
|
||||
console.log('\n Note: Real-time resource usage requires running containers/pods');
|
||||
}
|
||||
|
||||
export default command;
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Stop Command
|
||||
*
|
||||
* Stop OpenClaw deployment gracefully.
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import log from '../lib/logger.js';
|
||||
import DeploymentManager from '../lib/deployment-manager.js';
|
||||
import { promptConfirm } from '../lib/prompts.js';
|
||||
|
||||
const command = new Command('stop');
|
||||
|
||||
command
|
||||
.description('Stop OpenClaw deployment')
|
||||
.option('-t, --type <type>', 'Deployment type (auto-detect if not specified)')
|
||||
.option('-f, --force', 'Force stop (kill containers/processes)')
|
||||
.option('--volumes', 'Remove volumes (Docker only)')
|
||||
.option('--backup', 'Create backup before stopping')
|
||||
.option('-y, --yes', 'Skip confirmation prompt')
|
||||
.action(async (options) => {
|
||||
await handleStop(options);
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle stop command
|
||||
*/
|
||||
async function handleStop(options) {
|
||||
log.section('Stopping OpenClaw');
|
||||
|
||||
// Confirm stop
|
||||
if (!options.yes) {
|
||||
const confirmed = await promptConfirm(
|
||||
'Are you sure you want to stop OpenClaw? This will stop all services.',
|
||||
{ default: false }
|
||||
);
|
||||
|
||||
if (!confirmed) {
|
||||
log.info('Stop cancelled');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Create backup if requested
|
||||
if (options.backup) {
|
||||
log.info('Creating backup before stopping...');
|
||||
try {
|
||||
const BackupManager = (await import('../lib/backup-manager.js')).default;
|
||||
const backupManager = new BackupManager();
|
||||
await backupManager.create({ type: 'incremental' });
|
||||
log.success('Backup created');
|
||||
} catch (error) {
|
||||
log.warn(`Backup failed: ${error.message}`);
|
||||
// Continue with stop even if backup fails
|
||||
}
|
||||
}
|
||||
|
||||
const manager = new DeploymentManager({
|
||||
rootDir: process.cwd(),
|
||||
deploymentType: options.type,
|
||||
});
|
||||
|
||||
try {
|
||||
log.info('Stopping services...');
|
||||
|
||||
const success = await manager.stop({
|
||||
removeVolumes: options.volumes,
|
||||
});
|
||||
|
||||
if (success) {
|
||||
log.success('OpenClaw stopped successfully');
|
||||
|
||||
// Show post-stop message
|
||||
printPostStopMessage(options);
|
||||
} else {
|
||||
log.error('Failed to stop OpenClaw');
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Failed to stop: ${error.message}`);
|
||||
log.debug(error.stack);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print post-stop message
|
||||
*/
|
||||
function printPostStopMessage(options) {
|
||||
console.log(`
|
||||
${log.symbols.info} OpenClaw has been stopped.
|
||||
|
||||
To start again:
|
||||
${options.type === 'docker' ? 'docker compose up -d' : 'openclaw deploy'}
|
||||
|
||||
${options.volumes ? `
|
||||
Note: Volumes were removed. Data may need to be restored from backup.` : `
|
||||
Data is preserved. To remove data as well, use --volumes flag.`}
|
||||
`);
|
||||
}
|
||||
|
||||
export default command;
|
||||
@@ -0,0 +1,303 @@
|
||||
/**
|
||||
* Update Command
|
||||
*
|
||||
* Check for and apply OpenClaw updates.
|
||||
*/
|
||||
|
||||
import { Command } from 'commander';
|
||||
import { execa } from 'execa';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import log from '../lib/logger.js';
|
||||
import { promptConfirm } from '../lib/prompts.js';
|
||||
|
||||
const command = new Command('update');
|
||||
|
||||
command
|
||||
.description('Update OpenClaw')
|
||||
.addCommand(new Command('check')
|
||||
.description('Check for available updates')
|
||||
.option('--json', 'Output as JSON')
|
||||
.action((options) => handleUpdateCheck(options))
|
||||
)
|
||||
.addCommand(new Command('apply')
|
||||
.description('Apply available updates')
|
||||
.option('--dry-run', 'Show what would be updated without applying')
|
||||
.option('--rollback', 'Rollback to previous version')
|
||||
.option('--confirm', 'Skip confirmation prompt')
|
||||
.action((options) => handleUpdateApply(options))
|
||||
)
|
||||
.addCommand(new Command('rollback')
|
||||
.description('Rollback to previous version')
|
||||
.option('--version <version>', 'Specific version to rollback to')
|
||||
.option('--confirm', 'Skip confirmation prompt')
|
||||
.action((options) => handleUpdateRollback(options))
|
||||
)
|
||||
.addCommand(new Command('history')
|
||||
.description('Show update history')
|
||||
.action(() => handleUpdateHistory())
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle update check
|
||||
*/
|
||||
async function handleUpdateCheck(options) {
|
||||
log.section('Checking for Updates');
|
||||
|
||||
try {
|
||||
const currentVersion = await getCurrentVersion();
|
||||
const latestVersion = await getLatestVersion();
|
||||
|
||||
const updateAvailable = compareVersions(currentVersion, latestVersion) < 0;
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify({
|
||||
currentVersion,
|
||||
latestVersion,
|
||||
updateAvailable,
|
||||
}, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`
|
||||
Current Version: ${currentVersion}
|
||||
Latest Version: ${latestVersion}
|
||||
`);
|
||||
|
||||
if (updateAvailable) {
|
||||
log.success('Update available!');
|
||||
console.log(`
|
||||
To apply the update, run:
|
||||
openclaw update apply
|
||||
`);
|
||||
} else {
|
||||
log.info('You are running the latest version');
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Failed to check for updates: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle update apply
|
||||
*/
|
||||
async function handleUpdateApply(options) {
|
||||
log.section('Applying Updates');
|
||||
|
||||
const currentVersion = await getCurrentVersion();
|
||||
const latestVersion = await getLatestVersion();
|
||||
|
||||
if (compareVersions(currentVersion, latestVersion) >= 0) {
|
||||
log.info('No updates available');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`
|
||||
Current Version: ${currentVersion}
|
||||
Updating to: ${latestVersion}
|
||||
`);
|
||||
|
||||
if (!options.confirm && !options.dryRun) {
|
||||
const confirmed = await promptConfirm(
|
||||
'Apply update?',
|
||||
{ default: true }
|
||||
);
|
||||
|
||||
if (!confirmed) {
|
||||
log.info('Update cancelled');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
log.info('Dry run - showing what would be updated:');
|
||||
console.log(`
|
||||
Would update from ${currentVersion} to ${latestVersion}
|
||||
|
||||
Files that would be updated:
|
||||
- CLI binaries
|
||||
- Deployment scripts
|
||||
- Configuration templates
|
||||
`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Create backup before update
|
||||
log.info('Creating backup before update...');
|
||||
const BackupManager = (await import('../lib/backup-manager.js')).default;
|
||||
const backupManager = new BackupManager();
|
||||
await backupManager.create({ type: 'incremental', verify: false });
|
||||
log.success('Backup created');
|
||||
|
||||
// Apply update
|
||||
log.info('Applying update...');
|
||||
|
||||
// Update npm dependencies
|
||||
await execa('npm', ['install', '--production'], {
|
||||
cwd: process.cwd(),
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
// Update version file
|
||||
await fs.writeFile(
|
||||
path.join(process.cwd(), 'VERSION'),
|
||||
`${latestVersion}\n`,
|
||||
'utf-8'
|
||||
);
|
||||
|
||||
log.success(`Updated to version ${latestVersion}`);
|
||||
|
||||
console.log(`
|
||||
${log.symbols.info} Update complete! Restart services to apply changes:
|
||||
|
||||
Docker: docker compose restart
|
||||
Bare Metal: sudo systemctl restart openclaw-gateway
|
||||
Kubernetes: kubectl rollout restart deployment/openclaw
|
||||
`);
|
||||
} catch (error) {
|
||||
log.error(`Update failed: ${error.message}`);
|
||||
log.info('You can rollback using: openclaw update rollback');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle update rollback
|
||||
*/
|
||||
async function handleUpdateRollback(options) {
|
||||
log.section('Rolling Back Update');
|
||||
|
||||
if (!options.confirm) {
|
||||
const confirmed = await promptConfirm(
|
||||
'Rollback to previous version? This may cause data loss.',
|
||||
{ default: false }
|
||||
);
|
||||
|
||||
if (!confirmed) {
|
||||
log.info('Rollback cancelled');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const BackupManager = (await import('../lib/backup-manager.js')).default;
|
||||
const backupManager = new BackupManager();
|
||||
|
||||
// Find latest backup
|
||||
const backups = await backupManager.list();
|
||||
|
||||
if (backups.length === 0) {
|
||||
log.error('No backups available for rollback');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const latestBackup = backups[0];
|
||||
log.info(`Found backup: ${latestBackup.name}`);
|
||||
|
||||
// Restore from backup
|
||||
await backupManager.restore(latestBackup.name, { confirm: true });
|
||||
|
||||
log.success('Rollback complete');
|
||||
} catch (error) {
|
||||
log.error(`Rollback failed: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle update history
|
||||
*/
|
||||
async function handleUpdateHistory() {
|
||||
log.section('Update History');
|
||||
|
||||
try {
|
||||
const historyFile = path.join(process.cwd(), '.update-history.json');
|
||||
|
||||
if (!await fs.pathExists(historyFile)) {
|
||||
console.log(' No update history available');
|
||||
return;
|
||||
}
|
||||
|
||||
const history = await fs.readFile(historyFile, 'utf-8');
|
||||
const updates = JSON.parse(history);
|
||||
|
||||
console.log(' ┌─────────────────────────────────────────────────────────────┐');
|
||||
console.log(' │ Date │ From │ To │ Status │');
|
||||
console.log(' ├─────────────────────────────────────────────────────────────┤');
|
||||
|
||||
for (const update of updates.slice(-10)) {
|
||||
const date = update.date.substring(0, 19).padEnd(19);
|
||||
const from = (update.from || 'unknown').padEnd(11);
|
||||
const to = (update.to || 'unknown').padEnd(11);
|
||||
const status = (update.success ? 'Success' : 'Failed').padEnd(9);
|
||||
console.log(` │ ${date} │ ${from} │ ${to} │ ${status} │`);
|
||||
}
|
||||
|
||||
console.log(' └─────────────────────────────────────────────────────────────┘');
|
||||
} catch (error) {
|
||||
log.error(`Failed to read update history: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current version
|
||||
*/
|
||||
async function getCurrentVersion() {
|
||||
try {
|
||||
// Try VERSION file first
|
||||
const versionFile = path.join(process.cwd(), 'VERSION');
|
||||
if (await fs.pathExists(versionFile)) {
|
||||
const content = await fs.readFile(versionFile, 'utf-8');
|
||||
return content.trim();
|
||||
}
|
||||
|
||||
// Try package.json
|
||||
const pkgFile = path.join(process.cwd(), 'package.json');
|
||||
if (await fs.pathExists(pkgFile)) {
|
||||
const pkg = JSON.parse(await fs.readFile(pkgFile, 'utf-8'));
|
||||
return pkg.version || 'unknown';
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
} catch {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get latest version
|
||||
*/
|
||||
async function getLatestVersion() {
|
||||
try {
|
||||
// In production, this would fetch from npm or GitHub
|
||||
// For now, return current version as placeholder
|
||||
const pkgFile = path.join(process.cwd(), 'package.json');
|
||||
if (await fs.pathExists(pkgFile)) {
|
||||
const pkg = JSON.parse(await fs.readFile(pkgFile, 'utf-8'));
|
||||
return pkg.version || '1.0.0';
|
||||
}
|
||||
return '1.0.0';
|
||||
} catch {
|
||||
return '1.0.0';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare version strings
|
||||
*/
|
||||
function compareVersions(v1, v2) {
|
||||
const parts1 = v1.replace(/[^\d.]/g, '').split('.').map(Number);
|
||||
const parts2 = v2.replace(/[^\d.]/g, '').split('.').map(Number);
|
||||
|
||||
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
||||
const a = parts1[i] || 0;
|
||||
const b = parts2[i] || 0;
|
||||
if (a !== b) return a - b;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
export default command;
|
||||
@@ -0,0 +1,724 @@
|
||||
/**
|
||||
* Backup Manager
|
||||
*
|
||||
* Manages OpenClaw backups including creation, restoration, listing,
|
||||
* and scheduling.
|
||||
*/
|
||||
|
||||
import { execa } from 'execa';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import log from './logger.js';
|
||||
|
||||
class BackupManager {
|
||||
constructor(options = {}) {
|
||||
this.rootDir = options.rootDir || process.cwd();
|
||||
this.backupDir = options.backupDir || path.join(process.env.HOME || '', '.openclaw', 'backups');
|
||||
this.tempDir = options.tempDir || '/tmp/openclaw-backup';
|
||||
this.retentionDays = options.retentionDays || 30;
|
||||
|
||||
// Database settings
|
||||
this.postgresHost = options.postgresHost || 'localhost';
|
||||
this.postgresPort = options.postgresPort || 5432;
|
||||
this.postgresUser = options.postgresUser || 'openclaw';
|
||||
this.postgresDb = options.postgresDb || 'openclaw';
|
||||
this.redisHost = options.redisHost || 'localhost';
|
||||
this.redisPort = options.redisPort || 6379;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize backup directories
|
||||
*/
|
||||
async init() {
|
||||
log.info('Initializing backup directories...');
|
||||
|
||||
await fs.ensureDir(this.backupDir);
|
||||
await fs.ensureDir(path.join(this.backupDir, 'postgresql'));
|
||||
await fs.ensureDir(path.join(this.backupDir, 'redis'));
|
||||
await fs.ensureDir(path.join(this.backupDir, 'workspace'));
|
||||
await fs.ensureDir(path.join(this.backupDir, 'agent-state'));
|
||||
await fs.ensureDir(path.join(this.backupDir, 'config'));
|
||||
await fs.ensureDir(this.tempDir);
|
||||
|
||||
log.success('Backup directories initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate backup name
|
||||
*/
|
||||
generateBackupName(type = 'incremental') {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('T').join('_');
|
||||
const dayOfWeek = new Date().getDay();
|
||||
|
||||
// Full backup on Sunday (0), incremental otherwise
|
||||
const backupType = type === 'auto'
|
||||
? (dayOfWeek === 0 ? 'full' : 'incremental')
|
||||
: type;
|
||||
|
||||
return `openclaw_${backupType}_${timestamp}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create backup
|
||||
*/
|
||||
async create(options = {}) {
|
||||
const {
|
||||
type = 'incremental',
|
||||
verify = false,
|
||||
compress = true,
|
||||
} = options;
|
||||
|
||||
log.section('Creating Backup');
|
||||
|
||||
await this.init();
|
||||
|
||||
const backupName = this.generateBackupName(type);
|
||||
log.info(`Creating ${type} backup: ${backupName}`);
|
||||
|
||||
const results = {
|
||||
name: backupName,
|
||||
type,
|
||||
timestamp: new Date().toISOString(),
|
||||
components: {},
|
||||
errors: [],
|
||||
};
|
||||
|
||||
// Backup PostgreSQL
|
||||
try {
|
||||
results.components.postgresql = await this.backupPostgres(backupName, compress);
|
||||
} catch (error) {
|
||||
results.errors.push({ component: 'postgresql', error: error.message });
|
||||
}
|
||||
|
||||
// Backup Redis
|
||||
try {
|
||||
results.components.redis = await this.backupRedis(backupName);
|
||||
} catch (error) {
|
||||
results.errors.push({ component: 'redis', error: error.message });
|
||||
}
|
||||
|
||||
// Backup workspace
|
||||
try {
|
||||
results.components.workspace = await this.backupWorkspace(backupName, compress);
|
||||
} catch (error) {
|
||||
results.errors.push({ component: 'workspace', error: error.message });
|
||||
}
|
||||
|
||||
// Backup agent state
|
||||
try {
|
||||
results.components.agentState = await this.backupAgentState(backupName, compress);
|
||||
} catch (error) {
|
||||
results.errors.push({ component: 'agent-state', error: error.message });
|
||||
}
|
||||
|
||||
// Backup configuration
|
||||
try {
|
||||
results.components.config = await this.backupConfig(backupName, compress);
|
||||
} catch (error) {
|
||||
results.errors.push({ component: 'config', error: error.message });
|
||||
}
|
||||
|
||||
// Generate checksums
|
||||
await this.generateChecksums(backupName);
|
||||
|
||||
// Verify if requested
|
||||
if (verify) {
|
||||
results.verification = await this.verify(backupName);
|
||||
}
|
||||
|
||||
results.success = results.errors.length === 0;
|
||||
|
||||
if (results.success) {
|
||||
log.success(`Backup created: ${backupName}`);
|
||||
} else {
|
||||
log.warn(`Backup completed with ${results.errors.length} error(s)`);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup PostgreSQL database
|
||||
*/
|
||||
async backupPostgres(backupName, compress = true) {
|
||||
log.info('Backing up PostgreSQL...');
|
||||
|
||||
const outputFile = path.join(this.backupDir, 'postgresql', `${backupName}_postgresql.sql`);
|
||||
|
||||
try {
|
||||
// Use pg_dump for backup
|
||||
const password = process.env.POSTGRES_PASSWORD || '';
|
||||
const env = { ...process.env, PGPASSWORD: password };
|
||||
|
||||
await execa('pg_dump', [
|
||||
'-h', this.postgresHost,
|
||||
'-p', this.postgresPort.toString(),
|
||||
'-U', this.postgresUser,
|
||||
'-d', this.postgresDb,
|
||||
'-F', 'c', // Custom format
|
||||
'-f', `${outputFile}.dump`,
|
||||
], { env });
|
||||
|
||||
if (compress) {
|
||||
await execa('gzip', ['-f', `${outputFile}.dump`]);
|
||||
}
|
||||
|
||||
// Also create SQL text backup
|
||||
await execa('pg_dump', [
|
||||
'-h', this.postgresHost,
|
||||
'-p', this.postgresPort.toString(),
|
||||
'-U', this.postgresUser,
|
||||
'-d', this.postgresDb,
|
||||
'-f', outputFile,
|
||||
], { env });
|
||||
|
||||
if (compress) {
|
||||
await execa('gzip', ['-f', outputFile]);
|
||||
}
|
||||
|
||||
log.success('PostgreSQL backup created');
|
||||
return {
|
||||
success: true,
|
||||
files: [
|
||||
`${outputFile}.dump.gz`,
|
||||
`${outputFile}.gz`,
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
log.warn(`PostgreSQL backup skipped: ${error.message}`);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup Redis database
|
||||
*/
|
||||
async backupRedis(backupName) {
|
||||
log.info('Backing up Redis...');
|
||||
|
||||
const outputFile = path.join(this.backupDir, 'redis', `${backupName}_redis.rdb`);
|
||||
|
||||
try {
|
||||
// Trigger BGSAVE
|
||||
await execa('redis-cli', [
|
||||
'-h', this.redisHost,
|
||||
'-p', this.redisPort.toString(),
|
||||
'BGSAVE',
|
||||
]);
|
||||
|
||||
log.info('Waiting for Redis BGSAVE to complete...');
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
|
||||
// Try to copy RDB file
|
||||
const rdbPaths = ['/var/lib/redis/dump.rdb', '/var/lib/redis/6379/dump.rdb'];
|
||||
|
||||
for (const rdbPath of rdbPaths) {
|
||||
if (await fs.pathExists(rdbPath)) {
|
||||
await fs.copy(rdbPath, outputFile);
|
||||
log.success('Redis backup created');
|
||||
return { success: true, files: [outputFile] };
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Use --rdb option
|
||||
await execa('redis-cli', [
|
||||
'-h', this.redisHost,
|
||||
'-p', this.redisPort.toString(),
|
||||
'--rdb', outputFile,
|
||||
]);
|
||||
|
||||
log.success('Redis backup created');
|
||||
return { success: true, files: [outputFile] };
|
||||
} catch (error) {
|
||||
log.warn(`Redis backup skipped: ${error.message}`);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup workspace
|
||||
*/
|
||||
async backupWorkspace(backupName, compress = true) {
|
||||
log.info('Backing up workspace...');
|
||||
|
||||
const outputFile = path.join(this.backupDir, 'workspace', `${backupName}_workspace.tar.gz`);
|
||||
|
||||
const excludePatterns = [
|
||||
'node_modules',
|
||||
'.git',
|
||||
'*.log',
|
||||
'*.tmp',
|
||||
'.next',
|
||||
'dist',
|
||||
'build',
|
||||
];
|
||||
|
||||
const tarArgs = ['-cf', outputFile];
|
||||
|
||||
for (const pattern of excludePatterns) {
|
||||
tarArgs.push('--exclude', pattern);
|
||||
}
|
||||
|
||||
tarArgs.push('-C', this.rootDir, '.');
|
||||
|
||||
try {
|
||||
await execa('tar', tarArgs);
|
||||
log.success('Workspace backup created');
|
||||
return { success: true, files: [outputFile] };
|
||||
} catch (error) {
|
||||
log.warn(`Workspace backup skipped: ${error.message}`);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup agent state
|
||||
*/
|
||||
async backupAgentState(backupName, compress = true) {
|
||||
log.info('Backing up agent state...');
|
||||
|
||||
const outputFile = path.join(this.backupDir, 'agent-state', `${backupName}_agent-state.tar.gz`);
|
||||
const homeDir = process.env.HOME || '';
|
||||
|
||||
const statePaths = [
|
||||
path.join(homeDir, '.openclaw', 'agents'),
|
||||
path.join(homeDir, '.openclaw', 'cron'),
|
||||
].filter(p => p && !p.includes('undefined'));
|
||||
|
||||
if (statePaths.length === 0) {
|
||||
log.info('No agent state paths found');
|
||||
return { success: true, files: [], skipped: true };
|
||||
}
|
||||
|
||||
try {
|
||||
await execa('tar', ['-czf', outputFile, ...statePaths]);
|
||||
log.success('Agent state backup created');
|
||||
return { success: true, files: [outputFile] };
|
||||
} catch (error) {
|
||||
log.warn(`Agent state backup skipped: ${error.message}`);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup configuration
|
||||
*/
|
||||
async backupConfig(backupName, compress = true) {
|
||||
log.info('Backing up configuration...');
|
||||
|
||||
const outputFile = path.join(this.backupDir, 'config', `${backupName}_config.tar.gz`);
|
||||
|
||||
const configFiles = [
|
||||
path.join(this.rootDir, 'openclaw.json'),
|
||||
path.join(this.rootDir, '.env'),
|
||||
path.join(this.rootDir, 'litellm_config.yaml'),
|
||||
path.join(process.env.HOME || '', '.openclaw', 'openclaw.json'),
|
||||
].filter(f => f && !f.includes('undefined'));
|
||||
|
||||
const existingFiles = [];
|
||||
for (const file of configFiles) {
|
||||
if (await fs.pathExists(file)) {
|
||||
existingFiles.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
if (existingFiles.length === 0) {
|
||||
log.info('No configuration files found');
|
||||
return { success: true, files: [], skipped: true };
|
||||
}
|
||||
|
||||
try {
|
||||
await execa('tar', ['-czf', outputFile, ...existingFiles]);
|
||||
log.success('Configuration backup created');
|
||||
return { success: true, files: [outputFile] };
|
||||
} catch (error) {
|
||||
log.warn(`Configuration backup skipped: ${error.message}`);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate checksums for backup
|
||||
*/
|
||||
async generateChecksums(backupName) {
|
||||
log.info('Generating checksums...');
|
||||
|
||||
const checksumFile = path.join(this.backupDir, `${backupName}_checksums.sha256`);
|
||||
|
||||
try {
|
||||
const { stdout } = await execa('find', [
|
||||
this.backupDir,
|
||||
'-maxdepth', '2',
|
||||
'-name', `${backupName}*`,
|
||||
'-type', 'f',
|
||||
'-exec', 'sha256sum', '{}', ';',
|
||||
]);
|
||||
|
||||
await fs.writeFile(checksumFile, stdout);
|
||||
log.success('Checksums generated');
|
||||
return checksumFile;
|
||||
} catch (error) {
|
||||
log.warn(`Checksum generation failed: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify backup
|
||||
*/
|
||||
async verify(backupName) {
|
||||
log.info(`Verifying backup: ${backupName}`);
|
||||
|
||||
const results = {
|
||||
valid: true,
|
||||
checks: [],
|
||||
};
|
||||
|
||||
// Check PostgreSQL backup
|
||||
const pgDump = path.join(this.backupDir, 'postgresql', `${backupName}_postgresql.sql.gz`);
|
||||
if (await fs.pathExists(pgDump)) {
|
||||
try {
|
||||
await execa('gzip', ['-t', pgDump]);
|
||||
results.checks.push({ name: 'postgresql', valid: true });
|
||||
} catch (error) {
|
||||
results.checks.push({ name: 'postgresql', valid: false, error: error.message });
|
||||
results.valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check workspace backup
|
||||
const workspaceTar = path.join(this.backupDir, 'workspace', `${backupName}_workspace.tar.gz`);
|
||||
if (await fs.pathExists(workspaceTar)) {
|
||||
try {
|
||||
await execa('tar', ['-tzf', workspaceTar]);
|
||||
results.checks.push({ name: 'workspace', valid: true });
|
||||
} catch (error) {
|
||||
results.checks.push({ name: 'workspace', valid: false, error: error.message });
|
||||
results.valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify checksums
|
||||
const checksumFile = path.join(this.backupDir, `${backupName}_checksums.sha256`);
|
||||
if (await fs.pathExists(checksumFile)) {
|
||||
try {
|
||||
await execa('sha256sum', ['-c', path.basename(checksumFile)], {
|
||||
cwd: path.dirname(checksumFile),
|
||||
});
|
||||
results.checks.push({ name: 'checksums', valid: true });
|
||||
} catch (error) {
|
||||
results.checks.push({ name: 'checksums', valid: false, error: error.message });
|
||||
results.valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (results.valid) {
|
||||
log.success('Backup verification passed');
|
||||
} else {
|
||||
log.warn('Backup verification failed');
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* List backups
|
||||
*/
|
||||
async list() {
|
||||
log.info('Listing backups...');
|
||||
|
||||
try {
|
||||
const files = await fs.readdir(this.backupDir);
|
||||
|
||||
const backups = [];
|
||||
const backupGroups = {};
|
||||
|
||||
for (const file of files) {
|
||||
if (file.startsWith('openclaw_') && !file.endsWith('.sha256')) {
|
||||
const stat = await fs.stat(path.join(this.backupDir, file));
|
||||
const type = file.includes('full') ? 'full' : 'incremental';
|
||||
const date = file.match(/_(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2})/)?.[1];
|
||||
|
||||
if (!backupGroups[date]) {
|
||||
backupGroups[date] = { name: file, type, date, size: stat.size, components: [] };
|
||||
}
|
||||
backupGroups[date].components.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [date, backup] of Object.entries(backupGroups)) {
|
||||
backups.push(backup);
|
||||
}
|
||||
|
||||
return backups.sort((a, b) => new Date(b.date) - new Date(a.date));
|
||||
} catch (error) {
|
||||
log.error(`Failed to list backups: ${error.message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore from backup
|
||||
*/
|
||||
async restore(backupName, options = {}) {
|
||||
const { components = ['all'], confirm = false } = options;
|
||||
|
||||
log.section('Restoring Backup');
|
||||
log.info(`Restoring from: ${backupName}`);
|
||||
|
||||
if (!confirm) {
|
||||
log.warn('This will overwrite existing data. Use --confirm to proceed.');
|
||||
return { success: false, error: 'Confirmation required' };
|
||||
}
|
||||
|
||||
// Stop services before restore
|
||||
log.info('Stopping services...');
|
||||
try {
|
||||
await execa('systemctl', ['stop', 'openclaw-gateway']);
|
||||
} catch {
|
||||
log.debug('Gateway service not running');
|
||||
}
|
||||
|
||||
const results = {
|
||||
name: backupName,
|
||||
timestamp: new Date().toISOString(),
|
||||
components: {},
|
||||
errors: [],
|
||||
};
|
||||
|
||||
// Restore PostgreSQL
|
||||
if (components.includes('all') || components.includes('postgresql')) {
|
||||
try {
|
||||
results.components.postgresql = await this.restorePostgres(backupName);
|
||||
} catch (error) {
|
||||
results.errors.push({ component: 'postgresql', error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// Restore Redis
|
||||
if (components.includes('all') || components.includes('redis')) {
|
||||
try {
|
||||
results.components.redis = await this.restoreRedis(backupName);
|
||||
} catch (error) {
|
||||
results.errors.push({ component: 'redis', error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// Restore workspace
|
||||
if (components.includes('all') || components.includes('workspace')) {
|
||||
try {
|
||||
results.components.workspace = await this.restoreWorkspace(backupName);
|
||||
} catch (error) {
|
||||
results.errors.push({ component: 'workspace', error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// Restore configuration
|
||||
if (components.includes('all') || components.includes('config')) {
|
||||
try {
|
||||
results.components.config = await this.restoreConfig(backupName);
|
||||
} catch (error) {
|
||||
results.errors.push({ component: 'config', error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// Start services after restore
|
||||
log.info('Starting services...');
|
||||
try {
|
||||
await execa('systemctl', ['start', 'openclaw-gateway']);
|
||||
} catch {
|
||||
log.debug('Could not start gateway service');
|
||||
}
|
||||
|
||||
results.success = results.errors.length === 0;
|
||||
|
||||
if (results.success) {
|
||||
log.success(`Backup restored: ${backupName}`);
|
||||
} else {
|
||||
log.warn(`Restore completed with ${results.errors.length} error(s)`);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore PostgreSQL
|
||||
*/
|
||||
async restorePostgres(backupName) {
|
||||
log.info('Restoring PostgreSQL...');
|
||||
|
||||
const dumpFile = path.join(this.backupDir, 'postgresql', `${backupName}_postgresql.sql.gz`);
|
||||
|
||||
if (!await fs.pathExists(dumpFile)) {
|
||||
return { success: false, error: 'Backup file not found' };
|
||||
}
|
||||
|
||||
try {
|
||||
const password = process.env.POSTGRES_PASSWORD || '';
|
||||
const env = { ...process.env, PGPASSWORD: password };
|
||||
|
||||
// Decompress and restore
|
||||
const gunzip = execa('gunzip', ['-c', dumpFile]);
|
||||
const psql = execa('psql', [
|
||||
'-h', this.postgresHost,
|
||||
'-p', this.postgresPort.toString(),
|
||||
'-U', this.postgresUser,
|
||||
'-d', this.postgresDb,
|
||||
], { env });
|
||||
|
||||
gunzip.stdout.pipe(psql.stdin);
|
||||
await psql;
|
||||
|
||||
log.success('PostgreSQL restored');
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
log.error(`PostgreSQL restore failed: ${error.message}`);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore Redis
|
||||
*/
|
||||
async restoreRedis(backupName) {
|
||||
log.info('Restoring Redis...');
|
||||
|
||||
const rdbFile = path.join(this.backupDir, 'redis', `${backupName}_redis.rdb`);
|
||||
|
||||
if (!await fs.pathExists(rdbFile)) {
|
||||
return { success: false, error: 'Backup file not found' };
|
||||
}
|
||||
|
||||
try {
|
||||
// Stop Redis
|
||||
await execa('systemctl', ['stop', 'redis-server']);
|
||||
|
||||
// Copy RDB file
|
||||
const redisDir = '/var/lib/redis';
|
||||
await fs.copy(rdbFile, path.join(redisDir, 'dump.rdb'));
|
||||
await execa('chown', ['redis:redis', path.join(redisDir, 'dump.rdb')]);
|
||||
|
||||
// Start Redis
|
||||
await execa('systemctl', ['start', 'redis-server']);
|
||||
|
||||
log.success('Redis restored');
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
log.error(`Redis restore failed: ${error.message}`);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore workspace
|
||||
*/
|
||||
async restoreWorkspace(backupName) {
|
||||
log.info('Restoring workspace...');
|
||||
|
||||
const tarFile = path.join(this.backupDir, 'workspace', `${backupName}_workspace.tar.gz`);
|
||||
|
||||
if (!await fs.pathExists(tarFile)) {
|
||||
return { success: false, error: 'Backup file not found' };
|
||||
}
|
||||
|
||||
try {
|
||||
await execa('tar', ['-xzf', tarFile, '-C', this.rootDir]);
|
||||
log.success('Workspace restored');
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
log.error(`Workspace restore failed: ${error.message}`);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore configuration
|
||||
*/
|
||||
async restoreConfig(backupName) {
|
||||
log.info('Restoring configuration...');
|
||||
|
||||
const tarFile = path.join(this.backupDir, 'config', `${backupName}_config.tar.gz`);
|
||||
|
||||
if (!await fs.pathExists(tarFile)) {
|
||||
return { success: false, error: 'Backup file not found' };
|
||||
}
|
||||
|
||||
try {
|
||||
await execa('tar', ['-xzf', tarFile, '-C', this.rootDir]);
|
||||
log.success('Configuration restored');
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
log.error(`Configuration restore failed: ${error.message}`);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete backup
|
||||
*/
|
||||
async delete(backupName) {
|
||||
log.info(`Deleting backup: ${backupName}`);
|
||||
|
||||
try {
|
||||
const files = await fs.readdir(this.backupDir, { recursive: true });
|
||||
|
||||
for (const file of files) {
|
||||
if (file.includes(backupName)) {
|
||||
const fullPath = path.join(this.backupDir, file);
|
||||
await fs.remove(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
log.success(`Backup deleted: ${backupName}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error(`Failed to delete backup: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate old backups
|
||||
*/
|
||||
async rotate() {
|
||||
log.info(`Rotating backups older than ${this.retentionDays} days...`);
|
||||
|
||||
try {
|
||||
let deleted = 0;
|
||||
|
||||
const files = await fs.readdir(this.backupDir, { recursive: true });
|
||||
|
||||
for (const file of files) {
|
||||
const fullPath = path.join(this.backupDir, file);
|
||||
const stat = await fs.stat(fullPath);
|
||||
|
||||
const age = Date.now() - stat.mtimeMs;
|
||||
const ageDays = age / (1000 * 60 * 60 * 24);
|
||||
|
||||
if (ageDays > this.retentionDays) {
|
||||
await fs.remove(fullPath);
|
||||
deleted++;
|
||||
}
|
||||
}
|
||||
|
||||
log.success(`Rotation complete: ${deleted} file(s) deleted`);
|
||||
return deleted;
|
||||
} catch (error) {
|
||||
log.error(`Rotation failed: ${error.message}`);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get backup schedule
|
||||
*/
|
||||
getSchedule() {
|
||||
return {
|
||||
full: 'Sunday at 2:00 AM',
|
||||
incremental: 'Daily at 2:00 AM',
|
||||
retention: `${this.retentionDays} days`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default BackupManager;
|
||||
@@ -0,0 +1,461 @@
|
||||
/**
|
||||
* Bare Metal Deployer
|
||||
*
|
||||
* Handles bare metal (non-Docker) deployment for OpenClaw.
|
||||
* Installs and configures services directly on the host system.
|
||||
*/
|
||||
|
||||
import { execa } from 'execa';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import log from './logger.js';
|
||||
|
||||
class BareMetalDeployer {
|
||||
constructor(options = {}) {
|
||||
this.rootDir = options.rootDir || process.cwd();
|
||||
this.installDir = options.installDir || '/opt/openclaw';
|
||||
this.configDir = options.configDir || '/etc/openclaw';
|
||||
this.dataDir = options.dataDir || '/var/lib/openclaw';
|
||||
this.logDir = options.logDir || '/var/log/openclaw';
|
||||
this.systemdDir = '/etc/systemd/system';
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect OS and package manager
|
||||
*/
|
||||
async detectOS() {
|
||||
try {
|
||||
const { stdout } = await execa('cat', ['/etc/os-release']);
|
||||
const lines = stdout.split('\n');
|
||||
const osInfo = {};
|
||||
|
||||
for (const line of lines) {
|
||||
const [key, value] = line.split('=');
|
||||
if (key && value) {
|
||||
osInfo[key] = value.replace(/"/g, '');
|
||||
}
|
||||
}
|
||||
|
||||
let packageManager = 'apt';
|
||||
let installCommand = 'apt-get install -y';
|
||||
|
||||
if (osInfo.ID_LIKE?.includes('rhel') || osInfo.ID === 'rhel' || osInfo.ID === 'centos' || osInfo.ID === 'fedora') {
|
||||
packageManager = 'dnf';
|
||||
installCommand = 'dnf install -y';
|
||||
} else if (osInfo.ID === 'arch' || osInfo.ID_LIKE?.includes('arch')) {
|
||||
packageManager = 'pacman';
|
||||
installCommand = 'pacman -S --noconfirm';
|
||||
}
|
||||
|
||||
return {
|
||||
id: osInfo.ID,
|
||||
name: osInfo.NAME,
|
||||
version: osInfo.VERSION_ID,
|
||||
packageManager,
|
||||
installCommand,
|
||||
};
|
||||
} catch (error) {
|
||||
log.warn(`Failed to detect OS: ${error.message}`);
|
||||
return {
|
||||
id: 'unknown',
|
||||
name: 'Unknown',
|
||||
packageManager: 'apt',
|
||||
installCommand: 'apt-get install -y',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if running as root
|
||||
*/
|
||||
checkRoot() {
|
||||
return process.geteuid && process.geteuid() === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check prerequisites
|
||||
*/
|
||||
async checkPrerequisites() {
|
||||
const checks = {
|
||||
root: this.checkRoot(),
|
||||
node: false,
|
||||
nodeVersion: null,
|
||||
postgres: false,
|
||||
redis: false,
|
||||
ollama: false,
|
||||
};
|
||||
|
||||
// Check Node.js
|
||||
try {
|
||||
const { stdout } = await execa('node', ['--version']);
|
||||
checks.node = true;
|
||||
checks.nodeVersion = stdout.trim();
|
||||
} catch {
|
||||
checks.node = false;
|
||||
}
|
||||
|
||||
// Check PostgreSQL
|
||||
try {
|
||||
await execa('pg_isready', ['-h', 'localhost']);
|
||||
checks.postgres = true;
|
||||
} catch {
|
||||
checks.postgres = false;
|
||||
}
|
||||
|
||||
// Check Redis
|
||||
try {
|
||||
const { stdout } = await execa('redis-cli', ['ping']);
|
||||
checks.redis = stdout.trim() === 'PONG';
|
||||
} catch {
|
||||
checks.redis = false;
|
||||
}
|
||||
|
||||
// Check Ollama
|
||||
try {
|
||||
await execa('curl', ['-s', 'http://localhost:11434/api/tags']);
|
||||
checks.ollama = true;
|
||||
} catch {
|
||||
checks.ollama = false;
|
||||
}
|
||||
|
||||
return checks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install system dependencies
|
||||
*/
|
||||
async installDependencies() {
|
||||
const osInfo = await this.detectOS();
|
||||
log.info(`Detected ${osInfo.name} - using ${osInfo.packageManager}`);
|
||||
|
||||
const packages = [
|
||||
'nodejs',
|
||||
'npm',
|
||||
'postgresql',
|
||||
'postgresql-contrib',
|
||||
'redis-server',
|
||||
'curl',
|
||||
'wget',
|
||||
'git',
|
||||
];
|
||||
|
||||
log.info('Installing system dependencies...');
|
||||
|
||||
try {
|
||||
// Update package lists
|
||||
await execa('apt-get', ['update'], { stdio: 'inherit' });
|
||||
|
||||
// Install packages
|
||||
await execa('apt-get', ['install', '-y', ...packages], { stdio: 'inherit' });
|
||||
|
||||
log.success('System dependencies installed');
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error(`Failed to install dependencies: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install Node.js specific version
|
||||
*/
|
||||
async installNodeJS(version = '20') {
|
||||
log.info(`Installing Node.js v${version}...`);
|
||||
|
||||
try {
|
||||
// Use NodeSource repository
|
||||
await execa('curl', ['-fsSL', `https://deb.nodesource.com/setup_${version}.x`, '-o', '/tmp/nodesource_setup.sh']);
|
||||
await execa('bash', ['/tmp/nodesource_setup.sh'], { stdio: 'inherit' });
|
||||
await execa('apt-get', ['install', '-y', 'nodejs'], { stdio: 'inherit' });
|
||||
|
||||
const { stdout } = await execa('node', ['--version']);
|
||||
log.success(`Node.js ${stdout.trim()} installed`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error(`Failed to install Node.js: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install Ollama
|
||||
*/
|
||||
async installOllama() {
|
||||
log.info('Installing Ollama...');
|
||||
|
||||
try {
|
||||
await execa('curl', ['-fsSL', 'https://ollama.com/install.sh', '-o', '/tmp/ollama_install.sh']);
|
||||
await execa('bash', ['/tmp/ollama_install.sh'], { stdio: 'inherit' });
|
||||
|
||||
log.success('Ollama installed');
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error(`Failed to install Ollama: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup PostgreSQL database
|
||||
*/
|
||||
async setupPostgres(options = {}) {
|
||||
const {
|
||||
user = 'openclaw',
|
||||
password = this.generatePassword(),
|
||||
database = 'openclaw'
|
||||
} = options;
|
||||
|
||||
log.info('Setting up PostgreSQL...');
|
||||
|
||||
try {
|
||||
// Start PostgreSQL service
|
||||
await execa('systemctl', ['start', 'postgresql']);
|
||||
await execa('systemctl', ['enable', 'postgresql']);
|
||||
|
||||
// Create user and database
|
||||
await execa('sudo', ['-u', 'postgres', 'psql', '-c', `CREATE USER ${user} WITH PASSWORD '${password}';`]);
|
||||
await execa('sudo', ['-u', 'postgres', 'psql', '-c', `CREATE DATABASE ${database} OWNER ${user};`]);
|
||||
await execa('sudo', ['-u', 'postgres', 'psql', '-c', `GRANT ALL PRIVILEGES ON DATABASE ${database} TO ${user};`]);
|
||||
|
||||
// Install pgvector extension
|
||||
await execa('sudo', ['-u', 'postgres', 'psql', '-d', database, '-c', 'CREATE EXTENSION IF NOT EXISTS vector;']);
|
||||
|
||||
log.success(`PostgreSQL setup complete: database=${database}, user=${user}`);
|
||||
return { user, password, database, host: 'localhost', port: 5432 };
|
||||
} catch (error) {
|
||||
log.error(`Failed to setup PostgreSQL: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup Redis
|
||||
*/
|
||||
async setupRedis() {
|
||||
log.info('Setting up Redis...');
|
||||
|
||||
try {
|
||||
// Start Redis service
|
||||
await execa('systemctl', ['start', 'redis-server']);
|
||||
await execa('systemctl', ['enable', 'redis-server']);
|
||||
|
||||
// Configure Redis
|
||||
const redisConfig = `
|
||||
# OpenClaw Redis Configuration
|
||||
bind 127.0.0.1
|
||||
port 6379
|
||||
protected-mode yes
|
||||
daemonize yes
|
||||
pidfile /var/run/redis/redis-server.pid
|
||||
logfile /var/log/redis/redis-server.log
|
||||
dir /var/lib/redis
|
||||
`;
|
||||
await fs.writeFile('/etc/redis/redis.conf', redisConfig);
|
||||
await execa('systemctl', ['restart', 'redis-server']);
|
||||
|
||||
log.success('Redis setup complete');
|
||||
return { host: 'localhost', port: 6379 };
|
||||
} catch (error) {
|
||||
log.error(`Failed to setup Redis: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create systemd service
|
||||
*/
|
||||
async createSystemService(name, config) {
|
||||
const serviceContent = `[Unit]
|
||||
Description=OpenClaw ${config.displayName || name}
|
||||
After=network.target ${config.after || 'postgresql.service redis-server.service'}
|
||||
${config.wants ? `Wants=${config.wants}` : ''}
|
||||
|
||||
[Service]
|
||||
Type=${config.type || 'simple'}
|
||||
User=${config.user || 'openclaw'}
|
||||
Group=${config.group || 'openclaw'}
|
||||
WorkingDirectory=${config.workingDir || this.installDir}
|
||||
Environment=${config.environment || 'NODE_ENV=production'}
|
||||
ExecStart=${config.execStart}
|
||||
ExecReload=${config.execReload || `/bin/kill -s HUP $MAINPID`}
|
||||
ExecStop=${config.execStop || '/bin/kill -s TERM $MAINPID'}
|
||||
Restart=${config.restart || 'always'}
|
||||
RestartSec=${config.restartSec || '10s'}
|
||||
StandardOutput=append:${this.logDir}/${name}.log
|
||||
StandardError=append:${this.logDir}/${name}.error.log
|
||||
|
||||
# Security hardening
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`;
|
||||
|
||||
const servicePath = path.join(this.systemdDir, `openclaw-${name}.service`);
|
||||
await fs.writeFile(servicePath, serviceContent);
|
||||
|
||||
// Reload systemd
|
||||
await execa('systemctl', ['daemon-reload']);
|
||||
|
||||
log.success(`Created systemd service: openclaw-${name}.service`);
|
||||
return servicePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install OpenClaw application
|
||||
*/
|
||||
async installApp() {
|
||||
log.info('Installing OpenClaw application...');
|
||||
|
||||
try {
|
||||
// Create directories
|
||||
await fs.ensureDir(this.installDir);
|
||||
await fs.ensureDir(this.configDir);
|
||||
await fs.ensureDir(this.dataDir);
|
||||
await fs.ensureDir(this.logDir);
|
||||
|
||||
// Copy application files
|
||||
const files = [
|
||||
'package.json',
|
||||
'openclaw.json',
|
||||
'litellm_config.yaml',
|
||||
'scripts/',
|
||||
'agents/',
|
||||
'dashboard/',
|
||||
];
|
||||
|
||||
for (const file of files) {
|
||||
const src = path.join(this.rootDir, file);
|
||||
const dest = path.join(this.installDir, file);
|
||||
|
||||
if (await fs.pathExists(src)) {
|
||||
await fs.copy(src, dest);
|
||||
}
|
||||
}
|
||||
|
||||
// Install npm dependencies
|
||||
log.info('Installing npm dependencies...');
|
||||
await execa('npm', ['install', '--production'], {
|
||||
cwd: this.installDir,
|
||||
stdio: 'inherit'
|
||||
});
|
||||
|
||||
// Create system user
|
||||
try {
|
||||
await execa('useradd', ['-r', '-s', '/bin/false', 'openclaw']);
|
||||
} catch {
|
||||
// User might already exist
|
||||
log.debug('User openclaw may already exist');
|
||||
}
|
||||
|
||||
// Set permissions
|
||||
await execa('chown', ['-R', 'openclaw:openclaw', this.installDir]);
|
||||
await execa('chown', ['-R', 'openclaw:openclaw', this.logDir]);
|
||||
|
||||
log.success('OpenClaw application installed');
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error(`Failed to install application: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start services
|
||||
*/
|
||||
async startServices(services = []) {
|
||||
const defaultServices = ['gateway', 'litellm', 'ollama'];
|
||||
const toStart = services.length > 0 ? services : defaultServices;
|
||||
|
||||
log.info('Starting OpenClaw services...');
|
||||
|
||||
for (const service of toStart) {
|
||||
try {
|
||||
await execa('systemctl', ['start', `openclaw-${service}`]);
|
||||
await execa('systemctl', ['enable', `openclaw-${service}`]);
|
||||
log.success(`Started openclaw-${service}`);
|
||||
} catch (error) {
|
||||
log.error(`Failed to start openclaw-${service}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop services
|
||||
*/
|
||||
async stopServices(services = []) {
|
||||
const defaultServices = ['gateway', 'litellm', 'ollama'];
|
||||
const toStop = services.length > 0 ? services : defaultServices;
|
||||
|
||||
log.info('Stopping OpenClaw services...');
|
||||
|
||||
for (const service of toStop) {
|
||||
try {
|
||||
await execa('systemctl', ['stop', `openclaw-${service}`]);
|
||||
log.success(`Stopped openclaw-${service}`);
|
||||
} catch (error) {
|
||||
log.error(`Failed to stop openclaw-${service}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get service status
|
||||
*/
|
||||
async status() {
|
||||
const services = ['gateway', 'litellm', 'ollama', 'postgres', 'redis'];
|
||||
const results = [];
|
||||
|
||||
for (const service of services) {
|
||||
try {
|
||||
const { stdout } = await execa('systemctl', ['is-active', `openclaw-${service}`]);
|
||||
results.push({
|
||||
name: service,
|
||||
active: stdout.trim() === 'active',
|
||||
status: stdout.trim(),
|
||||
});
|
||||
} catch {
|
||||
results.push({
|
||||
name: service,
|
||||
active: false,
|
||||
status: 'inactive',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate secure password
|
||||
*/
|
||||
generatePassword(length = 24) {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Health check
|
||||
*/
|
||||
async healthCheck() {
|
||||
const status = await this.status();
|
||||
const allHealthy = status.every(s => s.active);
|
||||
|
||||
return {
|
||||
healthy: allHealthy,
|
||||
services: status,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default BareMetalDeployer;
|
||||
@@ -0,0 +1,646 @@
|
||||
/**
|
||||
* Cloud Deployer
|
||||
*
|
||||
* Handles cloud deployment for OpenClaw using Terraform.
|
||||
* Supports AWS, GCP, and Azure.
|
||||
*/
|
||||
|
||||
import { execa } from 'execa';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import log from './logger.js';
|
||||
|
||||
class CloudDeployer {
|
||||
constructor(options = {}) {
|
||||
this.rootDir = options.rootDir || process.cwd();
|
||||
this.provider = options.provider || 'aws';
|
||||
this.terraformDir = options.terraformDir || path.join(this.rootDir, 'terraform', this.provider);
|
||||
this.stateDir = options.stateDir || path.join(this.rootDir, '.terraform', this.provider);
|
||||
this.projectName = options.projectName || 'openclaw';
|
||||
this.region = options.region || this.getDefaultRegion();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default region based on provider
|
||||
*/
|
||||
getDefaultRegion() {
|
||||
const defaults = {
|
||||
aws: 'us-east-1',
|
||||
gcp: 'us-central1',
|
||||
azure: 'eastus',
|
||||
};
|
||||
return defaults[this.provider] || 'us-east-1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Terraform is available
|
||||
*/
|
||||
async checkTerraform() {
|
||||
try {
|
||||
await execa('terraform', ['version']);
|
||||
return { available: true };
|
||||
} catch (error) {
|
||||
return {
|
||||
available: false,
|
||||
error: 'Terraform is not installed or not in PATH'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check cloud provider CLI
|
||||
*/
|
||||
async checkProviderCLI() {
|
||||
const cliCommands = {
|
||||
aws: { command: 'aws', args: ['--version'] },
|
||||
gcp: { command: 'gcloud', args: ['--version'] },
|
||||
azure: { command: 'az', args: ['--version'] },
|
||||
};
|
||||
|
||||
const cli = cliCommands[this.provider];
|
||||
if (!cli) {
|
||||
return { available: false, error: `Unknown provider: ${this.provider}` };
|
||||
}
|
||||
|
||||
try {
|
||||
await execa(cli.command, cli.args);
|
||||
return { available: true };
|
||||
} catch (error) {
|
||||
return {
|
||||
available: false,
|
||||
error: `${cli.command} CLI is not installed or not in PATH`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate with cloud provider
|
||||
*/
|
||||
async authenticate() {
|
||||
log.info(`Authenticating with ${this.provider.toUpperCase()}...`);
|
||||
|
||||
const authChecks = {
|
||||
aws: async () => {
|
||||
try {
|
||||
await execa('aws', ['sts', 'get-caller-identity']);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
gcp: async () => {
|
||||
try {
|
||||
await execa('gcloud', ['auth', 'list']);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
azure: async () => {
|
||||
try {
|
||||
await execa('az', ['account', 'show']);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const isAuthenticated = await authChecks[this.provider]?.();
|
||||
|
||||
if (!isAuthenticated) {
|
||||
log.warn(`Not authenticated with ${this.provider.toUpperCase()}`);
|
||||
log.info('Run the following command to authenticate:');
|
||||
|
||||
const authCommands = {
|
||||
aws: 'aws configure',
|
||||
gcp: 'gcloud auth login',
|
||||
azure: 'az login',
|
||||
};
|
||||
|
||||
console.log(` ${authCommands[this.provider]}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
log.success(`Authenticated with ${this.provider.toUpperCase()}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Terraform
|
||||
*/
|
||||
async init() {
|
||||
log.info('Initializing Terraform...');
|
||||
|
||||
try {
|
||||
const terraform = await this.checkTerraform();
|
||||
if (!terraform.available) {
|
||||
throw new Error(terraform.error);
|
||||
}
|
||||
|
||||
// Ensure state directory exists
|
||||
await fs.ensureDir(this.stateDir);
|
||||
|
||||
await execa('terraform', ['init'], {
|
||||
cwd: this.terraformDir,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
log.success('Terraform initialized');
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error(`Terraform init failed: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate Terraform configuration
|
||||
*/
|
||||
async validate() {
|
||||
log.info('Validating Terraform configuration...');
|
||||
|
||||
try {
|
||||
const terraform = await this.checkTerraform();
|
||||
if (!terraform.available) {
|
||||
throw new Error(terraform.error);
|
||||
}
|
||||
|
||||
await execa('terraform', ['validate'], {
|
||||
cwd: this.terraformDir,
|
||||
});
|
||||
|
||||
log.success('Terraform configuration is valid');
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error(`Terraform validation failed: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plan Terraform deployment
|
||||
*/
|
||||
async plan(options = {}) {
|
||||
const { out = 'tfplan', destroy = false } = options;
|
||||
|
||||
log.info(`Creating Terraform ${destroy ? 'destroy' : 'deployment'} plan...`);
|
||||
|
||||
try {
|
||||
const terraform = await this.checkTerraform();
|
||||
if (!terraform.available) {
|
||||
throw new Error(terraform.error);
|
||||
}
|
||||
|
||||
const args = ['plan', '-out', out];
|
||||
|
||||
if (destroy) {
|
||||
args.push('-destroy');
|
||||
}
|
||||
|
||||
args.push(
|
||||
'-var', `project_name=${this.projectName}`,
|
||||
'-var', `region=${this.region}`
|
||||
);
|
||||
|
||||
await execa('terraform', args, {
|
||||
cwd: this.terraformDir,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
log.success('Terraform plan created');
|
||||
return out;
|
||||
} catch (error) {
|
||||
log.error(`Terraform plan failed: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply Terraform deployment
|
||||
*/
|
||||
async apply(options = {}) {
|
||||
const { autoApprove = false, planFile = null } = options;
|
||||
|
||||
log.info('Applying Terraform deployment...');
|
||||
|
||||
try {
|
||||
const terraform = await this.checkTerraform();
|
||||
if (!terraform.available) {
|
||||
throw new Error(terraform.error);
|
||||
}
|
||||
|
||||
const args = ['apply'];
|
||||
|
||||
if (autoApprove) {
|
||||
args.push('-auto-approve');
|
||||
}
|
||||
|
||||
if (planFile) {
|
||||
args.push(planFile);
|
||||
}
|
||||
|
||||
await execa('terraform', args, {
|
||||
cwd: this.terraformDir,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
log.success('Terraform deployment applied');
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error(`Terraform apply failed: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy Terraform deployment
|
||||
*/
|
||||
async destroy(options = {}) {
|
||||
const { autoApprove = false } = options;
|
||||
|
||||
log.info('Destroying Terraform deployment...');
|
||||
|
||||
try {
|
||||
const terraform = await this.checkTerraform();
|
||||
if (!terraform.available) {
|
||||
throw new Error(terraform.error);
|
||||
}
|
||||
|
||||
const args = ['destroy'];
|
||||
|
||||
if (autoApprove) {
|
||||
args.push('-auto-approve');
|
||||
}
|
||||
|
||||
args.push(
|
||||
'-var', `project_name=${this.projectName}`,
|
||||
'-var', `region=${this.region}`
|
||||
);
|
||||
|
||||
await execa('terraform', args, {
|
||||
cwd: this.terraformDir,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
log.success('Terraform deployment destroyed');
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error(`Terraform destroy failed: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get deployment outputs
|
||||
*/
|
||||
async getOutputs() {
|
||||
try {
|
||||
const terraform = await this.checkTerraform();
|
||||
if (!terraform.available) {
|
||||
throw new Error(terraform.error);
|
||||
}
|
||||
|
||||
const { stdout } = await execa('terraform', ['output', '-json'], {
|
||||
cwd: this.terraformDir,
|
||||
});
|
||||
|
||||
return JSON.parse(stdout);
|
||||
} catch (error) {
|
||||
log.error(`Failed to get outputs: ${error.message}`);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get deployment status
|
||||
*/
|
||||
async status() {
|
||||
try {
|
||||
const outputs = await this.getOutputs();
|
||||
|
||||
const status = {
|
||||
provider: this.provider,
|
||||
projectName: this.projectName,
|
||||
region: this.region,
|
||||
resources: {},
|
||||
};
|
||||
|
||||
// Parse outputs based on provider
|
||||
if (this.provider === 'aws') {
|
||||
status.resources = {
|
||||
eksCluster: outputs.eks_cluster_endpoint?.value,
|
||||
rdsInstance: outputs.rds_endpoint?.value,
|
||||
elasticacheCluster: outputs.elasticache_endpoint?.value,
|
||||
loadBalancer: outputs.load_balancer_dns?.value,
|
||||
};
|
||||
} else if (this.provider === 'gcp') {
|
||||
status.resources = {
|
||||
gkeCluster: outputs.gke_cluster_endpoint?.value,
|
||||
cloudSqlInstance: outputs.cloud_sql_connection_name?.value,
|
||||
memorystoreInstance: outputs.memorystore_host?.value,
|
||||
loadBalancer: outputs.load_balancer_ip?.value,
|
||||
};
|
||||
} else if (this.provider === 'azure') {
|
||||
status.resources = {
|
||||
aksCluster: outputs.aks_fqdn?.value,
|
||||
cosmosDB: outputs.cosmosdb_endpoint?.value,
|
||||
redisCache: outputs.redis_host?.value,
|
||||
loadBalancer: outputs.load_balancer_ip?.value,
|
||||
};
|
||||
}
|
||||
|
||||
return status;
|
||||
} catch (error) {
|
||||
return {
|
||||
provider: this.provider,
|
||||
projectName: this.projectName,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Terraform configuration
|
||||
*/
|
||||
async generateConfig(options = {}) {
|
||||
const {
|
||||
instanceType,
|
||||
nodeCount,
|
||||
storageSize,
|
||||
enableMonitoring,
|
||||
enableBackup,
|
||||
} = options;
|
||||
|
||||
log.info('Generating Terraform configuration...');
|
||||
|
||||
// Create terraform directory structure
|
||||
const modulesDir = path.join(this.terraformDir, 'modules');
|
||||
await fs.ensureDir(modulesDir);
|
||||
await fs.ensureDir(path.join(modulesDir, 'kubernetes'));
|
||||
await fs.ensureDir(path.join(modulesDir, 'database'));
|
||||
await fs.ensureDir(path.join(modulesDir, 'cache'));
|
||||
|
||||
// Generate main.tf
|
||||
const mainTf = this.generateMainTf({ instanceType, nodeCount, storageSize, enableMonitoring, enableBackup });
|
||||
await fs.writeFile(path.join(this.terraformDir, 'main.tf'), mainTf);
|
||||
|
||||
// Generate variables.tf
|
||||
const variablesTf = this.generateVariablesTf();
|
||||
await fs.writeFile(path.join(this.terraformDir, 'variables.tf'), variablesTf);
|
||||
|
||||
// Generate outputs.tf
|
||||
const outputsTf = this.generateOutputsTf();
|
||||
await fs.writeFile(path.join(this.terraformDir, 'outputs.tf'), outputsTf);
|
||||
|
||||
log.success('Terraform configuration generated');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate main.tf for AWS
|
||||
*/
|
||||
generateMainTf(options = {}) {
|
||||
const { instanceType = 't3.medium', nodeCount = 2, storageSize = 100 } = options;
|
||||
|
||||
return `# OpenClaw AWS Terraform Configuration
|
||||
# Generated by openclaw CLI
|
||||
|
||||
terraform {
|
||||
required_version = ">= 1.0.0"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
kubernetes = {
|
||||
source = "hashicorp/kubernetes"
|
||||
version = "~> 2.0"
|
||||
}
|
||||
helm = {
|
||||
source = "hashicorp/helm"
|
||||
version = "~> 2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "aws" {
|
||||
region = var.region
|
||||
}
|
||||
|
||||
# VPC
|
||||
module "vpc" {
|
||||
source = "./modules/vpc"
|
||||
|
||||
cidr_block = "10.0.0.0/16"
|
||||
public_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
|
||||
private_subnets = ["10.0.10.0/24", "10.0.11.0/24", "10.0.12.0/24"]
|
||||
|
||||
tags = {
|
||||
Name = "\${var.project_name}-vpc"
|
||||
Project = var.project_name
|
||||
Environment = var.environment
|
||||
}
|
||||
}
|
||||
|
||||
# EKS Cluster
|
||||
module "eks" {
|
||||
source = "./modules/eks"
|
||||
|
||||
cluster_name = var.project_name
|
||||
cluster_version = "1.28"
|
||||
|
||||
vpc_id = module.vpc.vpc_id
|
||||
subnet_ids = module.vpc.private_subnets
|
||||
|
||||
node_groups = {
|
||||
default = {
|
||||
instance_types = ["${instanceType}"]
|
||||
capacity_type = "ON_DEMAND"
|
||||
desired_size = ${nodeCount}
|
||||
max_size = ${nodeCount + 2}
|
||||
min_size = ${nodeCount}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# RDS PostgreSQL
|
||||
module "rds" {
|
||||
source = "./modules/rds"
|
||||
|
||||
identifier = "\${var.project_name}-postgres"
|
||||
engine = "postgres"
|
||||
engine_version = "15"
|
||||
instance_class = "db.t3.medium"
|
||||
|
||||
allocated_storage = ${storageSize}
|
||||
max_allocated_storage = ${storageSize * 2}
|
||||
|
||||
db_name = "openclaw"
|
||||
username = "openclaw"
|
||||
|
||||
vpc_id = module.vpc.vpc_id
|
||||
subnet_ids = module.vpc.private_subnets
|
||||
|
||||
tags = {
|
||||
Project = var.project_name
|
||||
}
|
||||
}
|
||||
|
||||
# ElastiCache Redis
|
||||
module "elasticache" {
|
||||
source = "./modules/elasticache"
|
||||
|
||||
cluster_id = "\${var.project_name}-redis"
|
||||
engine = "redis"
|
||||
engine_version = "7.0"
|
||||
node_type = "cache.t3.medium"
|
||||
num_nodes = 1
|
||||
|
||||
vpc_id = module.vpc.vpc_id
|
||||
subnet_ids = module.vpc.private_subnets
|
||||
|
||||
tags = {
|
||||
Project = var.project_name
|
||||
}
|
||||
}
|
||||
|
||||
# ECR Repository
|
||||
resource "aws_ecr_repository" "openclaw" {
|
||||
name = var.project_name
|
||||
image_tag_mutability = "MUTABLE"
|
||||
|
||||
image_scanning_configuration {
|
||||
scan_on_push = true
|
||||
}
|
||||
|
||||
tags = {
|
||||
Project = var.project_name
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate variables.tf
|
||||
*/
|
||||
generateVariablesTf() {
|
||||
return `# OpenClaw Variables
|
||||
|
||||
variable "project_name" {
|
||||
description = "Project name used for resource naming"
|
||||
type = string
|
||||
default = "openclaw"
|
||||
}
|
||||
|
||||
variable "region" {
|
||||
description = "AWS region"
|
||||
type = string
|
||||
default = "us-east-1"
|
||||
}
|
||||
|
||||
variable "environment" {
|
||||
description = "Environment name"
|
||||
type = string
|
||||
default = "production"
|
||||
}
|
||||
|
||||
variable "instance_type" {
|
||||
description = "EC2 instance type for EKS nodes"
|
||||
type = string
|
||||
default = "t3.medium"
|
||||
}
|
||||
|
||||
variable "node_count" {
|
||||
description = "Number of EKS nodes"
|
||||
type = number
|
||||
default = 2
|
||||
}
|
||||
|
||||
variable "storage_size" {
|
||||
description = "RDS storage size in GB"
|
||||
type = number
|
||||
default = 100
|
||||
}
|
||||
|
||||
variable "enable_monitoring" {
|
||||
description = "Enable CloudWatch monitoring"
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "enable_backup" {
|
||||
description = "Enable automated backups"
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate outputs.tf
|
||||
*/
|
||||
generateOutputsTf() {
|
||||
return `# OpenClaw Outputs
|
||||
|
||||
output "eks_cluster_endpoint" {
|
||||
description = "EKS cluster endpoint"
|
||||
value = module.eks.cluster_endpoint
|
||||
}
|
||||
|
||||
output "eks_cluster_name" {
|
||||
description = "EKS cluster name"
|
||||
value = module.eks.cluster_name
|
||||
}
|
||||
|
||||
output "rds_endpoint" {
|
||||
description = "RDS endpoint"
|
||||
value = module.rds.endpoint
|
||||
}
|
||||
|
||||
output "elasticache_endpoint" {
|
||||
description = "ElastiCache endpoint"
|
||||
value = module.elasticache.endpoint
|
||||
}
|
||||
|
||||
output "ecr_repository_url" {
|
||||
description = "ECR repository URL"
|
||||
value = aws_ecr_repository.openclaw.repository_url
|
||||
}
|
||||
|
||||
output "vpc_id" {
|
||||
description = "VPC ID"
|
||||
value = module.vpc.vpc_id
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Health check
|
||||
*/
|
||||
async healthCheck() {
|
||||
try {
|
||||
const status = await this.status();
|
||||
|
||||
if (status.error) {
|
||||
return { healthy: false, error: status.error };
|
||||
}
|
||||
|
||||
// Check if all resources are available
|
||||
const allResourcesAvailable = Object.values(status.resources).every(r => r);
|
||||
|
||||
return {
|
||||
healthy: allResourcesAvailable,
|
||||
details: status,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
healthy: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default CloudDeployer;
|
||||
@@ -0,0 +1,363 @@
|
||||
/**
|
||||
* Configuration Manager
|
||||
*
|
||||
* Manages OpenClaw configuration files including openclaw.json, .env,
|
||||
* and deployment-specific configurations.
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import yaml from 'yaml';
|
||||
import dotenv from 'dotenv';
|
||||
import log from './logger.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Default paths
|
||||
const HOME_DIR = process.env.HOME || process.env.USERPROFILE || '';
|
||||
const DEFAULT_OPENCLAW_DIR = path.join(HOME_DIR, '.openclaw');
|
||||
const DEFAULT_WORKSPACE_DIR = path.join(DEFAULT_OPENCLAW_DIR, 'workspace');
|
||||
const DEFAULT_AGENTS_DIR = path.join(DEFAULT_OPENCLAW_DIR, 'agents');
|
||||
|
||||
// Configuration schema
|
||||
const configSchema = {
|
||||
version: { type: 'string', required: true },
|
||||
collective: {
|
||||
type: 'object',
|
||||
required: true,
|
||||
properties: {
|
||||
name: { type: 'string', required: true },
|
||||
description: { type: 'string', required: true },
|
||||
version: { type: 'string', required: true },
|
||||
},
|
||||
},
|
||||
models: { type: 'object', required: true },
|
||||
agents: { type: 'array', required: true },
|
||||
model_routing: { type: 'object', required: false },
|
||||
};
|
||||
|
||||
class ConfigManager {
|
||||
constructor(options = {}) {
|
||||
this.rootDir = options.rootDir || process.cwd();
|
||||
this.openclawDir = options.openclawDir || DEFAULT_OPENCLAW_DIR;
|
||||
this.configPath = options.configPath || path.join(this.rootDir, 'openclaw.json');
|
||||
this.envPath = options.envPath || path.join(this.rootDir, '.env');
|
||||
this.config = null;
|
||||
this.env = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load configuration from openclaw.json
|
||||
*/
|
||||
async load() {
|
||||
try {
|
||||
if (!await fs.pathExists(this.configPath)) {
|
||||
log.warn(`Configuration file not found: ${this.configPath}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const content = await fs.readFile(this.configPath, 'utf-8');
|
||||
this.config = JSON.parse(content);
|
||||
log.debug(`Loaded configuration from ${this.configPath}`);
|
||||
return this.config;
|
||||
} catch (error) {
|
||||
log.error(`Failed to load configuration: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save configuration to openclaw.json
|
||||
*/
|
||||
async save(config = null) {
|
||||
try {
|
||||
const configToSave = config || this.config;
|
||||
if (!configToSave) {
|
||||
throw new Error('No configuration to save');
|
||||
}
|
||||
|
||||
await fs.ensureDir(path.dirname(this.configPath));
|
||||
await fs.writeFile(this.configPath, JSON.stringify(configToSave, null, 2), 'utf-8');
|
||||
log.success(`Configuration saved to ${this.configPath}`);
|
||||
return configToSave;
|
||||
} catch (error) {
|
||||
log.error(`Failed to save configuration: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load environment variables from .env file
|
||||
*/
|
||||
async loadEnv() {
|
||||
try {
|
||||
if (!await fs.pathExists(this.envPath)) {
|
||||
log.debug(`Environment file not found: ${this.envPath}`);
|
||||
return {};
|
||||
}
|
||||
|
||||
const content = await fs.readFile(this.envPath, 'utf-8');
|
||||
this.env = dotenv.parse(content);
|
||||
|
||||
// Merge with process.env
|
||||
Object.entries(this.env).forEach(([key, value]) => {
|
||||
if (!process.env[key]) {
|
||||
process.env[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
log.debug(`Loaded environment from ${this.envPath}`);
|
||||
return this.env;
|
||||
} catch (error) {
|
||||
log.error(`Failed to load environment: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save environment variables to .env file
|
||||
*/
|
||||
async saveEnv(env = null) {
|
||||
try {
|
||||
const envToSave = env || this.env;
|
||||
|
||||
// Generate .env content
|
||||
let content = '# Heretek OpenClaw Environment Configuration\n';
|
||||
content += `# Generated on ${new Date().toISOString()}\n\n`;
|
||||
|
||||
// Group environment variables by category
|
||||
const categories = {
|
||||
'LiteLLM Gateway': ['LITELLM_MASTER_KEY', 'LITELLM_SALT_KEY', 'LITELLM_HOST'],
|
||||
'AI Provider API Keys': ['MINIMAX_API_KEY', 'ZAI_API_KEY', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'GOOGLE_API_KEY', 'OLLAMA_HOST'],
|
||||
'Database': ['POSTGRES_USER', 'POSTGRES_PASSWORD', 'POSTGRES_DB', 'DATABASE_URL', 'REDIS_URL'],
|
||||
'OpenClaw': ['OPENCLAW_DIR', 'OPENCLAW_WORKSPACE', 'DEPLOYMENT_TYPE'],
|
||||
'Observability': ['LANGFUSE_ENABLED', 'LANGFUSE_PUBLIC_KEY', 'LANGFUSE_SECRET_KEY', 'LANGFUSE_HOST'],
|
||||
};
|
||||
|
||||
for (const [category, keys] of Object.entries(categories)) {
|
||||
content += `# =============================================================================\n`;
|
||||
content += `# ${category}\n`;
|
||||
content += `# =============================================================================\n`;
|
||||
|
||||
for (const key of keys) {
|
||||
if (envToSave[key] !== undefined) {
|
||||
content += `${key}=${envToSave[key]}\n`;
|
||||
} else if (process.env[key]) {
|
||||
content += `${key}=${process.env[key]}\n`;
|
||||
}
|
||||
}
|
||||
content += '\n';
|
||||
}
|
||||
|
||||
await fs.writeFile(this.envPath, content, 'utf-8');
|
||||
log.success(`Environment saved to ${this.envPath}`);
|
||||
return envToSave;
|
||||
} catch (error) {
|
||||
log.error(`Failed to save environment: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate configuration against schema
|
||||
*/
|
||||
validate(config = null) {
|
||||
const configToValidate = config || this.config;
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
|
||||
if (!configToValidate) {
|
||||
errors.push('No configuration to validate');
|
||||
return { valid: false, errors, warnings };
|
||||
}
|
||||
|
||||
// Check required top-level fields
|
||||
for (const [field, schema] of Object.entries(configSchema)) {
|
||||
if (schema.required && !(field in configToValidate)) {
|
||||
errors.push(`Missing required field: ${field}`);
|
||||
} else if (field in configToValidate) {
|
||||
const value = configToValidate[field];
|
||||
|
||||
// Type checking
|
||||
if (schema.type === 'string' && typeof value !== 'string') {
|
||||
errors.push(`Field '${field}' must be a string`);
|
||||
} else if (schema.type === 'object' && (typeof value !== 'object' || value === null)) {
|
||||
errors.push(`Field '${field}' must be an object`);
|
||||
} else if (schema.type === 'array' && !Array.isArray(value)) {
|
||||
errors.push(`Field '${field}' must be an array`);
|
||||
}
|
||||
|
||||
// Nested property validation
|
||||
if (schema.type === 'object' && schema.properties && typeof value === 'object') {
|
||||
for (const [prop, propSchema] of Object.entries(schema.properties)) {
|
||||
if (propSchema.required && !(prop in value)) {
|
||||
errors.push(`Missing required property '${field}.${prop}'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate agents array
|
||||
if (Array.isArray(configToValidate.agents)) {
|
||||
const agentIds = new Set();
|
||||
for (const agent of configToValidate.agents) {
|
||||
if (!agent.id) {
|
||||
errors.push('Agent missing required "id" field');
|
||||
} else if (agentIds.has(agent.id)) {
|
||||
errors.push(`Duplicate agent ID: ${agent.id}`);
|
||||
} else {
|
||||
agentIds.add(agent.id);
|
||||
}
|
||||
|
||||
if (!agent.model) {
|
||||
warnings.push(`Agent '${agent.id}' missing model assignment`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate model routing
|
||||
if (configToValidate.model_routing) {
|
||||
if (!configToValidate.model_routing.default) {
|
||||
warnings.push('model_routing.default not set');
|
||||
}
|
||||
}
|
||||
|
||||
const valid = errors.length === 0;
|
||||
return { valid, errors, warnings };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific configuration value
|
||||
*/
|
||||
get(key, defaultValue = undefined) {
|
||||
if (!this.config) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const keys = key.split('.');
|
||||
let value = this.config;
|
||||
|
||||
for (const k of keys) {
|
||||
if (value === undefined || value === null) {
|
||||
return defaultValue;
|
||||
}
|
||||
value = value[k];
|
||||
}
|
||||
|
||||
return value !== undefined ? value : defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a specific configuration value
|
||||
*/
|
||||
set(key, value) {
|
||||
if (!this.config) {
|
||||
this.config = {};
|
||||
}
|
||||
|
||||
const keys = key.split('.');
|
||||
let current = this.config;
|
||||
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
const k = keys[i];
|
||||
if (!(k in current)) {
|
||||
current[k] = {};
|
||||
}
|
||||
current = current[k];
|
||||
}
|
||||
|
||||
current[keys[keys.length - 1]] = value;
|
||||
return this.config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get environment variable
|
||||
*/
|
||||
getEnv(key, defaultValue = undefined) {
|
||||
return this.env[key] || process.env[key] || defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set environment variable
|
||||
*/
|
||||
setEnv(key, value) {
|
||||
this.env[key] = value;
|
||||
process.env[key] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create default configuration
|
||||
*/
|
||||
createDefault() {
|
||||
return {
|
||||
version: '2.0.0',
|
||||
collective: {
|
||||
name: 'OpenClaw Collective',
|
||||
description: 'Self-improving autonomous agent collective',
|
||||
version: '2.0.0',
|
||||
},
|
||||
models: {
|
||||
providers: {
|
||||
ollama: {
|
||||
type: 'ollama',
|
||||
models: [
|
||||
{ id: 'ollama/llama2', name: 'Llama 2' },
|
||||
{ id: 'ollama/nomic-embed-text-v2-moe', name: 'Nomic Embed' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
agents: [
|
||||
{
|
||||
id: 'steward',
|
||||
name: 'Steward',
|
||||
role: 'Orchestrator',
|
||||
model: 'agent/steward',
|
||||
port: 18790,
|
||||
},
|
||||
],
|
||||
model_routing: {
|
||||
default: 'ollama/llama2',
|
||||
aliases: {
|
||||
failover: 'ollama/llama2',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize configuration directory
|
||||
*/
|
||||
async initConfigDir() {
|
||||
try {
|
||||
await fs.ensureDir(this.openclawDir);
|
||||
await fs.ensureDir(DEFAULT_WORKSPACE_DIR);
|
||||
await fs.ensureDir(DEFAULT_AGENTS_DIR);
|
||||
|
||||
log.success(`Initialized OpenClaw directory: ${this.openclawDir}`);
|
||||
return this.openclawDir;
|
||||
} catch (error) {
|
||||
log.error(`Failed to initialize config directory: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configuration paths
|
||||
*/
|
||||
getPaths() {
|
||||
return {
|
||||
rootDir: this.rootDir,
|
||||
openclawDir: this.openclawDir,
|
||||
workspaceDir: DEFAULT_WORKSPACE_DIR,
|
||||
agentsDir: DEFAULT_AGENTS_DIR,
|
||||
configPath: this.configPath,
|
||||
envPath: this.envPath,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default ConfigManager;
|
||||
@@ -0,0 +1,406 @@
|
||||
/**
|
||||
* Deployment Manager
|
||||
*
|
||||
* Unified deployment manager that abstracts different deployment types.
|
||||
* Supports Docker, Bare Metal, Kubernetes, and Cloud deployments.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import log from './logger.js';
|
||||
import DockerDeployer from './docker-deployer.js';
|
||||
import BareMetalDeployer from './baremetal-deployer.js';
|
||||
import KubernetesDeployer from './kubernetes-deployer.js';
|
||||
import CloudDeployer from './cloud-deployer.js';
|
||||
|
||||
// Deployment types
|
||||
export const DeploymentType = {
|
||||
DOCKER: 'docker',
|
||||
BARE_METAL: 'bare-metal',
|
||||
KUBERNETES: 'kubernetes',
|
||||
VM: 'vm',
|
||||
AWS: 'aws',
|
||||
GCP: 'gcp',
|
||||
AZURE: 'azure',
|
||||
};
|
||||
|
||||
// Deployment status
|
||||
export const DeploymentStatus = {
|
||||
NOT_DEPLOYED: 'not-deployed',
|
||||
DEPLOYING: 'deploying',
|
||||
RUNNING: 'running',
|
||||
STOPPED: 'stopped',
|
||||
ERROR: 'error',
|
||||
UNKNOWN: 'unknown',
|
||||
};
|
||||
|
||||
class DeploymentManager {
|
||||
constructor(options = {}) {
|
||||
this.rootDir = options.rootDir || process.cwd();
|
||||
this.configPath = options.configPath || path.join(this.rootDir, 'openclaw.json');
|
||||
this.deploymentType = options.deploymentType || DeploymentType.DOCKER;
|
||||
this.deployer = null;
|
||||
this.config = null;
|
||||
|
||||
this.initializeDeployer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the appropriate deployer based on deployment type
|
||||
*/
|
||||
initializeDeployer() {
|
||||
const commonOptions = { rootDir: this.rootDir };
|
||||
|
||||
switch (this.deploymentType) {
|
||||
case DeploymentType.DOCKER:
|
||||
this.deployer = new DockerDeployer(commonOptions);
|
||||
break;
|
||||
case DeploymentType.BARE_METAL:
|
||||
case DeploymentType.VM:
|
||||
this.deployer = new BareMetalDeployer(commonOptions);
|
||||
break;
|
||||
case DeploymentType.KUBERNETES:
|
||||
this.deployer = new KubernetesDeployer(commonOptions);
|
||||
break;
|
||||
case DeploymentType.AWS:
|
||||
case DeploymentType.GCP:
|
||||
case DeploymentType.AZURE:
|
||||
this.deployer = new CloudDeployer({
|
||||
...commonOptions,
|
||||
provider: this.deploymentType,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown deployment type: ${this.deploymentType}`);
|
||||
}
|
||||
|
||||
log.debug(`Initialized ${this.deploymentType} deployer`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set deployment type
|
||||
*/
|
||||
setDeploymentType(type) {
|
||||
if (this.deploymentType !== type) {
|
||||
this.deploymentType = type;
|
||||
this.initializeDeployer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load configuration
|
||||
*/
|
||||
async loadConfig() {
|
||||
try {
|
||||
if (!await fs.pathExists(this.configPath)) {
|
||||
log.warn(`Configuration file not found: ${this.configPath}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const content = await fs.readFile(this.configPath, 'utf-8');
|
||||
this.config = JSON.parse(content);
|
||||
return this.config;
|
||||
} catch (error) {
|
||||
log.error(`Failed to load configuration: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check prerequisites for deployment
|
||||
*/
|
||||
async checkPrerequisites() {
|
||||
log.info('Checking deployment prerequisites...');
|
||||
|
||||
const results = {
|
||||
config: false,
|
||||
deployer: false,
|
||||
environment: false,
|
||||
};
|
||||
|
||||
// Check configuration
|
||||
try {
|
||||
await this.loadConfig();
|
||||
results.config = this.config !== null;
|
||||
if (results.config) {
|
||||
log.success('Configuration loaded');
|
||||
} else {
|
||||
log.warn('Configuration not found');
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Configuration check failed: ${error.message}`);
|
||||
}
|
||||
|
||||
// Check deployer-specific prerequisites
|
||||
try {
|
||||
if (this.deployer instanceof DockerDeployer) {
|
||||
const dockerCheck = await this.deployer.checkDocker();
|
||||
const composeCheck = await this.deployer.checkDockerCompose();
|
||||
results.deployer = dockerCheck.available && composeCheck.available;
|
||||
|
||||
if (!dockerCheck.available) log.warn('Docker not available');
|
||||
if (!composeCheck.available) log.warn('Docker Compose not available');
|
||||
} else if (this.deployer instanceof BareMetalDeployer) {
|
||||
const checks = await this.deployer.checkPrerequisites();
|
||||
results.deployer = checks.node;
|
||||
log.info(`Node.js: ${checks.node ? checks.nodeVersion : 'not installed'}`);
|
||||
} else if (this.deployer instanceof KubernetesDeployer) {
|
||||
const kubectlCheck = await this.deployer.checkKubectl();
|
||||
const clusterCheck = await this.deployer.checkCluster();
|
||||
results.deployer = kubectlCheck.available && clusterCheck.connected;
|
||||
} else if (this.deployer instanceof CloudDeployer) {
|
||||
const terraformCheck = await this.deployer.checkTerraform();
|
||||
const providerCheck = await this.deployer.checkProviderCLI();
|
||||
results.deployer = terraformCheck.available && providerCheck.available;
|
||||
}
|
||||
|
||||
if (results.deployer) {
|
||||
log.success('Deployer prerequisites met');
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Deployer check failed: ${error.message}`);
|
||||
}
|
||||
|
||||
// Check environment
|
||||
results.environment = true;
|
||||
log.success('Environment check passed');
|
||||
|
||||
const allPassed = Object.values(results).every(r => r);
|
||||
return {
|
||||
passed: allPassed,
|
||||
results,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Deploy OpenClaw
|
||||
*/
|
||||
async deploy(options = {}) {
|
||||
log.section('Deploying OpenClaw');
|
||||
|
||||
// Check prerequisites
|
||||
const prereqs = await this.checkPrerequisites();
|
||||
if (!prereqs.passed) {
|
||||
log.error('Prerequisites check failed. Cannot proceed with deployment.');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.deployer instanceof DockerDeployer) {
|
||||
return await this.deployDocker(options);
|
||||
} else if (this.deployer instanceof BareMetalDeployer) {
|
||||
return await this.deployBareMetal(options);
|
||||
} else if (this.deployer instanceof KubernetesDeployer) {
|
||||
return await this.deployKubernetes(options);
|
||||
} else if (this.deployer instanceof CloudDeployer) {
|
||||
return await this.deployCloud(options);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Deployment failed: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deploy using Docker
|
||||
*/
|
||||
async deployDocker(options = {}) {
|
||||
const { build = false, forceRecreate = false, pull = true } = options;
|
||||
|
||||
log.info('Starting Docker deployment...');
|
||||
|
||||
// Pull images
|
||||
if (pull) {
|
||||
await this.deployer.pull();
|
||||
}
|
||||
|
||||
// Validate compose file
|
||||
const validation = await this.deployer.validateComposeFile();
|
||||
if (!validation.valid) {
|
||||
throw new Error(validation.error);
|
||||
}
|
||||
|
||||
// Start deployment
|
||||
const success = await this.deployer.up({ build, forceRecreate });
|
||||
|
||||
if (success) {
|
||||
log.success('Docker deployment complete');
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deploy to Bare Metal
|
||||
*/
|
||||
async deployBareMetal(options = {}) {
|
||||
const { installDeps = true, configureServices = true } = options;
|
||||
|
||||
log.info('Starting Bare Metal deployment...');
|
||||
|
||||
// Install system dependencies
|
||||
if (installDeps) {
|
||||
await this.deployer.installDependencies();
|
||||
}
|
||||
|
||||
// Setup PostgreSQL
|
||||
const pgConfig = await this.deployer.setupPostgres();
|
||||
|
||||
// Setup Redis
|
||||
await this.deployer.setupRedis();
|
||||
|
||||
// Install application
|
||||
await this.deployer.installApp();
|
||||
|
||||
// Create and start services
|
||||
if (configureServices) {
|
||||
await this.deployer.startServices();
|
||||
}
|
||||
|
||||
log.success('Bare Metal deployment complete');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deploy to Kubernetes
|
||||
*/
|
||||
async deployKubernetes(options = {}) {
|
||||
const {
|
||||
method = 'helm',
|
||||
values = [],
|
||||
set = {},
|
||||
overlay = 'default',
|
||||
} = options;
|
||||
|
||||
log.info('Starting Kubernetes deployment...');
|
||||
|
||||
// Create namespace
|
||||
await this.deployer.createNamespace();
|
||||
|
||||
if (method === 'helm') {
|
||||
await this.deployer.deployHelm({ values, set });
|
||||
} else if (method === 'kustomize') {
|
||||
await this.deployer.deployKustomize({ overlay });
|
||||
} else {
|
||||
throw new Error(`Unknown Kubernetes deployment method: ${method}`);
|
||||
}
|
||||
|
||||
log.success('Kubernetes deployment complete');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deploy to Cloud
|
||||
*/
|
||||
async deployCloud(options = {}) {
|
||||
const { autoApprove = false, generateConfig = true, configOptions = {} } = options;
|
||||
|
||||
log.info('Starting Cloud deployment...');
|
||||
|
||||
// Authenticate
|
||||
const authenticated = await this.deployer.authenticate();
|
||||
if (!authenticated) {
|
||||
throw new Error('Cloud authentication failed');
|
||||
}
|
||||
|
||||
// Generate config if needed
|
||||
if (generateConfig) {
|
||||
await this.deployer.generateConfig(configOptions);
|
||||
}
|
||||
|
||||
// Initialize Terraform
|
||||
await this.deployer.init();
|
||||
|
||||
// Validate
|
||||
const valid = await this.deployer.validate();
|
||||
if (!valid) {
|
||||
throw new Error('Terraform validation failed');
|
||||
}
|
||||
|
||||
// Plan and apply
|
||||
const planFile = await this.deployer.plan();
|
||||
await this.deployer.apply({ autoApprove, planFile });
|
||||
|
||||
log.success('Cloud deployment complete');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop deployment
|
||||
*/
|
||||
async stop(options = {}) {
|
||||
log.info('Stopping deployment...');
|
||||
|
||||
try {
|
||||
if (this.deployer instanceof DockerDeployer) {
|
||||
return await this.deployer.down(options);
|
||||
} else if (this.deployer instanceof BareMetalDeployer) {
|
||||
return await this.deployer.stopServices();
|
||||
} else if (this.deployer instanceof KubernetesDeployer) {
|
||||
return await this.deployer.delete();
|
||||
} else if (this.deployer instanceof CloudDeployer) {
|
||||
return await this.deployer.destroy(options);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Failed to stop deployment: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get deployment status
|
||||
*/
|
||||
async status() {
|
||||
try {
|
||||
if (this.deployer instanceof DockerDeployer) {
|
||||
return await this.deployer.status();
|
||||
} else if (this.deployer instanceof BareMetalDeployer) {
|
||||
return await this.deployer.status();
|
||||
} else if (this.deployer instanceof KubernetesDeployer) {
|
||||
return await this.deployer.status();
|
||||
} else if (this.deployer instanceof CloudDeployer) {
|
||||
return await this.deployer.status();
|
||||
}
|
||||
} catch (error) {
|
||||
return { error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* View logs
|
||||
*/
|
||||
async logs(options = {}) {
|
||||
if (this.deployer instanceof DockerDeployer) {
|
||||
return await this.deployer.logs(options);
|
||||
} else if (this.deployer instanceof KubernetesDeployer) {
|
||||
return await this.deployer.logs(options);
|
||||
} else {
|
||||
log.warn('Logs command not available for this deployment type');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run health check
|
||||
*/
|
||||
async healthCheck() {
|
||||
if (this.deployer) {
|
||||
return await this.deployer.healthCheck();
|
||||
}
|
||||
return { healthy: false, error: 'No deployer initialized' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get deployment info
|
||||
*/
|
||||
async info() {
|
||||
return {
|
||||
type: this.deploymentType,
|
||||
status: await this.status(),
|
||||
health: await this.healthCheck(),
|
||||
config: this.config,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default DeploymentManager;
|
||||
@@ -0,0 +1,363 @@
|
||||
/**
|
||||
* Docker Deployer
|
||||
*
|
||||
* Handles Docker Compose deployment for OpenClaw.
|
||||
*/
|
||||
|
||||
import { execa } from 'execa';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import log from './logger.js';
|
||||
|
||||
class DockerDeployer {
|
||||
constructor(options = {}) {
|
||||
this.composeFile = options.composeFile || 'docker-compose.yml';
|
||||
this.projectName = options.projectName || 'openclaw';
|
||||
this.rootDir = options.rootDir || process.cwd();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Docker is available
|
||||
*/
|
||||
async checkDocker() {
|
||||
try {
|
||||
await execa('docker', ['--version']);
|
||||
return { available: true };
|
||||
} catch (error) {
|
||||
return {
|
||||
available: false,
|
||||
error: 'Docker is not installed or not in PATH'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Docker Compose is available
|
||||
*/
|
||||
async checkDockerCompose() {
|
||||
try {
|
||||
// Try new compose command first
|
||||
await execa('docker', ['compose', 'version']);
|
||||
return { available: true, command: 'docker compose' };
|
||||
} catch {
|
||||
try {
|
||||
// Fallback to docker-compose
|
||||
await execa('docker-compose', ['--version']);
|
||||
return { available: true, command: 'docker-compose' };
|
||||
} catch (error) {
|
||||
return {
|
||||
available: false,
|
||||
error: 'Docker Compose is not installed'
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate Docker Compose file
|
||||
*/
|
||||
async validateComposeFile() {
|
||||
const composePath = path.join(this.rootDir, this.composeFile);
|
||||
|
||||
if (!await fs.pathExists(composePath)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Compose file not found: ${composePath}`
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const composeCmd = await this.checkDockerCompose();
|
||||
if (!composeCmd.available) {
|
||||
return { valid: false, error: composeCmd.error };
|
||||
}
|
||||
|
||||
await execa(composeCmd.command, ['-f', composePath, 'config'], {
|
||||
cwd: this.rootDir,
|
||||
});
|
||||
|
||||
return { valid: true };
|
||||
} catch (error) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Compose file validation failed: ${error.message}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull latest images
|
||||
*/
|
||||
async pull() {
|
||||
log.info('Pulling latest Docker images...');
|
||||
|
||||
try {
|
||||
const composeCmd = await this.checkDockerCompose();
|
||||
if (!composeCmd.available) {
|
||||
throw new Error(composeCmd.error);
|
||||
}
|
||||
|
||||
await execa(composeCmd.command, ['-f', this.composeFile, 'pull'], {
|
||||
cwd: this.rootDir,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
log.success('Docker images pulled successfully');
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error(`Failed to pull images: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start deployment
|
||||
*/
|
||||
async up(options = {}) {
|
||||
const { detach = true, build = false, forceRecreate = false } = options;
|
||||
|
||||
log.info('Starting Docker Compose deployment...');
|
||||
|
||||
try {
|
||||
const composeCmd = await this.checkDockerCompose();
|
||||
if (!composeCmd.available) {
|
||||
throw new Error(composeCmd.error);
|
||||
}
|
||||
|
||||
const args = ['-f', this.composeFile, 'up'];
|
||||
|
||||
if (detach) args.push('-d');
|
||||
if (build) args.push('--build');
|
||||
if (forceRecreate) args.push('--force-recreate');
|
||||
|
||||
await execa(composeCmd.command, args, {
|
||||
cwd: this.rootDir,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
log.success('Docker Compose deployment started');
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error(`Failed to start deployment: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop deployment
|
||||
*/
|
||||
async down(options = {}) {
|
||||
const { removeVolumes = false, removeOrphans = false } = options;
|
||||
|
||||
log.info('Stopping Docker Compose deployment...');
|
||||
|
||||
try {
|
||||
const composeCmd = await this.checkDockerCompose();
|
||||
if (!composeCmd.available) {
|
||||
throw new Error(composeCmd.error);
|
||||
}
|
||||
|
||||
const args = ['-f', this.composeFile, 'down'];
|
||||
|
||||
if (removeVolumes) args.push('-v');
|
||||
if (removeOrphans) args.push('--remove-orphans');
|
||||
|
||||
await execa(composeCmd.command, args, {
|
||||
cwd: this.rootDir,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
log.success('Docker Compose deployment stopped');
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error(`Failed to stop deployment: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart services
|
||||
*/
|
||||
async restart(services = []) {
|
||||
log.info('Restarting services...');
|
||||
|
||||
try {
|
||||
const composeCmd = await this.checkDockerCompose();
|
||||
if (!composeCmd.available) {
|
||||
throw new Error(composeCmd.error);
|
||||
}
|
||||
|
||||
const args = ['-f', this.composeFile, 'restart'];
|
||||
if (services.length > 0) {
|
||||
args.push(...services);
|
||||
}
|
||||
|
||||
await execa(composeCmd.command, args, {
|
||||
cwd: this.rootDir,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
log.success('Services restarted');
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error(`Failed to restart services: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get service status
|
||||
*/
|
||||
async status() {
|
||||
try {
|
||||
const composeCmd = await this.checkDockerCompose();
|
||||
if (!composeCmd.available) {
|
||||
throw new Error(composeCmd.error);
|
||||
}
|
||||
|
||||
const { stdout } = await execa(composeCmd.command, ['-f', this.composeFile, 'ps'], {
|
||||
cwd: this.rootDir,
|
||||
});
|
||||
|
||||
return {
|
||||
running: true,
|
||||
output: stdout,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
running: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* View logs
|
||||
*/
|
||||
async logs(options = {}) {
|
||||
const { services = [], follow = false, tail = 'all', timestamps = false } = options;
|
||||
|
||||
try {
|
||||
const composeCmd = await this.checkDockerCompose();
|
||||
if (!composeCmd.available) {
|
||||
throw new Error(composeCmd.error);
|
||||
}
|
||||
|
||||
const args = ['-f', this.composeFile, 'logs'];
|
||||
|
||||
if (follow) args.push('-f');
|
||||
if (tail !== 'all') args.push('--tail', tail.toString());
|
||||
if (timestamps) args.push('-t');
|
||||
|
||||
if (services.length > 0) {
|
||||
args.push(...services);
|
||||
}
|
||||
|
||||
const subprocess = execa(composeCmd.command, args, {
|
||||
cwd: this.rootDir,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
return subprocess;
|
||||
} catch (error) {
|
||||
log.error(`Failed to view logs: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute command in container
|
||||
*/
|
||||
async exec(service, command, options = {}) {
|
||||
try {
|
||||
const composeCmd = await this.checkDockerCompose();
|
||||
if (!composeCmd.available) {
|
||||
throw new Error(composeCmd.error);
|
||||
}
|
||||
|
||||
const args = ['-f', this.composeFile, 'exec'];
|
||||
|
||||
if (options.workdir) args.push('-w', options.workdir);
|
||||
if (options.user) args.push('-u', options.user);
|
||||
if (options.env) {
|
||||
for (const [key, value] of Object.entries(options.env)) {
|
||||
args.push('-e', `${key}=${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
args.push(service, ...command);
|
||||
|
||||
const { stdout } = await execa(composeCmd.command, args, {
|
||||
cwd: this.rootDir,
|
||||
});
|
||||
|
||||
return stdout;
|
||||
} catch (error) {
|
||||
log.error(`Failed to execute command in ${service}: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run database migration
|
||||
*/
|
||||
async migrate() {
|
||||
log.info('Running database migrations...');
|
||||
return this.exec('gateway', ['npm', 'run', 'migrate']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Health check
|
||||
*/
|
||||
async healthCheck() {
|
||||
const status = await this.status();
|
||||
|
||||
if (!status.running) {
|
||||
return { healthy: false, error: status.error };
|
||||
}
|
||||
|
||||
// Parse ps output to check container health
|
||||
const lines = status.output.split('\n').filter(l => l.trim());
|
||||
const unhealthy = lines.some(line =>
|
||||
line.includes('unhealthy') || line.includes('exited') || line.includes('dead')
|
||||
);
|
||||
|
||||
return {
|
||||
healthy: !unhealthy,
|
||||
details: status.output,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get container info
|
||||
*/
|
||||
async info() {
|
||||
try {
|
||||
const composeCmd = await this.checkDockerCompose();
|
||||
if (!composeCmd.available) {
|
||||
throw new Error(composeCmd.error);
|
||||
}
|
||||
|
||||
const { stdout } = await execa(composeCmd.command, [
|
||||
'-f', this.composeFile, 'ps', '--format', 'json'
|
||||
], {
|
||||
cwd: this.rootDir,
|
||||
});
|
||||
|
||||
const containers = stdout.split('\n').filter(l => l.trim()).map(l => JSON.parse(l));
|
||||
|
||||
return {
|
||||
containers,
|
||||
count: containers.length,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
containers: [],
|
||||
count: 0,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default DockerDeployer;
|
||||
@@ -0,0 +1,405 @@
|
||||
/**
|
||||
* Health Checker
|
||||
*
|
||||
* Comprehensive health checking for OpenClaw services.
|
||||
*/
|
||||
|
||||
import { execa } from 'execa';
|
||||
import axios from 'axios';
|
||||
import log from './logger.js';
|
||||
|
||||
class HealthChecker {
|
||||
constructor(options = {}) {
|
||||
this.timeout = options.timeout || 5000;
|
||||
this.gatewayUrl = options.gatewayUrl || 'http://localhost:18789';
|
||||
this.litellmUrl = options.litellmUrl || 'http://localhost:4000';
|
||||
this.postgresHost = options.postgresHost || 'localhost';
|
||||
this.postgresPort = options.postgresPort || 5432;
|
||||
this.redisHost = options.redisHost || 'localhost';
|
||||
this.redisPort = options.redisPort || 6379;
|
||||
this.ollamaUrl = options.ollamaUrl || 'http://localhost:11434';
|
||||
this.langfuseUrl = options.langfuseUrl || 'http://localhost:3000';
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all health checks
|
||||
*/
|
||||
async checkAll() {
|
||||
const checks = {
|
||||
gateway: await this.checkGateway(),
|
||||
litellm: await this.checkLiteLLM(),
|
||||
postgres: await this.checkPostgres(),
|
||||
redis: await this.checkRedis(),
|
||||
ollama: await this.checkOllama(),
|
||||
langfuse: await this.checkLangfuse(),
|
||||
agents: await this.checkAgents(),
|
||||
};
|
||||
|
||||
const allHealthy = Object.values(checks).every(c => c.healthy);
|
||||
|
||||
return {
|
||||
healthy: allHealthy,
|
||||
timestamp: new Date().toISOString(),
|
||||
checks,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check Gateway health
|
||||
*/
|
||||
async checkGateway() {
|
||||
try {
|
||||
const response = await axios.get(`${this.gatewayUrl}/health`, {
|
||||
timeout: this.timeout,
|
||||
});
|
||||
|
||||
return {
|
||||
healthy: true,
|
||||
status: response.status,
|
||||
responseTime: response.headers['x-response-time'] || 'unknown',
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
healthy: false,
|
||||
error: error.message,
|
||||
status: error.response?.status,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check LiteLLM health
|
||||
*/
|
||||
async checkLiteLLM() {
|
||||
try {
|
||||
const response = await axios.get(`${this.litellmUrl}/health`, {
|
||||
timeout: this.timeout,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${process.env.LITELLM_MASTER_KEY || 'heretek-master-key-change-me'}`,
|
||||
},
|
||||
});
|
||||
|
||||
// Get model count
|
||||
const modelsResponse = await axios.get(`${this.litellmUrl}/v1/models`, {
|
||||
timeout: this.timeout,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${process.env.LITELLM_MASTER_KEY || 'heretek-master-key-change-me'}`,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
healthy: true,
|
||||
status: response.status,
|
||||
modelCount: modelsResponse.data?.data?.length || 0,
|
||||
responseTime: response.headers['x-response-time'] || 'unknown',
|
||||
version: response.data?.version || 'unknown',
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
healthy: false,
|
||||
error: error.message,
|
||||
status: error.response?.status,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check PostgreSQL health
|
||||
*/
|
||||
async checkPostgres() {
|
||||
try {
|
||||
// Try pg_isready
|
||||
try {
|
||||
await execa('pg_isready', [
|
||||
'-h', this.postgresHost,
|
||||
'-p', this.postgresPort.toString(),
|
||||
], { timeout: this.timeout / 1000 });
|
||||
|
||||
// Check pgvector extension
|
||||
try {
|
||||
const { stdout } = await execa('psql', [
|
||||
'-h', this.postgresHost,
|
||||
'-p', this.postgresPort.toString(),
|
||||
'-U', process.env.POSTGRES_USER || 'openclaw',
|
||||
'-d', process.env.POSTGRES_DB || 'openclaw',
|
||||
'-t', '-c',
|
||||
"SELECT 1 FROM pg_extension WHERE extname='vector';",
|
||||
], { timeout: this.timeout / 1000 });
|
||||
|
||||
return {
|
||||
healthy: true,
|
||||
pgvector: stdout.trim() === '1',
|
||||
responseTime: 'unknown',
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
healthy: true,
|
||||
pgvector: false,
|
||||
responseTime: 'unknown',
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Fallback: try to connect via psql
|
||||
await execa('psql', [
|
||||
'-h', this.postgresHost,
|
||||
'-p', this.postgresPort.toString(),
|
||||
'-U', process.env.POSTGRES_USER || 'openclaw',
|
||||
'-d', process.env.POSTGRES_DB || 'openclaw',
|
||||
'-c', 'SELECT 1;',
|
||||
], { timeout: this.timeout / 1000 });
|
||||
|
||||
return {
|
||||
healthy: true,
|
||||
responseTime: 'unknown',
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
healthy: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check Redis health
|
||||
*/
|
||||
async checkRedis() {
|
||||
try {
|
||||
const { stdout } = await execa('redis-cli', [
|
||||
'-h', this.redisHost,
|
||||
'-p', this.redisPort.toString(),
|
||||
'ping',
|
||||
], { timeout: this.timeout / 1000 });
|
||||
|
||||
if (stdout.trim() === 'PONG') {
|
||||
// Get memory info
|
||||
try {
|
||||
const infoOutput = await execa('redis-cli', [
|
||||
'-h', this.redisHost,
|
||||
'-p', this.redisPort.toString(),
|
||||
'info', 'memory',
|
||||
], { timeout: this.timeout / 1000 });
|
||||
|
||||
const usedMemory = infoOutput.stdout.match(/used_memory_human:([^\r\n]+)/)?.[1]?.trim() || 'unknown';
|
||||
|
||||
return {
|
||||
healthy: true,
|
||||
memoryUsed: usedMemory,
|
||||
responseTime: 'unknown',
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
healthy: true,
|
||||
responseTime: 'unknown',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
healthy: false,
|
||||
error: 'Redis returned unexpected response',
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
healthy: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check Ollama health
|
||||
*/
|
||||
async checkOllama() {
|
||||
try {
|
||||
const response = await axios.get(`${this.ollamaUrl}/api/tags`, {
|
||||
timeout: this.timeout,
|
||||
});
|
||||
|
||||
const models = response.data?.models || [];
|
||||
|
||||
return {
|
||||
healthy: true,
|
||||
modelCount: models.length,
|
||||
models: models.map(m => m.name),
|
||||
responseTime: 'unknown',
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
healthy: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check Langfuse health
|
||||
*/
|
||||
async checkLangfuse() {
|
||||
try {
|
||||
const response = await axios.get(`${this.langfuseUrl}/api/health`, {
|
||||
timeout: this.timeout,
|
||||
});
|
||||
|
||||
return {
|
||||
healthy: true,
|
||||
status: response.status,
|
||||
responseTime: 'unknown',
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
healthy: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check registered agents
|
||||
*/
|
||||
async checkAgents() {
|
||||
try {
|
||||
const response = await axios.get(`${this.litellmUrl}/v1/agents`, {
|
||||
timeout: this.timeout,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${process.env.LITELLM_MASTER_KEY || 'heretek-master-key-change-me'}`,
|
||||
},
|
||||
});
|
||||
|
||||
const agents = response.data?.agents || [];
|
||||
|
||||
return {
|
||||
healthy: true,
|
||||
agentCount: agents.length,
|
||||
agents: agents.map(a => a.agent_name || a.name),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
healthy: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check specific service
|
||||
*/
|
||||
async checkService(service) {
|
||||
const checkers = {
|
||||
gateway: () => this.checkGateway(),
|
||||
litellm: () => this.checkLiteLLM(),
|
||||
postgres: () => this.checkPostgres(),
|
||||
redis: () => this.checkRedis(),
|
||||
ollama: () => this.checkOllama(),
|
||||
langfuse: () => this.checkLangfuse(),
|
||||
agents: () => this.checkAgents(),
|
||||
};
|
||||
|
||||
const checker = checkers[service];
|
||||
if (!checker) {
|
||||
return {
|
||||
healthy: false,
|
||||
error: `Unknown service: ${service}`,
|
||||
};
|
||||
}
|
||||
|
||||
return await checker();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate health report
|
||||
*/
|
||||
async generateReport() {
|
||||
const results = await this.checkAll();
|
||||
|
||||
const report = {
|
||||
summary: {
|
||||
healthy: results.healthy,
|
||||
timestamp: results.timestamp,
|
||||
totalChecks: Object.keys(results.checks).length,
|
||||
healthyChecks: Object.values(results.checks).filter(c => c.healthy).length,
|
||||
},
|
||||
details: results.checks,
|
||||
recommendations: [],
|
||||
};
|
||||
|
||||
// Generate recommendations
|
||||
for (const [service, check] of Object.entries(results.checks)) {
|
||||
if (!check.healthy) {
|
||||
report.recommendations.push({
|
||||
service,
|
||||
issue: check.error || 'Service unhealthy',
|
||||
action: this.getRecommendation(service),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recommendation for a service
|
||||
*/
|
||||
getRecommendation(service) {
|
||||
const recommendations = {
|
||||
gateway: 'Check if Gateway is running and port 18789 is accessible',
|
||||
litellm: 'Verify LiteLLM is running and API key is correct',
|
||||
postgres: 'Ensure PostgreSQL is running and credentials are correct',
|
||||
redis: 'Ensure Redis is running and accessible',
|
||||
ollama: 'Start Ollama service and verify it is accessible',
|
||||
langfuse: 'Check Langfuse deployment and configuration',
|
||||
agents: 'Verify agents are deployed and connected to Gateway',
|
||||
};
|
||||
|
||||
return recommendations[service] || 'Check service logs for more information';
|
||||
}
|
||||
|
||||
/**
|
||||
* Print health status
|
||||
*/
|
||||
printStatus(results) {
|
||||
console.log('\n');
|
||||
log.section('OpenClaw Health Status');
|
||||
|
||||
const overallStatus = results.healthy
|
||||
? chalk.green('HEALTHY')
|
||||
: chalk.red('UNHEALTHY');
|
||||
|
||||
console.log(`Overall Status: ${overallStatus}`);
|
||||
console.log(`Timestamp: ${results.timestamp}`);
|
||||
console.log('');
|
||||
|
||||
const chalk = (await import('chalk')).default;
|
||||
|
||||
for (const [service, check] of Object.entries(results.checks)) {
|
||||
const status = check.healthy
|
||||
? chalk.green('✓')
|
||||
: chalk.red('✗');
|
||||
|
||||
console.log(`${status} ${service.toUpperCase()}`);
|
||||
|
||||
if (check.healthy) {
|
||||
if (check.modelCount !== undefined) {
|
||||
console.log(` Models: ${check.modelCount}`);
|
||||
}
|
||||
if (check.agentCount !== undefined) {
|
||||
console.log(` Agents: ${check.agentCount}`);
|
||||
}
|
||||
if (check.pgvector !== undefined) {
|
||||
console.log(` pgvector: ${check.pgvector ? 'enabled' : 'disabled'}`);
|
||||
}
|
||||
if (check.memoryUsed !== undefined) {
|
||||
console.log(` Memory: ${check.memoryUsed}`);
|
||||
}
|
||||
} else {
|
||||
console.log(` Error: ${check.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
|
||||
export default HealthChecker;
|
||||
@@ -0,0 +1,439 @@
|
||||
/**
|
||||
* Kubernetes Deployer
|
||||
*
|
||||
* Handles Kubernetes deployment for OpenClaw using Helm or Kustomize.
|
||||
*/
|
||||
|
||||
import { execa } from 'execa';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import yaml from 'yaml';
|
||||
import log from './logger.js';
|
||||
|
||||
class KubernetesDeployer {
|
||||
constructor(options = {}) {
|
||||
this.rootDir = options.rootDir || process.cwd();
|
||||
this.namespace = options.namespace || 'openclaw';
|
||||
this.releaseName = options.releaseName || 'openclaw';
|
||||
this.chartDir = options.chartDir || path.join(this.rootDir, 'charts', 'openclaw');
|
||||
this.kustomizeDir = options.kustomizeDir || path.join(this.rootDir, 'deploy', 'k8s');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if kubectl is available
|
||||
*/
|
||||
async checkKubectl() {
|
||||
try {
|
||||
await execa('kubectl', ['version', '--client']);
|
||||
return { available: true };
|
||||
} catch (error) {
|
||||
return {
|
||||
available: false,
|
||||
error: 'kubectl is not installed or not in PATH'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check cluster connectivity
|
||||
*/
|
||||
async checkCluster() {
|
||||
try {
|
||||
const kubectl = await this.checkKubectl();
|
||||
if (!kubectl.available) {
|
||||
return { connected: false, error: kubectl.error };
|
||||
}
|
||||
|
||||
await execa('kubectl', ['cluster-info']);
|
||||
return { connected: true };
|
||||
} catch (error) {
|
||||
return {
|
||||
connected: false,
|
||||
error: `Cannot connect to cluster: ${error.message}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Helm is available
|
||||
*/
|
||||
async checkHelm() {
|
||||
try {
|
||||
await execa('helm', ['version']);
|
||||
return { available: true };
|
||||
} catch (error) {
|
||||
return {
|
||||
available: false,
|
||||
error: 'Helm is not installed or not in PATH'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Kustomize is available
|
||||
*/
|
||||
async checkKustomize() {
|
||||
try {
|
||||
await execa('kustomize', ['version']);
|
||||
return { available: true };
|
||||
} catch {
|
||||
try {
|
||||
// kubectl has built-in kustomize
|
||||
await execa('kubectl', ['version', '--client']);
|
||||
return { available: true, builtin: true };
|
||||
} catch (error) {
|
||||
return {
|
||||
available: false,
|
||||
error: 'Kustomize is not installed'
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create namespace if not exists
|
||||
*/
|
||||
async createNamespace() {
|
||||
log.info(`Ensuring namespace '${this.namespace}' exists...`);
|
||||
|
||||
try {
|
||||
await execa('kubectl', ['create', 'namespace', this.namespace, '--dry-run=client', '-o', 'yaml']);
|
||||
await execa('kubectl', ['apply', '-f', '-'], {
|
||||
input: `apiVersion: v1\nkind: Namespace\nmetadata:\n name: ${this.namespace}\n`,
|
||||
});
|
||||
log.success(`Namespace '${this.namespace}' ready`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error(`Failed to create namespace: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deploy using Helm
|
||||
*/
|
||||
async deployHelm(options = {}) {
|
||||
const {
|
||||
values = [],
|
||||
set = {},
|
||||
wait = true,
|
||||
timeout = '5m',
|
||||
upgrade = false,
|
||||
} = options;
|
||||
|
||||
const helm = await this.checkHelm();
|
||||
if (!helm.available) {
|
||||
throw new Error(helm.error);
|
||||
}
|
||||
|
||||
log.info(`Deploying with Helm to namespace '${this.namespace}'...`);
|
||||
|
||||
try {
|
||||
const args = upgrade ? ['upgrade', '--install'] : ['install'];
|
||||
|
||||
args.push(
|
||||
this.releaseName,
|
||||
this.chartDir,
|
||||
'--namespace', this.namespace,
|
||||
'--create-namespace'
|
||||
);
|
||||
|
||||
if (wait) args.push('--wait');
|
||||
if (timeout) args.push('--timeout', timeout);
|
||||
|
||||
for (const valueFile of values) {
|
||||
args.push('-f', valueFile);
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(set)) {
|
||||
args.push('--set', `${key}=${value}`);
|
||||
}
|
||||
|
||||
await execa('helm', args, { stdio: 'inherit' });
|
||||
|
||||
log.success(`Helm deployment complete: ${this.releaseName}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error(`Helm deployment failed: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deploy using Kustomize
|
||||
*/
|
||||
async deployKustomize(options = {}) {
|
||||
const { overlay = 'default', dryRun = false } = options;
|
||||
|
||||
const kustomize = await this.checkKustomize();
|
||||
if (!kustomize.available) {
|
||||
throw new Error(kustomize.error);
|
||||
}
|
||||
|
||||
log.info(`Deploying with Kustomize to namespace '${this.namespace}'...`);
|
||||
|
||||
try {
|
||||
const overlayDir = path.join(this.kustomizeDir, 'overlays', overlay);
|
||||
|
||||
if (!await fs.pathExists(overlayDir)) {
|
||||
throw new Error(`Overlay not found: ${overlayDir}`);
|
||||
}
|
||||
|
||||
let manifest;
|
||||
|
||||
if (kustomize.builtin) {
|
||||
const { stdout } = await execa('kubectl', ['kustomize', overlayDir]);
|
||||
manifest = stdout;
|
||||
} else {
|
||||
const { stdout } = await execa('kustomize', ['build', overlayDir]);
|
||||
manifest = stdout;
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
log.info('Dry run mode - manifest generated but not applied');
|
||||
console.log(manifest);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Apply to cluster
|
||||
await execa('kubectl', ['apply', '-f', '-'], {
|
||||
input: manifest,
|
||||
});
|
||||
|
||||
log.success('Kustomize deployment complete');
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error(`Kustomize deployment failed: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply raw manifests
|
||||
*/
|
||||
async applyManifests(manifestDir) {
|
||||
log.info(`Applying manifests from ${manifestDir}...`);
|
||||
|
||||
try {
|
||||
await execa('kubectl', ['apply', '-R', '-f', manifestDir], {
|
||||
cwd: this.rootDir,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
log.success('Manifests applied');
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error(`Failed to apply manifests: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get deployment status
|
||||
*/
|
||||
async status() {
|
||||
try {
|
||||
// Get pods
|
||||
const podsResult = await execa('kubectl', [
|
||||
'get', 'pods', '-n', this.namespace, '-o', 'json'
|
||||
]);
|
||||
const pods = JSON.parse(podsResult.stdout);
|
||||
|
||||
// Get deployments
|
||||
const deploymentsResult = await execa('kubectl', [
|
||||
'get', 'deployments', '-n', this.namespace, '-o', 'json'
|
||||
]);
|
||||
const deployments = JSON.parse(deploymentsResult.stdout);
|
||||
|
||||
// Get services
|
||||
const servicesResult = await execa('kubectl', [
|
||||
'get', 'services', '-n', this.namespace, '-o', 'json'
|
||||
]);
|
||||
const services = JSON.parse(servicesResult.stdout);
|
||||
|
||||
// Get statefulsets
|
||||
const statefulsetsResult = await execa('kubectl', [
|
||||
'get', 'statefulsets', '-n', this.namespace, '-o', 'json'
|
||||
]);
|
||||
const statefulsets = JSON.parse(statefulsetsResult.stdout);
|
||||
|
||||
return {
|
||||
namespace: this.namespace,
|
||||
pods: pods.items.map(p => ({
|
||||
name: p.metadata.name,
|
||||
status: p.status.phase,
|
||||
ready: p.status.containerStatuses?.every(cs => cs.ready) || false,
|
||||
restarts: p.status.containerStatuses?.reduce((acc, cs) => acc + cs.restartCount, 0) || 0,
|
||||
})),
|
||||
deployments: deployments.items.map(d => ({
|
||||
name: d.metadata.name,
|
||||
ready: d.status.readyReplicas || 0,
|
||||
replicas: d.status.replicas || 0,
|
||||
})),
|
||||
services: services.items.map(s => ({
|
||||
name: s.metadata.name,
|
||||
type: s.spec.type,
|
||||
clusterIP: s.spec.clusterIP,
|
||||
ports: s.spec.ports?.map(p => p.port) || [],
|
||||
})),
|
||||
statefulsets: statefulsets.items.map(s => ({
|
||||
name: s.metadata.name,
|
||||
ready: s.status.readyReplicas || 0,
|
||||
replicas: s.status.replicas || 0,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
namespace: this.namespace,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* View logs
|
||||
*/
|
||||
async logs(options = {}) {
|
||||
const {
|
||||
selector,
|
||||
container,
|
||||
follow = false,
|
||||
tail = 100,
|
||||
timestamps = false,
|
||||
} = options;
|
||||
|
||||
try {
|
||||
const args = ['logs', '-n', this.namespace];
|
||||
|
||||
if (selector) args.push('-l', selector);
|
||||
if (container) args.push('-c', container);
|
||||
if (follow) args.push('-f');
|
||||
if (tail) args.push('--tail', tail.toString());
|
||||
if (timestamps) args.push('--timestamps');
|
||||
|
||||
const subprocess = execa('kubectl', args, {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
return subprocess;
|
||||
} catch (error) {
|
||||
log.error(`Failed to view logs: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scale deployment
|
||||
*/
|
||||
async scale(deployment, replicas) {
|
||||
log.info(`Scaling ${deployment} to ${replicas} replicas...`);
|
||||
|
||||
try {
|
||||
await execa('kubectl', [
|
||||
'scale', 'deployment', deployment,
|
||||
'--replicas', replicas.toString(),
|
||||
'-n', this.namespace,
|
||||
]);
|
||||
|
||||
log.success(`Scaled ${deployment} to ${replicas} replicas`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error(`Failed to scale deployment: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback deployment
|
||||
*/
|
||||
async rollback(deployment) {
|
||||
log.info(`Rolling back ${deployment}...`);
|
||||
|
||||
try {
|
||||
await execa('kubectl', [
|
||||
'rollout', 'undo', 'deployment', deployment,
|
||||
'-n', this.namespace,
|
||||
]);
|
||||
|
||||
log.success(`Rolling back ${deployment}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error(`Failed to rollback: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete deployment
|
||||
*/
|
||||
async delete() {
|
||||
log.info(`Deleting OpenClaw from namespace '${this.namespace}'...`);
|
||||
|
||||
try {
|
||||
// If using Helm
|
||||
const helm = await this.checkHelm();
|
||||
if (helm.available) {
|
||||
await execa('helm', ['uninstall', this.releaseName, '-n', this.namespace], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
} else {
|
||||
// Delete all resources
|
||||
await execa('kubectl', ['delete', 'all', '--all', '-n', this.namespace], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
}
|
||||
|
||||
log.success('Deployment deleted');
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error(`Failed to delete deployment: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Health check
|
||||
*/
|
||||
async healthCheck() {
|
||||
const status = await this.status();
|
||||
|
||||
if (status.error) {
|
||||
return { healthy: false, error: status.error };
|
||||
}
|
||||
|
||||
// Check if all pods are ready
|
||||
const allPodsReady = status.pods?.every(p => p.ready) || false;
|
||||
|
||||
// Check if all deployments have ready replicas
|
||||
const allDeploymentsReady = status.deployments?.every(d => d.ready > 0) || false;
|
||||
|
||||
return {
|
||||
healthy: allPodsReady && allDeploymentsReady,
|
||||
details: status,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Port forward a service
|
||||
*/
|
||||
async portForward(service, localPort, remotePort) {
|
||||
log.info(`Port forwarding ${service} to localhost:${localPort}...`);
|
||||
|
||||
try {
|
||||
const subprocess = execa('kubectl', [
|
||||
'port-forward', '-n', this.namespace,
|
||||
`service/${service}`,
|
||||
`${localPort}:${remotePort}`,
|
||||
], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
return subprocess;
|
||||
} catch (error) {
|
||||
log.error(`Failed to port forward: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesDeployer;
|
||||
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* Logger Utility
|
||||
*
|
||||
* Provides consistent logging with colors and formatting for the CLI.
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
|
||||
const symbols = {
|
||||
success: '✓',
|
||||
error: '✗',
|
||||
warning: '⚠',
|
||||
info: 'ℹ',
|
||||
debug: '●',
|
||||
arrow: '→',
|
||||
check: '☑',
|
||||
empty: '☐',
|
||||
spinner: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
|
||||
};
|
||||
|
||||
const levels = {
|
||||
error: 'error',
|
||||
warn: 'warn',
|
||||
info: 'info',
|
||||
debug: 'debug',
|
||||
success: 'success',
|
||||
};
|
||||
|
||||
class Logger {
|
||||
constructor(options = {}) {
|
||||
this.level = options.level || levels.info;
|
||||
this.prefix = options.prefix || '';
|
||||
this.showTimestamp = options.showTimestamp ?? false;
|
||||
}
|
||||
|
||||
_getTimestamp() {
|
||||
if (!this.showTimestamp) return '';
|
||||
return chalk.dim(`[${new Date().toISOString().split('T')[1].split('.')[0]}] `);
|
||||
}
|
||||
|
||||
_log(level, color, message, ...args) {
|
||||
const timestamp = this._getTimestamp();
|
||||
const prefix = this.prefix ? `${this.prefix} ` : '';
|
||||
|
||||
switch (level) {
|
||||
case levels.error:
|
||||
console.error(`${timestamp}${prefix}${chalk.red(symbols.error)} ${color(message)}`, ...args);
|
||||
break;
|
||||
case levels.warn:
|
||||
console.warn(`${timestamp}${prefix}${chalk.yellow(symbols.warning)} ${color(message)}`, ...args);
|
||||
break;
|
||||
case levels.info:
|
||||
console.info(`${timestamp}${prefix}${chalk.blue(symbols.info)} ${color(message)}`, ...args);
|
||||
break;
|
||||
case levels.debug:
|
||||
if (this.level === levels.debug) {
|
||||
console.debug(`${timestamp}${prefix}${chalk.gray(symbols.debug)} ${color(message)}`, ...args);
|
||||
}
|
||||
break;
|
||||
case levels.success:
|
||||
console.log(`${timestamp}${prefix}${chalk.green(symbols.success)} ${color(message)}`, ...args);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
error(message, ...args) {
|
||||
this._log(levels.error, chalk.red, message, ...args);
|
||||
}
|
||||
|
||||
warn(message, ...args) {
|
||||
this._log(levels.warn, chalk.yellow, message, ...args);
|
||||
}
|
||||
|
||||
info(message, ...args) {
|
||||
this._log(levels.info, chalk.blue, message, ...args);
|
||||
}
|
||||
|
||||
debug(message, ...args) {
|
||||
this._log(levels.debug, chalk.gray, message, ...args);
|
||||
}
|
||||
|
||||
success(message, ...args) {
|
||||
this._log(levels.success, chalk.green, message, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a section header
|
||||
*/
|
||||
section(title) {
|
||||
console.log(`\n${chalk.cyan('═'.repeat(60))}`);
|
||||
console.log(`${chalk.cyan(symbols.arrow)} ${chalk.bold(title)}`);
|
||||
console.log(`${chalk.cyan('═'.repeat(60))}\n`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a sub-header
|
||||
*/
|
||||
subheader(title) {
|
||||
console.log(`\n${chalk.yellow('─'.repeat(40))}`);
|
||||
console.log(`${chalk.yellow('→')} ${chalk.bold(title)}`);
|
||||
console.log(`${chalk.yellow('─'.repeat(40))}\n`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a list item
|
||||
*/
|
||||
listItem(text, options = {}) {
|
||||
const { indent = 2, symbol = '•', color = chalk.white } = options;
|
||||
console.log(`${' '.repeat(indent)}${chalk.gray(symbol)} ${color(text)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a key-value pair
|
||||
*/
|
||||
kv(key, value, options = {}) {
|
||||
const { indent = 2, keyColor = chalk.cyan, valueColor = chalk.white } = options;
|
||||
console.log(`${' '.repeat(indent)}${keyColor(`${key}:`)} ${valueColor(value)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a box with a message
|
||||
*/
|
||||
box(message, options = {}) {
|
||||
const { title, padding = 1, borderColor = chalk.cyan } = options;
|
||||
const lines = message.split('\n');
|
||||
const maxWidth = Math.max(...lines.map(l => l.length), title ? title.length : 0);
|
||||
const innerWidth = maxWidth + padding * 2;
|
||||
const horizontal = '─'.repeat(innerWidth);
|
||||
|
||||
console.log(borderColor('┌' + '─'.repeat(title ? innerWidth : innerWidth) + '┐'));
|
||||
if (title) {
|
||||
const titlePadding = ' '.repeat(Math.floor((innerWidth - title.length) / 2));
|
||||
console.log(borderColor('│') + titlePadding + chalk.bold(title) + titlePadding + borderColor('│'));
|
||||
console.log(borderColor('├' + horizontal + '┤'));
|
||||
}
|
||||
for (const line of lines) {
|
||||
const rightPadding = ' '.repeat(innerWidth - line.length - padding);
|
||||
const leftPadding = ' '.repeat(padding);
|
||||
console.log(borderColor('│') + leftPadding + line + rightPadding + borderColor('│'));
|
||||
}
|
||||
console.log(borderColor('└' + horizontal + '┘'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a progress bar
|
||||
*/
|
||||
progress(current, total, options = {}) {
|
||||
const { width = 30, label = '' } = options;
|
||||
const percentage = Math.round((current / total) * 100);
|
||||
const filled = Math.round((width * current) / total);
|
||||
const empty = width - filled;
|
||||
const bar = chalk.green('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
|
||||
console.log(`\r${label} [${bar}] ${percentage}% (${current}/${total})`);
|
||||
}
|
||||
}
|
||||
|
||||
// Create default logger instance
|
||||
const log = new Logger();
|
||||
|
||||
export { Logger, log, symbols, levels };
|
||||
export default log;
|
||||
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* Prompts Utility
|
||||
*
|
||||
* Interactive prompts for CLI using Inquirer.
|
||||
*/
|
||||
|
||||
import inquirer from 'inquirer';
|
||||
|
||||
/**
|
||||
* Ask a text input question
|
||||
*/
|
||||
export async function promptText(question, options = {}) {
|
||||
const { default: defaultValue, validate, message: errorMsg } = options;
|
||||
|
||||
const result = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'value',
|
||||
message: question,
|
||||
default: defaultValue,
|
||||
validate: validate ? (input) => {
|
||||
if (validate(input)) return true;
|
||||
return errorMsg || 'Invalid input';
|
||||
} : true,
|
||||
},
|
||||
]);
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask a password question (hidden input)
|
||||
*/
|
||||
export async function promptPassword(question, options = {}) {
|
||||
const { validate, mask = '*' } = options;
|
||||
|
||||
const result = await inquirer.prompt([
|
||||
{
|
||||
type: 'password',
|
||||
name: 'value',
|
||||
message: question,
|
||||
mask,
|
||||
validate: validate || ((input) => {
|
||||
if (input && input.length >= 8) return true;
|
||||
return 'Password must be at least 8 characters';
|
||||
}),
|
||||
},
|
||||
]);
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask a single-choice selection question
|
||||
*/
|
||||
export async function promptSelect(question, choices, options = {}) {
|
||||
const { default: defaultValue } = options;
|
||||
|
||||
const result = await inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'value',
|
||||
message: question,
|
||||
choices,
|
||||
default: defaultValue,
|
||||
},
|
||||
]);
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask a multi-selection question
|
||||
*/
|
||||
export async function promptCheckbox(question, choices, options = {}) {
|
||||
const { default: defaultValue, validate } = options;
|
||||
|
||||
const result = await inquirer.prompt([
|
||||
{
|
||||
type: 'checkbox',
|
||||
name: 'value',
|
||||
message: question,
|
||||
choices,
|
||||
default: defaultValue,
|
||||
validate: validate || ((input) => {
|
||||
if (input.length > 0) return true;
|
||||
return 'Please select at least one option';
|
||||
}),
|
||||
},
|
||||
]);
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask a confirm (yes/no) question
|
||||
*/
|
||||
export async function promptConfirm(question, options = {}) {
|
||||
const { default: defaultValue = false } = options;
|
||||
|
||||
const result = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'value',
|
||||
message: question,
|
||||
default: defaultValue,
|
||||
},
|
||||
]);
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask for a number input
|
||||
*/
|
||||
export async function promptNumber(question, options = {}) {
|
||||
const { min, max, default: defaultValue, validate } = options;
|
||||
|
||||
const result = await inquirer.prompt([
|
||||
{
|
||||
type: 'number',
|
||||
name: 'value',
|
||||
message: question,
|
||||
default: defaultValue,
|
||||
validate: validate || ((input) => {
|
||||
const num = Number(input);
|
||||
if (isNaN(num)) return 'Please enter a valid number';
|
||||
if (min !== undefined && num < min) return `Value must be at least ${min}`;
|
||||
if (max !== undefined && num > max) return `Value must be at most ${max}`;
|
||||
return true;
|
||||
}),
|
||||
},
|
||||
]);
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask for an editor input (opens system editor)
|
||||
*/
|
||||
export async function promptEditor(question, options = {}) {
|
||||
const { default: defaultValue } = options;
|
||||
|
||||
const result = await inquirer.prompt([
|
||||
{
|
||||
type: 'editor',
|
||||
name: 'value',
|
||||
message: question,
|
||||
default: defaultValue,
|
||||
},
|
||||
]);
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a sequence of prompts (wizard style)
|
||||
*/
|
||||
export async function promptSequence(questions) {
|
||||
const result = await inquirer.prompt(questions);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a progress spinner using Ora
|
||||
*/
|
||||
export async function withSpinner(message, action) {
|
||||
const ora = (await import('ora')).default;
|
||||
const spinner = ora(message).start();
|
||||
|
||||
try {
|
||||
const result = await action(spinner);
|
||||
spinner.succeed();
|
||||
return result;
|
||||
} catch (error) {
|
||||
spinner.fail(error.message || 'Operation failed');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
promptText,
|
||||
promptPassword,
|
||||
promptSelect,
|
||||
promptCheckbox,
|
||||
promptConfirm,
|
||||
promptNumber,
|
||||
promptEditor,
|
||||
promptSequence,
|
||||
withSpinner,
|
||||
};
|
||||
@@ -0,0 +1,522 @@
|
||||
#!/bin/bash
|
||||
# ==============================================================================
|
||||
# Heretek OpenClaw - Bare Metal Installation Script
|
||||
# ==============================================================================
|
||||
# This script performs a complete automated installation of OpenClaw on
|
||||
# bare metal Linux servers. It detects the OS and installs appropriate
|
||||
# dependencies, then configures all services.
|
||||
#
|
||||
# Usage: curl -fsSL https://raw.githubusercontent.com/Heretek-AI/heretek-openclaw/main/scripts/install/bare-metal-install.sh | sudo bash
|
||||
#
|
||||
# Or download and run:
|
||||
# curl -fsSL https://raw.githubusercontent.com/Heretek-AI/heretek-openclaw/main/scripts/install/bare-metal-install.sh -o bare-metal-install.sh
|
||||
# chmod +x bare-metal-install.sh
|
||||
# sudo ./bare-metal-install.sh
|
||||
#
|
||||
# Version: 1.0.0
|
||||
# Last Updated: 2026-03-31
|
||||
# ==============================================================================
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Script directory
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration
|
||||
OPENCLAW_DIR="/root/heretek/heretek-openclaw"
|
||||
LITELLM_DIR="/opt/litellm"
|
||||
CONFIG_DIR="/etc/openclaw"
|
||||
LOG_DIR="/var/log/openclaw"
|
||||
BACKUP_DIR="/var/backups/openclaw"
|
||||
|
||||
# Logging functions
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
log_step() {
|
||||
echo -e "${CYAN}[STEP]${NC} $1"
|
||||
}
|
||||
|
||||
# Check if running as root
|
||||
check_root() {
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
log_error "This script must be run as root (use sudo)"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Detect OS and architecture
|
||||
detect_os() {
|
||||
log_step "Detecting operating system..."
|
||||
|
||||
if [[ -f /etc/os-release ]]; then
|
||||
. /etc/os-release
|
||||
OS=$ID
|
||||
VERSION=$VERSION_ID
|
||||
OS_FAMILY=$ID_LIKE
|
||||
|
||||
log_info "Detected: $OS $VERSION"
|
||||
|
||||
# Determine installer script
|
||||
if [[ "$OS" == "ubuntu" || "$OS" == "debian" || "$OS_FAMILY" == *"debian"* ]]; then
|
||||
INSTALLER_SCRIPT="ubuntu-deps.sh"
|
||||
log_info "Using Ubuntu/Debian installer"
|
||||
elif [[ "$OS" == "rhel" || "$OS" == "centos" || "$OS" == "rocky" || "$OS" == "almalinux" || "$OS_FAMILY" == *"rhel"* ]]; then
|
||||
INSTALLER_SCRIPT="rhel-deps.sh"
|
||||
log_info "Using RHEL installer"
|
||||
else
|
||||
log_error "Unsupported OS: $OS"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
log_error "Cannot detect OS. /etc/os-release not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Detect architecture
|
||||
ARCH=$(uname -m)
|
||||
log_info "Architecture: $ARCH"
|
||||
|
||||
# Detect GPU
|
||||
detect_gpu
|
||||
}
|
||||
|
||||
# Detect GPU
|
||||
detect_gpu() {
|
||||
log_step "Detecting GPU..."
|
||||
|
||||
GPU_TYPE="none"
|
||||
|
||||
# Check for AMD GPU
|
||||
if lspci | grep -i "vga.*amd\|vga.*advanced micro devices" &>/dev/null; then
|
||||
GPU_TYPE="amd"
|
||||
log_info "Detected AMD GPU"
|
||||
fi
|
||||
|
||||
# Check for NVIDIA GPU
|
||||
if lspci | grep -i "vga.*nvidia\|3d.*nvidia" &>/dev/null; then
|
||||
GPU_TYPE="nvidia"
|
||||
log_info "Detected NVIDIA GPU"
|
||||
fi
|
||||
|
||||
# Check for ROCm devices
|
||||
if [[ -e /dev/kfd && -e /dev/dri ]]; then
|
||||
GPU_TYPE="amd"
|
||||
log_info "Detected AMD ROCm devices"
|
||||
fi
|
||||
|
||||
# Check for NVIDIA devices
|
||||
if [[ -e /dev/nvidia0 ]]; then
|
||||
GPU_TYPE="nvidia"
|
||||
log_info "Detected NVIDIA CUDA devices"
|
||||
fi
|
||||
|
||||
log_info "GPU Type: $GPU_TYPE"
|
||||
}
|
||||
|
||||
# Check system requirements
|
||||
check_requirements() {
|
||||
log_step "Checking system requirements..."
|
||||
|
||||
# Check CPU cores
|
||||
CPU_CORES=$(nproc)
|
||||
if [[ $CPU_CORES -lt 2 ]]; then
|
||||
log_warning "Minimum 2 CPU cores recommended. Found: $CPU_CORES"
|
||||
fi
|
||||
log_info "CPU Cores: $CPU_CORES"
|
||||
|
||||
# Check RAM
|
||||
RAM_GB=$(free -g | awk '/^Mem:/{print $2}')
|
||||
if [[ $RAM_GB -lt 4 ]]; then
|
||||
log_warning "Minimum 4GB RAM recommended. Found: ${RAM_GB}GB"
|
||||
fi
|
||||
log_info "RAM: ${RAM_GB}GB"
|
||||
|
||||
# Check disk space
|
||||
DISK_GB=$(df -BG / | awk 'NR==2 {print $4}' | tr -d 'G')
|
||||
if [[ $DISK_GB -lt 10 ]]; then
|
||||
log_warning "Minimum 10GB free disk space recommended. Found: ${DISK_GB}GB"
|
||||
fi
|
||||
log_info "Free Disk Space: ${DISK_GB}GB"
|
||||
}
|
||||
|
||||
# Install dependencies
|
||||
install_dependencies() {
|
||||
log_step "Installing dependencies..."
|
||||
|
||||
# Download and run the appropriate dependencies script
|
||||
DEPS_URL="https://raw.githubusercontent.com/Heretek-AI/heretek-openclaw/main/scripts/install/$INSTALLER_SCRIPT"
|
||||
|
||||
log_info "Downloading $INSTALLER_SCRIPT..."
|
||||
curl -fsSL "$DEPS_URL" -o "/tmp/$INSTALLER_SCRIPT"
|
||||
chmod +x "/tmp/$INSTALLER_SCRIPT"
|
||||
|
||||
log_info "Running $INSTALLER_SCRIPT..."
|
||||
"/tmp/$INSTALLER_SCRIPT"
|
||||
|
||||
log_success "Dependencies installed"
|
||||
}
|
||||
|
||||
# Clone repository
|
||||
clone_repository() {
|
||||
log_step "Cloning OpenClaw repository..."
|
||||
|
||||
if [[ -d "$OPENCLAW_DIR" ]]; then
|
||||
log_warning "Directory $OPENCLAW_DIR already exists"
|
||||
log_info "Pulling latest changes..."
|
||||
cd "$OPENCLAW_DIR"
|
||||
git pull
|
||||
else
|
||||
log_info "Cloning repository to $OPENCLAW_DIR..."
|
||||
git clone https://github.com/Heretek-AI/heretek-openclaw.git "$OPENCLAW_DIR"
|
||||
fi
|
||||
|
||||
log_success "Repository cloned"
|
||||
}
|
||||
|
||||
# Create directories
|
||||
create_directories() {
|
||||
log_step "Creating directories..."
|
||||
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
mkdir -p "$LOG_DIR"
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
mkdir -p /etc/litellm
|
||||
mkdir -p /var/log/litellm
|
||||
|
||||
# Set permissions
|
||||
chmod 755 "$CONFIG_DIR"
|
||||
chmod 755 "$LOG_DIR"
|
||||
chmod 755 "$BACKUP_DIR"
|
||||
chmod 755 /etc/litellm
|
||||
chmod 755 /var/log/litellm
|
||||
|
||||
log_success "Directories created"
|
||||
}
|
||||
|
||||
# Configure environment
|
||||
configure_environment() {
|
||||
log_step "Configuring environment..."
|
||||
|
||||
cd "$OPENCLAW_DIR"
|
||||
|
||||
# Copy environment template
|
||||
if [[ -f ".env.bare-metal.example" ]]; then
|
||||
cp ".env.bare-metal.example" "$CONFIG_DIR/.env"
|
||||
log_info "Environment file created: $CONFIG_DIR/.env"
|
||||
elif [[ -f ".env.example" ]]; then
|
||||
cp ".env.example" "$CONFIG_DIR/.env"
|
||||
log_info "Environment file created: $CONFIG_DIR/.env"
|
||||
else
|
||||
log_warning "No environment template found. Creating basic template..."
|
||||
create_basic_env
|
||||
fi
|
||||
|
||||
# Set permissions
|
||||
chmod 600 "$CONFIG_DIR/.env"
|
||||
|
||||
log_success "Environment configured"
|
||||
}
|
||||
|
||||
# Create basic environment file
|
||||
create_basic_env() {
|
||||
cat > "$CONFIG_DIR/.env" << 'EOF'
|
||||
# Heretek OpenClaw - Environment Configuration
|
||||
# Generated by bare-metal-install.sh
|
||||
|
||||
# LiteLLM Gateway
|
||||
LITELLM_MASTER_KEY=change-me-openssl-rand-hex-32
|
||||
LITELLM_SALT_KEY=change-me-openssl-rand-hex-32
|
||||
LITELLM_PORT=4000
|
||||
|
||||
# Model Providers
|
||||
MINIMAX_API_KEY=your-minimax-key-here
|
||||
ZAI_API_KEY=your-zai-key-here
|
||||
|
||||
# Database
|
||||
POSTGRES_USER=openclaw
|
||||
POSTGRES_PASSWORD=change-me-secure-password
|
||||
POSTGRES_DB=openclaw
|
||||
DATABASE_URL=postgresql://openclaw:change-me-secure-password@localhost:5432/openclaw
|
||||
|
||||
# Redis
|
||||
REDIS_URL=redis://localhost:6379/0
|
||||
|
||||
# Ollama
|
||||
OLLAMA_HOST=http://localhost:11434
|
||||
|
||||
# OpenClaw
|
||||
OPENCLAW_DIR=/root/.openclaw
|
||||
OPENCLAW_WORKSPACE=/root/.openclaw/agents
|
||||
EOF
|
||||
}
|
||||
|
||||
# Configure PostgreSQL
|
||||
configure_postgresql() {
|
||||
log_step "Configuring PostgreSQL..."
|
||||
|
||||
# Generate secure password
|
||||
DB_PASSWORD=$(openssl rand -hex 16)
|
||||
|
||||
# Create database and user
|
||||
sudo -u postgres psql -c "CREATE DATABASE openclaw;" 2>/dev/null || true
|
||||
sudo -u postgres psql -c "CREATE USER openclaw WITH PASSWORD '$DB_PASSWORD';" 2>/dev/null || true
|
||||
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE openclaw TO openclaw;" 2>/dev/null || true
|
||||
sudo -u postgres psql -d openclaw -c "CREATE EXTENSION IF NOT EXISTS vector;" 2>/dev/null || true
|
||||
|
||||
# Update environment file with password
|
||||
sed -i "s/change-me-secure-password/$DB_PASSWORD/g" "$CONFIG_DIR/.env"
|
||||
sed -i "s/POSTGRES_PASSWORD=.*/POSTGRES_PASSWORD=$DB_PASSWORD/" "$CONFIG_DIR/.env"
|
||||
sed -i "s/DATABASE_URL=.*/DATABASE_URL=postgresql:\/\/openclaw:$DB_PASSWORD@localhost:5432\/openclaw/" "$CONFIG_DIR/.env"
|
||||
|
||||
log_success "PostgreSQL configured"
|
||||
}
|
||||
|
||||
# Configure Redis
|
||||
configure_redis() {
|
||||
log_step "Configuring Redis..."
|
||||
|
||||
# Generate secure password
|
||||
REDIS_PASSWORD=$(openssl rand -hex 16)
|
||||
|
||||
# Update Redis configuration
|
||||
if [[ -f /etc/redis/redis.conf ]]; then
|
||||
sed -i "s/# requirepass/requirepass $REDIS_PASSWORD/" /etc/redis/redis.conf 2>/dev/null || \
|
||||
echo "requirepass $REDIS_PASSWORD" >> /etc/redis/redis.conf
|
||||
|
||||
# Restart Redis
|
||||
systemctl restart redis
|
||||
fi
|
||||
|
||||
# Update environment file
|
||||
sed -i "s|REDIS_URL=.*|REDIS_URL=redis://:$REDIS_PASSWORD@localhost:6379/0|" "$CONFIG_DIR/.env"
|
||||
|
||||
log_success "Redis configured"
|
||||
}
|
||||
|
||||
# Configure Ollama for GPU
|
||||
configure_ollama() {
|
||||
if [[ "$GPU_TYPE" == "amd" ]]; then
|
||||
log_step "Configuring Ollama for AMD ROCm..."
|
||||
|
||||
# Create systemd override
|
||||
mkdir -p /etc/systemd/system/ollama.service.d
|
||||
cat > /etc/systemd/system/ollama.service.d/rocm.conf << EOF
|
||||
[Service]
|
||||
Environment="HSA_OVERRIDE_GFX_VERSION=10.3.0"
|
||||
Environment="OLLAMA_HOST=0.0.0.0:11434"
|
||||
DevicePolicy=closed
|
||||
DeviceAllow=/dev/kfd rw
|
||||
DeviceAllow=/dev/dri rw
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl restart ollama
|
||||
|
||||
log_success "Ollama configured for AMD ROCm"
|
||||
|
||||
elif [[ "$GPU_TYPE" == "nvidia" ]]; then
|
||||
log_step "Configuring Ollama for NVIDIA CUDA..."
|
||||
|
||||
# Create systemd override
|
||||
mkdir -p /etc/systemd/system/ollama.service.d
|
||||
cat > /etc/systemd/system/ollama.service.d/cuda.conf << EOF
|
||||
[Service]
|
||||
Environment="OLLAMA_HOST=0.0.0.0:11434"
|
||||
Environment="PATH=/usr/bin:/usr/local/cuda/bin"
|
||||
Environment="LD_LIBRARY_PATH=/usr/local/cuda/lib64"
|
||||
DevicePolicy=closed
|
||||
DeviceAllow=/dev/nvidia0 rw
|
||||
DeviceAllow=/dev/nvidiactl rw
|
||||
DeviceAllow=/dev/nvidia-uvm rw
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl restart ollama
|
||||
|
||||
log_success "Ollama configured for NVIDIA CUDA"
|
||||
else
|
||||
log_info "No GPU detected. Ollama will run on CPU."
|
||||
fi
|
||||
|
||||
# Pull embedding model
|
||||
log_info "Pulling embedding model..."
|
||||
ollama pull nomic-embed-text-v2-moe 2>/dev/null || log_warning "Failed to pull embedding model"
|
||||
}
|
||||
|
||||
# Install systemd services
|
||||
install_systemd_services() {
|
||||
log_step "Installing systemd services..."
|
||||
|
||||
cd "$OPENCLAW_DIR"
|
||||
|
||||
# Copy service files
|
||||
if [[ -d "systemd" ]]; then
|
||||
cp systemd/openclaw-gateway.service /etc/systemd/system/ 2>/dev/null || true
|
||||
cp systemd/litellm.service /etc/systemd/system/ 2>/dev/null || true
|
||||
|
||||
# Update service files with correct paths
|
||||
sed -i "s|WorkingDirectory=.*|WorkingDirectory=$OPENCLAW_DIR|" /etc/systemd/system/openclaw-gateway.service 2>/dev/null || true
|
||||
sed -i "s|EnvironmentFile=.*|EnvironmentFile=$CONFIG_DIR/.env|" /etc/systemd/system/openclaw-gateway.service 2>/dev/null || true
|
||||
|
||||
# Reload systemd
|
||||
systemctl daemon-reload
|
||||
|
||||
# Enable services
|
||||
systemctl enable openclaw-gateway 2>/dev/null || true
|
||||
systemctl enable litellm 2>/dev/null || true
|
||||
|
||||
log_success "Systemd services installed"
|
||||
else
|
||||
log_warning "systemd directory not found. Skipping service installation."
|
||||
fi
|
||||
}
|
||||
|
||||
# Initialize database
|
||||
initialize_database() {
|
||||
log_step "Initializing database..."
|
||||
|
||||
cd "$OPENCLAW_DIR"
|
||||
|
||||
# Run migrations if available
|
||||
if [[ -f "package.json" ]] && command -v npm &>/dev/null; then
|
||||
npm run db:migrate 2>/dev/null || log_warning "Database migration failed"
|
||||
fi
|
||||
|
||||
log_success "Database initialized"
|
||||
}
|
||||
|
||||
# Configure LiteLLM
|
||||
configure_litellm() {
|
||||
log_step "Configuring LiteLLM..."
|
||||
|
||||
cd "$OPENCLAW_DIR"
|
||||
|
||||
# Copy LiteLLM configuration
|
||||
if [[ -f "litellm_config.yaml" ]]; then
|
||||
cp "litellm_config.yaml" /etc/litellm/litellm_config.yaml
|
||||
chown litellm:litellm /etc/litellm/litellm_config.yaml 2>/dev/null || true
|
||||
log_success "LiteLLM configured"
|
||||
else
|
||||
log_warning "litellm_config.yaml not found"
|
||||
fi
|
||||
}
|
||||
|
||||
# Print installation summary
|
||||
print_summary() {
|
||||
echo ""
|
||||
echo "=============================================="
|
||||
echo " Heretek OpenClaw - Installation Complete!"
|
||||
echo "=============================================="
|
||||
echo ""
|
||||
log_success "OpenClaw has been installed successfully!"
|
||||
echo ""
|
||||
echo "Installation Details:"
|
||||
echo " - OpenClaw Directory: $OPENCLAW_DIR"
|
||||
echo " - Configuration Directory: $CONFIG_DIR"
|
||||
echo " - Log Directory: $LOG_DIR"
|
||||
echo " - Backup Directory: $BACKUP_DIR"
|
||||
echo ""
|
||||
echo "Next Steps:"
|
||||
echo ""
|
||||
echo " 1. Edit environment file with your API keys:"
|
||||
echo " nano $CONFIG_DIR/.env"
|
||||
echo ""
|
||||
echo " 2. Start services:"
|
||||
echo " sudo systemctl start postgresql"
|
||||
echo " sudo systemctl start redis"
|
||||
echo " sudo systemctl start ollama"
|
||||
echo " sudo systemctl start litellm"
|
||||
echo " sudo systemctl start openclaw-gateway"
|
||||
echo ""
|
||||
echo " 3. Verify installation:"
|
||||
echo " cd $OPENCLAW_DIR"
|
||||
echo " ./scripts/health-check.sh"
|
||||
echo ""
|
||||
echo " 4. Access LiteLLM Dashboard:"
|
||||
echo " http://localhost:4000/ui"
|
||||
echo ""
|
||||
echo " 5. Check OpenClaw status:"
|
||||
echo " openclaw gateway status"
|
||||
echo ""
|
||||
echo "Documentation:"
|
||||
echo " - Bare Metal Deployment: $OPENCLAW_DIR/docs/deployment/BARE_METAL_DEPLOYMENT.md"
|
||||
echo " - Troubleshooting: $OPENCLAW_DIR/docs/deployment/NON_DOCKER_TROUBLESHOOTING.md"
|
||||
echo ""
|
||||
echo "=============================================="
|
||||
}
|
||||
|
||||
# Main installation function
|
||||
main() {
|
||||
echo ""
|
||||
echo "=============================================="
|
||||
echo " Heretek OpenClaw - Bare Metal Installer"
|
||||
echo "=============================================="
|
||||
echo ""
|
||||
|
||||
check_root
|
||||
detect_os
|
||||
check_requirements
|
||||
install_dependencies
|
||||
clone_repository
|
||||
create_directories
|
||||
configure_environment
|
||||
configure_postgresql
|
||||
configure_redis
|
||||
configure_ollama
|
||||
install_systemd_services
|
||||
initialize_database
|
||||
configure_litellm
|
||||
print_summary
|
||||
}
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--gpu)
|
||||
GPU_TYPE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--no-gpu)
|
||||
GPU_TYPE="none"
|
||||
shift
|
||||
;;
|
||||
--help)
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --gpu TYPE Specify GPU type (amd, nvidia, none)"
|
||||
echo " --no-gpu Skip GPU configuration"
|
||||
echo " --help Show this help message"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
@@ -0,0 +1,708 @@
|
||||
#!/bin/bash
|
||||
# ==============================================================================
|
||||
# Heretek OpenClaw - Post-Installation Configuration Script
|
||||
# ==============================================================================
|
||||
# This script performs post-installation configuration tasks for OpenClaw
|
||||
# bare metal and VM deployments. It configures databases, services, and
|
||||
# performs initial setup validation.
|
||||
#
|
||||
# Usage: sudo ./post-install.sh
|
||||
#
|
||||
# Version: 1.0.0
|
||||
# Last Updated: 2026-03-31
|
||||
# ==============================================================================
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Script directory
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration
|
||||
CONFIG_DIR="/etc/openclaw"
|
||||
LOG_DIR="/var/log/openclaw"
|
||||
BACKUP_DIR="/var/backups/openclaw"
|
||||
|
||||
# Logging functions
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
log_step() {
|
||||
echo -e "${CYAN}[STEP]${NC} $1"
|
||||
}
|
||||
|
||||
# Check if running as root
|
||||
check_root() {
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
log_error "This script must be run as root (use sudo)"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Verify prerequisites
|
||||
verify_prerequisites() {
|
||||
log_step "Verifying prerequisites..."
|
||||
|
||||
local missing=()
|
||||
|
||||
# Check required commands
|
||||
for cmd in psql redis-cli ollama openclaw; do
|
||||
if ! command -v $cmd &>/dev/null; then
|
||||
missing+=($cmd)
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ${#missing[@]} -gt 0 ]]; then
|
||||
log_warning "Missing commands: ${missing[*]}"
|
||||
log_warning "Please run the dependencies script first."
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_success "Prerequisites verified"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Configure PostgreSQL database
|
||||
configure_postgresql() {
|
||||
log_step "Configuring PostgreSQL database..."
|
||||
|
||||
# Check if database already exists
|
||||
if sudo -u postgres psql -lqt | cut -d \| -f 1 | grep -qw openclaw; then
|
||||
log_info "Database 'openclaw' already exists"
|
||||
else
|
||||
# Generate secure password
|
||||
DB_PASSWORD=$(openssl rand -hex 16)
|
||||
|
||||
# Create database and user
|
||||
sudo -u postgres psql -c "CREATE DATABASE openclaw;" 2>/dev/null || true
|
||||
sudo -u postgres psql -c "CREATE USER openclaw WITH PASSWORD '$DB_PASSWORD';" 2>/dev/null || true
|
||||
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE openclaw TO openclaw;" 2>/dev/null || true
|
||||
sudo -u postgres psql -d openclaw -c "CREATE EXTENSION IF NOT EXISTS vector;" 2>/dev/null || true
|
||||
|
||||
log_info "Generated PostgreSQL password: $DB_PASSWORD"
|
||||
|
||||
# Update environment file if it exists
|
||||
if [[ -f "$CONFIG_DIR/.env" ]]; then
|
||||
sed -i "s/POSTGRES_PASSWORD=.*/POSTGRES_PASSWORD=$DB_PASSWORD/" "$CONFIG_DIR/.env"
|
||||
sed -i "s|DATABASE_URL=.*|DATABASE_URL=postgresql://openclaw:$DB_PASSWORD@localhost:5432/openclaw|" "$CONFIG_DIR/.env"
|
||||
log_success "Environment file updated with database credentials"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Verify pgvector extension
|
||||
if sudo -u postgres psql -d openclaw -c "SELECT * FROM pg_extension WHERE extname = 'vector';" | grep -q vector; then
|
||||
log_success "pgvector extension enabled"
|
||||
else
|
||||
log_warning "pgvector extension may not be installed"
|
||||
fi
|
||||
|
||||
log_success "PostgreSQL configured"
|
||||
}
|
||||
|
||||
# Configure Redis
|
||||
configure_redis() {
|
||||
log_step "Configuring Redis..."
|
||||
|
||||
# Generate secure password
|
||||
REDIS_PASSWORD=$(openssl rand -hex 16)
|
||||
|
||||
# Update Redis configuration
|
||||
if [[ -f /etc/redis/redis.conf ]]; then
|
||||
# Check if requirepass already set
|
||||
if grep -q "^requirepass" /etc/redis/redis.conf; then
|
||||
log_info "Redis password already configured"
|
||||
else
|
||||
echo "requirepass $REDIS_PASSWORD" >> /etc/redis/redis.conf
|
||||
systemctl restart redis
|
||||
log_info "Generated Redis password: $REDIS_PASSWORD"
|
||||
fi
|
||||
|
||||
# Update environment file
|
||||
if [[ -f "$CONFIG_DIR/.env" ]]; then
|
||||
sed -i "s|REDIS_URL=.*|REDIS_URL=redis://:$REDIS_PASSWORD@localhost:6379/0|" "$CONFIG_DIR/.env"
|
||||
log_success "Environment file updated with Redis credentials"
|
||||
fi
|
||||
else
|
||||
log_warning "Redis configuration file not found"
|
||||
fi
|
||||
|
||||
# Test Redis connection
|
||||
if redis-cli -a "$REDIS_PASSWORD" ping 2>/dev/null | grep -q PONG; then
|
||||
log_success "Redis connection verified"
|
||||
else
|
||||
log_warning "Redis connection test failed"
|
||||
fi
|
||||
|
||||
log_success "Redis configured"
|
||||
}
|
||||
|
||||
# Configure Ollama
|
||||
configure_ollama() {
|
||||
log_step "Configuring Ollama..."
|
||||
|
||||
# Check if Ollama is running
|
||||
if systemctl is-active --quiet ollama; then
|
||||
log_success "Ollama service is running"
|
||||
else
|
||||
log_info "Starting Ollama service..."
|
||||
systemctl start ollama
|
||||
systemctl enable ollama
|
||||
fi
|
||||
|
||||
# Pull required embedding model
|
||||
log_info "Pulling embedding model..."
|
||||
if ollama list | grep -q "nomic-embed-text"; then
|
||||
log_info "Embedding model already exists"
|
||||
else
|
||||
ollama pull nomic-embed-text-v2-moe
|
||||
log_success "Embedding model pulled"
|
||||
fi
|
||||
|
||||
# Test Ollama connection
|
||||
if curl -s http://localhost:11434/api/tags | grep -q "models"; then
|
||||
log_success "Ollama connection verified"
|
||||
else
|
||||
log_warning "Ollama connection test failed"
|
||||
fi
|
||||
|
||||
log_success "Ollama configured"
|
||||
}
|
||||
|
||||
# Configure LiteLLM
|
||||
configure_litellm() {
|
||||
log_step "Configuring LiteLLM..."
|
||||
|
||||
# Create directories if needed
|
||||
mkdir -p /etc/litellm
|
||||
mkdir -p /var/log/litellm
|
||||
|
||||
# Copy configuration if it exists in project
|
||||
if [[ -f "$PROJECT_DIR/litellm_config.yaml" ]]; then
|
||||
cp "$PROJECT_DIR/litellm_config.yaml" /etc/litellm/litellm_config.yaml
|
||||
|
||||
# Set ownership
|
||||
if id -u litellm &>/dev/null; then
|
||||
chown litellm:litellm /etc/litellm/litellm_config.yaml
|
||||
fi
|
||||
|
||||
log_success "LiteLLM configuration copied"
|
||||
else
|
||||
log_warning "litellm_config.yaml not found in project"
|
||||
fi
|
||||
|
||||
# Create systemd service if not exists
|
||||
if [[ ! -f /etc/systemd/system/litellm.service ]]; then
|
||||
log_info "Creating LiteLLM systemd service..."
|
||||
create_litellm_service
|
||||
fi
|
||||
|
||||
log_success "LiteLLM configured"
|
||||
}
|
||||
|
||||
# Create LiteLLM systemd service
|
||||
create_litellm_service() {
|
||||
cat > /etc/systemd/system/litellm.service << 'EOF'
|
||||
[Unit]
|
||||
Description=LiteLLM Proxy Service
|
||||
Documentation=https://docs.litellm.ai
|
||||
After=network.target postgresql.service redis.service
|
||||
Wants=postgresql.service redis.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=litellm
|
||||
Group=litellm
|
||||
|
||||
# Environment configuration
|
||||
Environment="LITELLM_CONFIG_PATH=/etc/litellm/litellm_config.yaml"
|
||||
Environment="DATABASE_URL=postgresql://openclaw:password@localhost:5432/openclaw"
|
||||
Environment="REDIS_URL=redis://localhost:6379/0"
|
||||
EnvironmentFile=-/etc/openclaw/.env
|
||||
|
||||
# Working directory
|
||||
WorkingDirectory=/opt/litellm
|
||||
|
||||
# Main execution
|
||||
ExecStart=/opt/litellm/venv/bin/litellm --config /etc/litellm/litellm_config.yaml --port 4000 --num_workers 4
|
||||
|
||||
# Restart configuration
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
# Resource limits
|
||||
LimitNOFILE=65535
|
||||
|
||||
# Security hardening
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=read-only
|
||||
PrivateTmp=true
|
||||
|
||||
# Allow write access to log directory
|
||||
ReadWritePaths=/var/log/litellm
|
||||
|
||||
# Logging
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=litellm
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
}
|
||||
|
||||
# Configure OpenClaw Gateway
|
||||
configure_openclaw() {
|
||||
log_step "Configuring OpenClaw Gateway..."
|
||||
|
||||
# Check if OpenClaw is installed
|
||||
if ! command -v openclaw &>/dev/null; then
|
||||
log_error "OpenClaw Gateway not installed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Create workspace directory
|
||||
mkdir -p ~/.openclaw
|
||||
mkdir -p ~/.openclaw/agents
|
||||
mkdir -p ~/.openclaw/logs
|
||||
|
||||
# Copy configuration if it exists in project
|
||||
if [[ -f "$PROJECT_DIR/openclaw.json" ]]; then
|
||||
cp "$PROJECT_DIR/openclaw.json" ~/.openclaw/openclaw.json
|
||||
log_success "OpenClaw configuration copied"
|
||||
fi
|
||||
|
||||
# Validate configuration
|
||||
if openclaw gateway validate 2>/dev/null; then
|
||||
log_success "OpenClaw configuration validated"
|
||||
else
|
||||
log_warning "OpenClaw configuration validation failed"
|
||||
fi
|
||||
|
||||
# Create systemd service if not exists
|
||||
if [[ ! -f /etc/systemd/system/openclaw-gateway.service ]]; then
|
||||
log_info "Creating OpenClaw Gateway systemd service..."
|
||||
create_openclaw_service
|
||||
fi
|
||||
|
||||
log_success "OpenClaw Gateway configured"
|
||||
}
|
||||
|
||||
# Create OpenClaw Gateway systemd service
|
||||
create_openclaw_service() {
|
||||
cat > /etc/systemd/system/openclaw-gateway.service << 'EOF'
|
||||
[Unit]
|
||||
Description=OpenClaw Gateway Service
|
||||
Documentation=https://github.com/Heretek-AI/heretek-openclaw
|
||||
After=network.target litellm.service postgresql.service redis.service
|
||||
Wants=litellm.service postgresql.service redis.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Group=root
|
||||
|
||||
# Environment configuration
|
||||
Environment="OPENCLAW_DIR=/root/.openclaw"
|
||||
Environment="OPENCLAW_WORKSPACE=/root/.openclaw/agents"
|
||||
Environment="NODE_ENV=production"
|
||||
EnvironmentFile=-/etc/openclaw/.env
|
||||
|
||||
# Working directory
|
||||
WorkingDirectory=/root/heretek/heretek-openclaw
|
||||
|
||||
# Main execution
|
||||
ExecStart=/usr/bin/node /root/heretek/heretek-openclaw/dist/gateway/index.js
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
|
||||
# Resource limits
|
||||
LimitNOFILE=65535
|
||||
LimitNPROC=4096
|
||||
|
||||
# Security hardening
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=read-only
|
||||
PrivateTmp=true
|
||||
|
||||
# Allow write access to OpenClaw directories
|
||||
ReadWritePaths=/root/.openclaw
|
||||
ReadWritePaths=/var/log/openclaw
|
||||
|
||||
# Logging
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=openclaw-gateway
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
}
|
||||
|
||||
# Enable and start services
|
||||
enable_services() {
|
||||
log_step "Enabling and starting services..."
|
||||
|
||||
# List of services to enable
|
||||
local services=("postgresql" "redis" "ollama" "litellm" "openclaw-gateway")
|
||||
|
||||
for service in "${services[@]}"; do
|
||||
# Check if service file exists
|
||||
if [[ -f /etc/systemd/system/${service}.service ]] || systemctl list-unit-files | grep -q "^${service}"; then
|
||||
log_info "Enabling $service..."
|
||||
systemctl enable "$service" 2>/dev/null || log_warning "Failed to enable $service"
|
||||
|
||||
log_info "Starting $service..."
|
||||
systemctl start "$service" 2>/dev/null || log_warning "Failed to start $service"
|
||||
|
||||
# Wait for service to be ready
|
||||
sleep 2
|
||||
|
||||
if systemctl is-active --quiet "$service"; then
|
||||
log_success "$service is running"
|
||||
else
|
||||
log_warning "$service is not running"
|
||||
fi
|
||||
else
|
||||
log_warning "Service $service not found"
|
||||
fi
|
||||
done
|
||||
|
||||
log_success "Services enabled and started"
|
||||
}
|
||||
|
||||
# Run database migrations
|
||||
run_migrations() {
|
||||
log_step "Running database migrations..."
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
# Check if package.json exists
|
||||
if [[ -f "package.json" ]]; then
|
||||
# Check if npm is available
|
||||
if command -v npm &>/dev/null; then
|
||||
# Run migrations
|
||||
if npm run db:migrate 2>/dev/null; then
|
||||
log_success "Database migrations completed"
|
||||
else
|
||||
log_warning "Database migrations failed or not available"
|
||||
fi
|
||||
else
|
||||
log_warning "npm not found. Skipping migrations."
|
||||
fi
|
||||
else
|
||||
log_warning "package.json not found. Skipping migrations."
|
||||
fi
|
||||
|
||||
log_success "Database migrations complete"
|
||||
}
|
||||
|
||||
# Create agent workspaces
|
||||
create_agent_workspaces() {
|
||||
log_step "Creating agent workspaces..."
|
||||
|
||||
# List of agents to create
|
||||
local agents=(
|
||||
"steward:orchestrator"
|
||||
"alpha:triad"
|
||||
"beta:triad"
|
||||
"charlie:triad"
|
||||
"examiner:interrogator"
|
||||
"explorer:scout"
|
||||
"sentinel:guardian"
|
||||
"coder:artisan"
|
||||
"dreamer:visionary"
|
||||
"empath:diplomat"
|
||||
"historian:archivist"
|
||||
)
|
||||
|
||||
# Check if deploy-agent.sh exists
|
||||
if [[ -f "$PROJECT_DIR/agents/deploy-agent.sh" ]]; then
|
||||
for agent_info in "${agents[@]}"; do
|
||||
agent=$(echo "$agent_info" | cut -d: -f1)
|
||||
role=$(echo "$agent_info" | cut -d: -f2)
|
||||
|
||||
if [[ -d "~/.openclaw/agents/$agent" ]]; then
|
||||
log_info "Agent workspace already exists: $agent"
|
||||
else
|
||||
log_info "Creating agent workspace: $agent ($role)..."
|
||||
cd "$PROJECT_DIR"
|
||||
./agents/deploy-agent.sh "$agent" "$role" 2>/dev/null || log_warning "Failed to create agent: $agent"
|
||||
fi
|
||||
done
|
||||
|
||||
log_success "Agent workspaces created"
|
||||
else
|
||||
log_warning "deploy-agent.sh not found. Skipping agent workspace creation."
|
||||
fi
|
||||
}
|
||||
|
||||
# Configure log rotation
|
||||
configure_log_rotation() {
|
||||
log_step "Configuring log rotation..."
|
||||
|
||||
cat > /etc/logrotate.d/openclaw << 'EOF'
|
||||
/var/log/openclaw/*.log {
|
||||
daily
|
||||
rotate 7
|
||||
compress
|
||||
delaycompress
|
||||
missingok
|
||||
notifempty
|
||||
create 0640 root root
|
||||
sharedscripts
|
||||
postrotate
|
||||
systemctl reload openclaw-gateway 2>/dev/null || true
|
||||
endscript
|
||||
}
|
||||
|
||||
/var/log/litellm/*.log {
|
||||
daily
|
||||
rotate 7
|
||||
compress
|
||||
delaycompress
|
||||
missingok
|
||||
notifempty
|
||||
create 0640 litellm litellm
|
||||
sharedscripts
|
||||
postrotate
|
||||
systemctl reload litellm 2>/dev/null || true
|
||||
endscript
|
||||
}
|
||||
EOF
|
||||
|
||||
log_success "Log rotation configured"
|
||||
}
|
||||
|
||||
# Configure backup
|
||||
configure_backup() {
|
||||
log_step "Configuring automated backup..."
|
||||
|
||||
# Create backup script
|
||||
cat > /usr/local/bin/openclaw-backup.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
BACKUP_DIR="/var/backups/openclaw"
|
||||
DATE=$(date +%Y%m%d_%H%M%S)
|
||||
RETENTION_DAYS=7
|
||||
|
||||
mkdir -p $BACKUP_DIR
|
||||
|
||||
# Backup OpenClaw configuration
|
||||
tar -czf $BACKUP_DIR/openclaw-config-$DATE.tar.gz \
|
||||
~/.openclaw/ \
|
||||
/etc/litellm/ \
|
||||
/etc/openclaw/ 2>/dev/null
|
||||
|
||||
# Backup PostgreSQL
|
||||
pg_dump -U openclaw openclaw 2>/dev/null | gzip > $BACKUP_DIR/openclaw-db-$DATE.sql.gz
|
||||
|
||||
# Backup Redis
|
||||
redis-cli BGSAVE 2>/dev/null
|
||||
sleep 2
|
||||
cp /var/lib/redis/dump.rdb $BACKUP_DIR/redis-dump-$DATE.rdb 2>/dev/null
|
||||
|
||||
# Remove old backups
|
||||
find $BACKUP_DIR -name "*.tar.gz" -mtime +$RETENTION_DAYS -delete 2>/dev/null
|
||||
find $BACKUP_DIR -name "*.sql.gz" -mtime +$RETENTION_DAYS -delete 2>/dev/null
|
||||
find $BACKUP_DIR -name "*.rdb" -mtime +$RETENTION_DAYS -delete 2>/dev/null
|
||||
|
||||
echo "Backup completed: $DATE" >> /var/log/openclaw/backup.log
|
||||
EOF
|
||||
|
||||
chmod +x /usr/local/bin/openclaw-backup.sh
|
||||
|
||||
# Create systemd timer
|
||||
if [[ ! -f /etc/systemd/system/openclaw-backup.timer ]]; then
|
||||
cat > /etc/systemd/system/openclaw-backup.timer << 'EOF'
|
||||
[Unit]
|
||||
Description=Daily OpenClaw Backup
|
||||
Documentation=file:///root/heretek/heretek-openclaw/docs/operations/AUTOMATED_BACKUP.md
|
||||
|
||||
[Timer]
|
||||
OnCalendar=daily
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
EOF
|
||||
|
||||
cat > /etc/systemd/system/openclaw-backup.service << 'EOF'
|
||||
[Unit]
|
||||
Description=OpenClaw Backup Service
|
||||
After=postgresql.service redis.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/bin/openclaw-backup.sh
|
||||
User=root
|
||||
Group=root
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable openclaw-backup.timer
|
||||
systemctl start openclaw-backup.timer
|
||||
fi
|
||||
|
||||
log_success "Automated backup configured"
|
||||
}
|
||||
|
||||
# Run health checks
|
||||
run_health_checks() {
|
||||
log_step "Running health checks..."
|
||||
|
||||
local checks_passed=0
|
||||
local checks_failed=0
|
||||
|
||||
# Check PostgreSQL
|
||||
if psql -U openclaw -d openclaw -c "SELECT 1;" &>/dev/null; then
|
||||
log_success "PostgreSQL: OK"
|
||||
((checks_passed++))
|
||||
else
|
||||
log_error "PostgreSQL: FAILED"
|
||||
((checks_failed++))
|
||||
fi
|
||||
|
||||
# Check Redis
|
||||
if redis-cli ping &>/dev/null; then
|
||||
log_success "Redis: OK"
|
||||
((checks_passed++))
|
||||
else
|
||||
log_error "Redis: FAILED"
|
||||
((checks_failed++))
|
||||
fi
|
||||
|
||||
# Check Ollama
|
||||
if curl -s http://localhost:11434/api/tags &>/dev/null; then
|
||||
log_success "Ollama: OK"
|
||||
((checks_passed++))
|
||||
else
|
||||
log_error "Ollama: FAILED"
|
||||
((checks_failed++))
|
||||
fi
|
||||
|
||||
# Check LiteLLM
|
||||
if curl -s http://localhost:4000/health &>/dev/null; then
|
||||
log_success "LiteLLM: OK"
|
||||
((checks_passed++))
|
||||
else
|
||||
log_warning "LiteLLM: Not running (may need manual start)"
|
||||
fi
|
||||
|
||||
# Check OpenClaw Gateway
|
||||
if openclaw gateway status &>/dev/null; then
|
||||
log_success "OpenClaw Gateway: OK"
|
||||
((checks_passed++))
|
||||
else
|
||||
log_warning "OpenClaw Gateway: Not running (may need manual start)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
log_info "Health Check Summary: $checks_passed passed, $checks_failed failed"
|
||||
|
||||
if [[ $checks_failed -gt 0 ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Print installation summary
|
||||
print_summary() {
|
||||
echo ""
|
||||
echo "=============================================="
|
||||
echo " Heretek OpenClaw - Post-Installation Complete"
|
||||
echo "=============================================="
|
||||
echo ""
|
||||
log_success "Post-installation configuration completed!"
|
||||
echo ""
|
||||
echo "Configuration Summary:"
|
||||
echo " - PostgreSQL: Configured with pgvector"
|
||||
echo " - Redis: Configured with password authentication"
|
||||
echo " - Ollama: Running with embedding model"
|
||||
echo " - LiteLLM: Configured"
|
||||
echo " - OpenClaw Gateway: Configured"
|
||||
echo " - Log Rotation: Configured"
|
||||
echo " - Automated Backup: Configured"
|
||||
echo ""
|
||||
echo "Next Steps:"
|
||||
echo ""
|
||||
echo " 1. Edit environment file with your API keys:"
|
||||
echo " nano $CONFIG_DIR/.env"
|
||||
echo ""
|
||||
echo " 2. Start all services:"
|
||||
echo " sudo systemctl start postgresql redis ollama litellm openclaw-gateway"
|
||||
echo ""
|
||||
echo " 3. Verify installation:"
|
||||
echo " cd $PROJECT_DIR"
|
||||
echo " ./scripts/health-check.sh"
|
||||
echo ""
|
||||
echo " 4. Create agent workspaces (if not done):"
|
||||
echo " ./agents/deploy-agent.sh steward orchestrator"
|
||||
echo " (repeat for all agents)"
|
||||
echo ""
|
||||
echo "Documentation:"
|
||||
echo " - BARE_METAL_DEPLOYMENT.md: $PROJECT_DIR/docs/deployment/BARE_METAL_DEPLOYMENT.md"
|
||||
echo " - TROUBLESHOOTING: $PROJECT_DIR/docs/deployment/NON_DOCKER_TROUBLESHOOTING.md"
|
||||
echo ""
|
||||
echo "=============================================="
|
||||
}
|
||||
|
||||
# Main function
|
||||
main() {
|
||||
echo ""
|
||||
echo "=============================================="
|
||||
echo " Heretek OpenClaw - Post-Installation"
|
||||
echo "=============================================="
|
||||
echo ""
|
||||
|
||||
check_root
|
||||
|
||||
if verify_prerequisites; then
|
||||
configure_postgresql
|
||||
configure_redis
|
||||
configure_ollama
|
||||
configure_litellm
|
||||
configure_openclaw
|
||||
enable_services
|
||||
run_migrations
|
||||
create_agent_workspaces
|
||||
configure_log_rotation
|
||||
configure_backup
|
||||
|
||||
echo ""
|
||||
run_health_checks
|
||||
print_summary
|
||||
else
|
||||
log_error "Prerequisites not met. Please install dependencies first."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
@@ -0,0 +1,435 @@
|
||||
#!/bin/bash
|
||||
# ==============================================================================
|
||||
# Heretek OpenClaw - RHEL/CentOS Dependencies Installer
|
||||
# ==============================================================================
|
||||
# This script installs all required dependencies for OpenClaw bare metal
|
||||
# deployment on RHEL 9+, CentOS Stream 9, Rocky Linux 9, and AlmaLinux 9.
|
||||
#
|
||||
# Usage: sudo ./rhel-deps.sh
|
||||
#
|
||||
# Version: 1.0.0
|
||||
# Last Updated: 2026-03-31
|
||||
# ==============================================================================
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Logging functions
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Check if running as root
|
||||
check_root() {
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
log_error "This script must be run as root (use sudo)"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check OS version
|
||||
check_os() {
|
||||
if [[ -f /etc/os-release ]]; then
|
||||
. /etc/os-release
|
||||
OS=$ID
|
||||
VERSION=$VERSION_ID
|
||||
|
||||
log_info "Detected OS: $OS $VERSION"
|
||||
|
||||
# Check for supported versions
|
||||
if [[ "$OS" == "rhel" ]]; then
|
||||
if (( $(echo "$VERSION < 9" | bc -l) )); then
|
||||
log_error "RHEL 9 or higher is required. Found: $VERSION"
|
||||
exit 1
|
||||
fi
|
||||
elif [[ "$OS" == "centos" ]]; then
|
||||
if [[ "$VERSION_ID" != "9"* ]]; then
|
||||
log_error "CentOS Stream 9 or higher is required. Found: $VERSION"
|
||||
exit 1
|
||||
fi
|
||||
elif [[ "$OS" == "rocky" ]]; then
|
||||
if (( $(echo "$VERSION < 9" | bc -l) )); then
|
||||
log_error "Rocky Linux 9 or higher is required. Found: $VERSION"
|
||||
exit 1
|
||||
fi
|
||||
elif [[ "$OS" == "almalinux" ]]; then
|
||||
if (( $(echo "$VERSION < 9" | bc -l) )); then
|
||||
log_error "AlmaLinux 9 or higher is required. Found: $VERSION"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
log_warning "Unsupported OS: $OS. Script may not work correctly."
|
||||
fi
|
||||
else
|
||||
log_error "Cannot detect OS. /etc/os-release not found."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Update system packages
|
||||
update_system() {
|
||||
log_info "Updating system packages..."
|
||||
dnf update -y -q
|
||||
log_success "System packages updated"
|
||||
}
|
||||
|
||||
# Install EPEL repository
|
||||
install_epel() {
|
||||
log_info "Installing EPEL repository..."
|
||||
|
||||
if ! rpm -q epel-release &>/dev/null; then
|
||||
dnf install -y -q epel-release
|
||||
log_success "EPEL repository installed"
|
||||
else
|
||||
log_info "EPEL repository already installed"
|
||||
fi
|
||||
}
|
||||
|
||||
# Install core dependencies
|
||||
install_core_deps() {
|
||||
log_info "Installing core dependencies..."
|
||||
|
||||
dnf install -y -q \
|
||||
curl \
|
||||
git \
|
||||
wget \
|
||||
gnupg2 \
|
||||
ca-certificates \
|
||||
gcc \
|
||||
gcc-c++ \
|
||||
make \
|
||||
openssl-devel \
|
||||
libffi-devel \
|
||||
bzip2-devel \
|
||||
readline-devel \
|
||||
sqlite-devel \
|
||||
ncurses-devel \
|
||||
xz-devel \
|
||||
tk-devel \
|
||||
libxml2-devel \
|
||||
libxmlsec1-devel \
|
||||
zlib-devel \
|
||||
jq \
|
||||
net-tools \
|
||||
lsof \
|
||||
htop \
|
||||
vim \
|
||||
unzip \
|
||||
pkg-config \
|
||||
policycoreutils-python-utils
|
||||
|
||||
log_success "Core dependencies installed"
|
||||
}
|
||||
|
||||
# Install Python 3.10+
|
||||
install_python() {
|
||||
log_info "Installing Python 3.10+..."
|
||||
|
||||
# Check if Python 3.10+ is already installed
|
||||
if command -v python3 &> /dev/null; then
|
||||
PYTHON_VERSION=$(python3 --version | awk '{print $2}')
|
||||
log_info "Python $PYTHON_VERSION already installed"
|
||||
|
||||
# Check if version is 3.10+
|
||||
if (( $(echo "$PYTHON_VERSION < 3.10" | bc -l) )); then
|
||||
log_warning "Python 3.10+ required. Installing..."
|
||||
dnf install -y -q python3 python3-pip python3-devel
|
||||
fi
|
||||
else
|
||||
dnf install -y -q python3 python3-pip python3-devel
|
||||
fi
|
||||
|
||||
# Upgrade pip
|
||||
python3 -m pip install --upgrade pip --quiet
|
||||
|
||||
log_success "Python installed: $(python3 --version)"
|
||||
}
|
||||
|
||||
# Install Node.js 20 LTS
|
||||
install_nodejs() {
|
||||
log_info "Installing Node.js 20 LTS..."
|
||||
|
||||
# Check if Node.js is already installed
|
||||
if command -v node &> /dev/null; then
|
||||
NODE_VERSION=$(node --version | cut -d'v' -f2 | cut -d'.' -f1)
|
||||
log_info "Node.js $(node --version) already installed"
|
||||
|
||||
if [[ $NODE_VERSION -lt 20 ]]; then
|
||||
log_warning "Node.js 20+ required. Installing..."
|
||||
else
|
||||
log_success "Node.js version is adequate"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Install Node.js 20 LTS
|
||||
curl -fsSL https://rpm.nodesource.com/setup_20.x | bash -
|
||||
dnf install -y -q nodejs
|
||||
|
||||
log_success "Node.js installed: $(node --version)"
|
||||
log_success "npm installed: $(npm --version)"
|
||||
}
|
||||
|
||||
# Install PostgreSQL 15 with pgvector
|
||||
install_postgresql() {
|
||||
log_info "Installing PostgreSQL 15 with pgvector..."
|
||||
|
||||
# Check if PostgreSQL is already installed
|
||||
if command -v psql &> /dev/null; then
|
||||
PG_VERSION=$(psql --version | awk '{print $3}' | cut -d'.' -f1)
|
||||
log_info "PostgreSQL $PG_VERSION already installed"
|
||||
|
||||
if [[ $PG_VERSION -ge 15 ]]; then
|
||||
log_success "PostgreSQL version is adequate"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Add PostgreSQL repository
|
||||
dnf install -y -q https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm
|
||||
|
||||
# Disable default PostgreSQL module
|
||||
dnf -qy module disable postgresql
|
||||
|
||||
# Install PostgreSQL 15
|
||||
dnf install -y -q postgresql15 postgresql15-contrib postgresql15-pgvector
|
||||
|
||||
# Start PostgreSQL
|
||||
systemctl start postgresql-15
|
||||
systemctl enable postgresql-15
|
||||
|
||||
log_success "PostgreSQL 15 installed with pgvector"
|
||||
}
|
||||
|
||||
# Install Redis 7
|
||||
install_redis() {
|
||||
log_info "Installing Redis 7..."
|
||||
|
||||
# Check if Redis is already installed
|
||||
if command -v redis-cli &> /dev/null; then
|
||||
REDIS_VERSION=$(redis-cli --version | awk '{print $2}')
|
||||
log_info "Redis $REDIS_VERSION already installed"
|
||||
|
||||
if (( $(echo "$REDIS_VERSION >= 7" | bc -l) )); then
|
||||
log_success "Redis version is adequate"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Install Redis from Remi repository
|
||||
dnf install -y -q dnf-utils
|
||||
dnf config-manager --set-enabled crb # CodeReady Builder
|
||||
dnf install -y -q https://rpms.remirepo.net/enterprise/remi-release-9.rpm
|
||||
dnf module reset redis -y -q
|
||||
dnf module enable redis:7 -y -q
|
||||
dnf install -y -q redis
|
||||
|
||||
# Start Redis
|
||||
systemctl start redis
|
||||
systemctl enable redis
|
||||
|
||||
log_success "Redis 7 installed"
|
||||
}
|
||||
|
||||
# Install Ollama
|
||||
install_ollama() {
|
||||
log_info "Installing Ollama..."
|
||||
|
||||
# Check if Ollama is already installed
|
||||
if command -v ollama &> /dev/null; then
|
||||
log_info "Ollama already installed: $(ollama --version 2>/dev/null || echo 'unknown')"
|
||||
log_success "Ollama installation verified"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Install Ollama
|
||||
curl -fsSL https://ollama.ai/install.sh | sh
|
||||
|
||||
# Start Ollama
|
||||
systemctl start ollama
|
||||
systemctl enable ollama
|
||||
|
||||
log_success "Ollama installed"
|
||||
}
|
||||
|
||||
# Install LiteLLM dependencies
|
||||
install_litellm_deps() {
|
||||
log_info "Installing LiteLLM dependencies..."
|
||||
|
||||
# Create litellm user if not exists
|
||||
if ! id -u litellm &>/dev/null; then
|
||||
useradd -r -s /bin/false litellm
|
||||
log_info "Created litellm user"
|
||||
fi
|
||||
|
||||
# Create directories
|
||||
mkdir -p /opt/litellm
|
||||
mkdir -p /etc/litellm
|
||||
mkdir -p /var/log/litellm
|
||||
chown litellm:litellm /opt/litellm
|
||||
chown litellm:litellm /var/log/litellm
|
||||
|
||||
# Create virtual environment
|
||||
if [[ ! -f /opt/litellm/venv/bin/python ]]; then
|
||||
python3 -m venv /opt/litellm/venv
|
||||
log_info "Created LiteLLM virtual environment"
|
||||
fi
|
||||
|
||||
# Install LiteLLM
|
||||
/opt/litellm/venv/bin/pip install --upgrade pip --quiet
|
||||
/opt/litellm/venv/bin/pip install \
|
||||
'litellm[proxy]' \
|
||||
'litellm[langfuse]' \
|
||||
'litellm[postgres]' \
|
||||
'litellm[redis]' \
|
||||
psycopg2-binary \
|
||||
redis \
|
||||
langfuse --quiet
|
||||
|
||||
log_success "LiteLLM installed"
|
||||
}
|
||||
|
||||
# Install OpenClaw Gateway
|
||||
install_openclaw() {
|
||||
log_info "Installing OpenClaw Gateway..."
|
||||
|
||||
# Check if OpenClaw is already installed
|
||||
if command -v openclaw &> /dev/null; then
|
||||
log_info "OpenClaw already installed: $(openclaw --version)"
|
||||
log_success "OpenClaw installation verified"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Install OpenClaw
|
||||
curl -fsSL https://openclaw.ai/install.sh | sh
|
||||
|
||||
log_success "OpenClaw Gateway installed"
|
||||
}
|
||||
|
||||
# Configure firewall (firewalld)
|
||||
configure_firewall() {
|
||||
log_info "Configuring firewall..."
|
||||
|
||||
if command -v firewall-cmd &> /dev/null; then
|
||||
# Check if firewalld is active
|
||||
if ! systemctl is-active --quiet firewalld; then
|
||||
log_warning "firewalld is not active. Skipping firewall configuration."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Allow required ports
|
||||
firewall-cmd --permanent --add-service=ssh
|
||||
firewall-cmd --permanent --add-port=4000/tcp # LiteLLM
|
||||
firewall-cmd --permanent --add-port=18789/tcp # OpenClaw Gateway
|
||||
firewall-cmd --permanent --add-port=3000/tcp # Dashboard (optional)
|
||||
|
||||
# Allow localhost for internal services
|
||||
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="127.0.0.1" port port="5432" protocol="tcp" accept'
|
||||
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="127.0.0.1" port port="6379" protocol="tcp" accept'
|
||||
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="127.0.0.1" port port="11434" protocol="tcp" accept'
|
||||
|
||||
# Reload firewall
|
||||
firewall-cmd --reload
|
||||
|
||||
log_success "Firewall configured"
|
||||
else
|
||||
log_warning "firewalld not installed. Skipping firewall configuration."
|
||||
fi
|
||||
}
|
||||
|
||||
# Configure SELinux (optional)
|
||||
configure_selinux() {
|
||||
log_info "Checking SELinux configuration..."
|
||||
|
||||
if command -v getenforce &> /dev/null; then
|
||||
SELINUX_STATUS=$(getenforce)
|
||||
log_info "SELinux status: $SELINUX_STATUS"
|
||||
|
||||
if [[ "$SELINUX_STATUS" == "Enforcing" ]]; then
|
||||
log_warning "SELinux is in Enforcing mode. You may need to create custom policies for OpenClaw."
|
||||
log_info "See docs/deployment/VM_DEPLOYMENT.md for SELinux configuration."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Print installation summary
|
||||
print_summary() {
|
||||
echo ""
|
||||
echo "=============================================="
|
||||
echo " Heretek OpenClaw - Installation Summary"
|
||||
echo "=============================================="
|
||||
echo ""
|
||||
log_success "All dependencies installed successfully!"
|
||||
echo ""
|
||||
echo "Installed components:"
|
||||
echo " - Python: $(python3 --version)"
|
||||
echo " - Node.js: $(node --version)"
|
||||
echo " - npm: $(npm --version)"
|
||||
echo " - PostgreSQL: $(psql --version)"
|
||||
echo " - Redis: $(redis-cli --version)"
|
||||
echo " - Ollama: $(ollama --version 2>/dev/null || echo 'installed')"
|
||||
echo " - OpenClaw: $(openclaw --version 2>/dev/null || echo 'installed')"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Clone the OpenClaw repository:"
|
||||
echo " git clone https://github.com/Heretek-AI/heretek-openclaw.git"
|
||||
echo " cd heretek-openclaw"
|
||||
echo ""
|
||||
echo " 2. Copy environment template:"
|
||||
echo " cp .env.vm.example .env"
|
||||
echo " nano .env # Edit with your values"
|
||||
echo ""
|
||||
echo " 3. Run post-installation script:"
|
||||
echo " sudo ./scripts/install/post-install.sh"
|
||||
echo ""
|
||||
echo " 4. Verify installation:"
|
||||
echo " ./scripts/health-check.sh"
|
||||
echo ""
|
||||
echo "=============================================="
|
||||
}
|
||||
|
||||
# Main installation function
|
||||
main() {
|
||||
echo ""
|
||||
echo "=============================================="
|
||||
echo " Heretek OpenClaw - RHEL Dependencies"
|
||||
echo "=============================================="
|
||||
echo ""
|
||||
|
||||
check_root
|
||||
check_os
|
||||
update_system
|
||||
install_epel
|
||||
install_core_deps
|
||||
install_python
|
||||
install_nodejs
|
||||
install_postgresql
|
||||
install_redis
|
||||
install_ollama
|
||||
install_litellm_deps
|
||||
install_openclaw
|
||||
configure_firewall
|
||||
configure_selinux
|
||||
print_summary
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
@@ -0,0 +1,392 @@
|
||||
#!/bin/bash
|
||||
# ==============================================================================
|
||||
# Heretek OpenClaw - Ubuntu/Debian Dependencies Installer
|
||||
# ==============================================================================
|
||||
# This script installs all required dependencies for OpenClaw bare metal
|
||||
# deployment on Ubuntu 20.04+ and Debian 11+ systems.
|
||||
#
|
||||
# Usage: sudo ./ubuntu-deps.sh
|
||||
#
|
||||
# Version: 1.0.0
|
||||
# Last Updated: 2026-03-31
|
||||
# ==============================================================================
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Logging functions
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Check if running as root
|
||||
check_root() {
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
log_error "This script must be run as root (use sudo)"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check OS version
|
||||
check_os() {
|
||||
if [[ -f /etc/os-release ]]; then
|
||||
. /etc/os-release
|
||||
OS=$ID
|
||||
VERSION=$VERSION_ID
|
||||
|
||||
log_info "Detected OS: $OS $VERSION"
|
||||
|
||||
# Check for supported versions
|
||||
if [[ "$OS" == "ubuntu" ]]; then
|
||||
if (( $(echo "$VERSION < 20.04" | bc -l) )); then
|
||||
log_error "Ubuntu 20.04 or higher is required. Found: $VERSION"
|
||||
exit 1
|
||||
fi
|
||||
elif [[ "$OS" == "debian" ]]; then
|
||||
if (( $(echo "$VERSION < 11" | bc -l) )); then
|
||||
log_error "Debian 11 or higher is required. Found: $VERSION"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
log_warning "Unsupported OS: $OS. Script may not work correctly."
|
||||
fi
|
||||
else
|
||||
log_error "Cannot detect OS. /etc/os-release not found."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Update system packages
|
||||
update_system() {
|
||||
log_info "Updating system packages..."
|
||||
apt-get update -qq
|
||||
apt-get upgrade -y -qq
|
||||
log_success "System packages updated"
|
||||
}
|
||||
|
||||
# Install core dependencies
|
||||
install_core_deps() {
|
||||
log_info "Installing core dependencies..."
|
||||
|
||||
apt-get install -y -qq \
|
||||
curl \
|
||||
git \
|
||||
wget \
|
||||
gnupg \
|
||||
ca-certificates \
|
||||
software-properties-common \
|
||||
build-essential \
|
||||
libssl-dev \
|
||||
libffi-dev \
|
||||
zlib1g-dev \
|
||||
libbz2-dev \
|
||||
libreadline-dev \
|
||||
libsqlite3-dev \
|
||||
libncursesw5-dev \
|
||||
xz-utils \
|
||||
tk-dev \
|
||||
libxml2-dev \
|
||||
libxmlsec1-dev \
|
||||
liblzma-dev \
|
||||
jq \
|
||||
net-tools \
|
||||
lsof \
|
||||
htop \
|
||||
vim \
|
||||
unzip \
|
||||
pkg-config
|
||||
|
||||
log_success "Core dependencies installed"
|
||||
}
|
||||
|
||||
# Install Python 3.10+
|
||||
install_python() {
|
||||
log_info "Installing Python 3.10+..."
|
||||
|
||||
# Check if Python 3.10+ is already installed
|
||||
if command -v python3 &> /dev/null; then
|
||||
PYTHON_VERSION=$(python3 --version | awk '{print $2}')
|
||||
log_info "Python $PYTHON_VERSION already installed"
|
||||
|
||||
# Check if version is 3.10+
|
||||
if (( $(echo "$PYTHON_VERSION < 3.10" | bc -l) )); then
|
||||
log_warning "Python 3.10+ required. Installing..."
|
||||
apt-get install -y -qq python3 python3-pip python3-venv python3-dev
|
||||
fi
|
||||
else
|
||||
apt-get install -y -qq python3 python3-pip python3-venv python3-dev
|
||||
fi
|
||||
|
||||
# Upgrade pip
|
||||
python3 -m pip install --upgrade pip --quiet
|
||||
|
||||
log_success "Python installed: $(python3 --version)"
|
||||
}
|
||||
|
||||
# Install Node.js 20 LTS
|
||||
install_nodejs() {
|
||||
log_info "Installing Node.js 20 LTS..."
|
||||
|
||||
# Check if Node.js is already installed
|
||||
if command -v node &> /dev/null; then
|
||||
NODE_VERSION=$(node --version | cut -d'v' -f2 | cut -d'.' -f1)
|
||||
log_info "Node.js $(node --version) already installed"
|
||||
|
||||
if [[ $NODE_VERSION -lt 20 ]]; then
|
||||
log_warning "Node.js 20+ required. Installing..."
|
||||
else
|
||||
log_success "Node.js version is adequate"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Install Node.js 20 LTS
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
|
||||
apt-get install -y -qq nodejs
|
||||
|
||||
log_success "Node.js installed: $(node --version)"
|
||||
log_success "npm installed: $(npm --version)"
|
||||
}
|
||||
|
||||
# Install PostgreSQL 15 with pgvector
|
||||
install_postgresql() {
|
||||
log_info "Installing PostgreSQL 15 with pgvector..."
|
||||
|
||||
# Check if PostgreSQL is already installed
|
||||
if command -v psql &> /dev/null; then
|
||||
PG_VERSION=$(psql --version | awk '{print $3}' | cut -d'.' -f1)
|
||||
log_info "PostgreSQL $PG_VERSION already installed"
|
||||
|
||||
if [[ $PG_VERSION -ge 15 ]]; then
|
||||
log_success "PostgreSQL version is adequate"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Add PostgreSQL repository
|
||||
sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt '$(lsb_release -cs)'-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
|
||||
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
|
||||
|
||||
# Update and install
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq postgresql-15 postgresql-contrib-15 postgresql-15-pgvector
|
||||
|
||||
# Start PostgreSQL
|
||||
systemctl start postgresql
|
||||
systemctl enable postgresql
|
||||
|
||||
log_success "PostgreSQL 15 installed with pgvector"
|
||||
}
|
||||
|
||||
# Install Redis 7
|
||||
install_redis() {
|
||||
log_info "Installing Redis 7..."
|
||||
|
||||
# Check if Redis is already installed
|
||||
if command -v redis-cli &> /dev/null; then
|
||||
REDIS_VERSION=$(redis-cli --version | awk '{print $2}')
|
||||
log_info "Redis $REDIS_VERSION already installed"
|
||||
|
||||
if (( $(echo "$REDIS_VERSION >= 7" | bc -l) )); then
|
||||
log_success "Redis version is adequate"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Add Redis repository
|
||||
curl -fsSL https://packages.redis.io/gpg | gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg
|
||||
echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/redis.list
|
||||
|
||||
# Update and install
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq redis
|
||||
|
||||
# Start Redis
|
||||
systemctl start redis
|
||||
systemctl enable redis
|
||||
|
||||
log_success "Redis 7 installed"
|
||||
}
|
||||
|
||||
# Install Ollama
|
||||
install_ollama() {
|
||||
log_info "Installing Ollama..."
|
||||
|
||||
# Check if Ollama is already installed
|
||||
if command -v ollama &> /dev/null; then
|
||||
log_info "Ollama already installed: $(ollama --version 2>/dev/null || echo 'unknown')"
|
||||
log_success "Ollama installation verified"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Install Ollama
|
||||
curl -fsSL https://ollama.ai/install.sh | sh
|
||||
|
||||
# Start Ollama
|
||||
systemctl start ollama
|
||||
systemctl enable ollama
|
||||
|
||||
log_success "Ollama installed"
|
||||
}
|
||||
|
||||
# Install LiteLLM dependencies
|
||||
install_litellm_deps() {
|
||||
log_info "Installing LiteLLM dependencies..."
|
||||
|
||||
# Create litellm user if not exists
|
||||
if ! id -u litellm &>/dev/null; then
|
||||
useradd -r -s /bin/false litellm
|
||||
log_info "Created litellm user"
|
||||
fi
|
||||
|
||||
# Create directories
|
||||
mkdir -p /opt/litellm
|
||||
mkdir -p /etc/litellm
|
||||
mkdir -p /var/log/litellm
|
||||
chown litellm:litellm /opt/litellm
|
||||
chown litellm:litellm /var/log/litellm
|
||||
|
||||
# Create virtual environment
|
||||
if [[ ! -f /opt/litellm/venv/bin/python ]]; then
|
||||
python3 -m venv /opt/litellm/venv
|
||||
log_info "Created LiteLLM virtual environment"
|
||||
fi
|
||||
|
||||
# Install LiteLLM
|
||||
/opt/litellm/venv/bin/pip install --upgrade pip --quiet
|
||||
/opt/litellm/venv/bin/pip install \
|
||||
'litellm[proxy]' \
|
||||
'litellm[langfuse]' \
|
||||
'litellm[postgres]' \
|
||||
'litellm[redis]' \
|
||||
psycopg2-binary \
|
||||
redis \
|
||||
langfuse --quiet
|
||||
|
||||
log_success "LiteLLM installed"
|
||||
}
|
||||
|
||||
# Install OpenClaw Gateway
|
||||
install_openclaw() {
|
||||
log_info "Installing OpenClaw Gateway..."
|
||||
|
||||
# Check if OpenClaw is already installed
|
||||
if command -v openclaw &> /dev/null; then
|
||||
log_info "OpenClaw already installed: $(openclaw --version)"
|
||||
log_success "OpenClaw installation verified"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Install OpenClaw
|
||||
curl -fsSL https://openclaw.ai/install.sh | sh
|
||||
|
||||
log_success "OpenClaw Gateway installed"
|
||||
}
|
||||
|
||||
# Configure firewall (UFW)
|
||||
configure_firewall() {
|
||||
log_info "Configuring firewall..."
|
||||
|
||||
if command -v ufw &> /dev/null; then
|
||||
# Check if UFW is active
|
||||
if ! ufw status | grep -q "Status: active"; then
|
||||
log_warning "UFW is not active. Skipping firewall configuration."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Allow required ports
|
||||
ufw allow ssh
|
||||
ufw allow 4000/tcp # LiteLLM
|
||||
ufw allow 18789/tcp # OpenClaw Gateway
|
||||
ufw allow 3000/tcp # Dashboard (optional)
|
||||
|
||||
# Allow localhost for internal services
|
||||
ufw allow from 127.0.0.1 to any port 5432 # PostgreSQL
|
||||
ufw allow from 127.0.0.1 to any port 6379 # Redis
|
||||
ufw allow from 127.0.0.1 to any port 11434 # Ollama
|
||||
|
||||
log_success "Firewall configured"
|
||||
else
|
||||
log_warning "UFW not installed. Skipping firewall configuration."
|
||||
fi
|
||||
}
|
||||
|
||||
# Print installation summary
|
||||
print_summary() {
|
||||
echo ""
|
||||
echo "=============================================="
|
||||
echo " Heretek OpenClaw - Installation Summary"
|
||||
echo "=============================================="
|
||||
echo ""
|
||||
log_success "All dependencies installed successfully!"
|
||||
echo ""
|
||||
echo "Installed components:"
|
||||
echo " - Python: $(python3 --version)"
|
||||
echo " - Node.js: $(node --version)"
|
||||
echo " - npm: $(npm --version)"
|
||||
echo " - PostgreSQL: $(psql --version)"
|
||||
echo " - Redis: $(redis-cli --version)"
|
||||
echo " - Ollama: $(ollama --version 2>/dev/null || echo 'installed')"
|
||||
echo " - OpenClaw: $(openclaw --version 2>/dev/null || echo 'installed')"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Clone the OpenClaw repository:"
|
||||
echo " git clone https://github.com/Heretek-AI/heretek-openclaw.git"
|
||||
echo " cd heretek-openclaw"
|
||||
echo ""
|
||||
echo " 2. Copy environment template:"
|
||||
echo " cp .env.bare-metal.example .env"
|
||||
echo " nano .env # Edit with your values"
|
||||
echo ""
|
||||
echo " 3. Run post-installation script:"
|
||||
echo " sudo ./scripts/install/post-install.sh"
|
||||
echo ""
|
||||
echo " 4. Verify installation:"
|
||||
echo " ./scripts/health-check.sh"
|
||||
echo ""
|
||||
echo "=============================================="
|
||||
}
|
||||
|
||||
# Main installation function
|
||||
main() {
|
||||
echo ""
|
||||
echo "=============================================="
|
||||
echo " Heretek OpenClaw - Ubuntu Dependencies"
|
||||
echo "=============================================="
|
||||
echo ""
|
||||
|
||||
check_root
|
||||
check_os
|
||||
update_system
|
||||
install_core_deps
|
||||
install_python
|
||||
install_nodejs
|
||||
install_postgresql
|
||||
install_redis
|
||||
install_ollama
|
||||
install_litellm_deps
|
||||
install_openclaw
|
||||
configure_firewall
|
||||
print_summary
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
@@ -0,0 +1,664 @@
|
||||
#!/bin/bash
|
||||
# ==============================================================================
|
||||
# Heretek OpenClaw - VM Installation Script
|
||||
# ==============================================================================
|
||||
# This script performs a complete automated installation of OpenClaw on
|
||||
# virtual machines (VMs) across different cloud platforms. It detects
|
||||
# the OS and cloud platform, then installs appropriate dependencies.
|
||||
#
|
||||
# Usage: curl -fsSL https://raw.githubusercontent.com/Heretek-AI/heretek-openclaw/main/scripts/install/vm-install.sh | sudo bash
|
||||
#
|
||||
# Or download and run:
|
||||
# curl -fsSL https://raw.githubusercontent.com/Heretek-AI/heretek-openclaw/main/scripts/install/vm-install.sh -o vm-install.sh
|
||||
# chmod +x vm-install.sh
|
||||
# sudo ./vm-install.sh [--os OS_TYPE] [--gpu GPU_TYPE] [--cloud CLOUD_PROVIDER]
|
||||
#
|
||||
# Options:
|
||||
# --os OS_TYPE Specify OS type (ubuntu, rhel, auto)
|
||||
# --gpu GPU_TYPE Specify GPU type (amd, nvidia, none, auto)
|
||||
# --cloud PROVIDER Specify cloud provider (aws, gcp, azure, auto)
|
||||
# --help Show this help message
|
||||
#
|
||||
# Version: 1.0.0
|
||||
# Last Updated: 2026-03-31
|
||||
# ==============================================================================
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Script directory
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
MAGENTA='\033[0;35m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration
|
||||
OPENCLAW_DIR="/root/heretek/heretek-openclaw"
|
||||
LITELLM_DIR="/opt/litellm"
|
||||
CONFIG_DIR="/etc/openclaw"
|
||||
LOG_DIR="/var/log/openclaw"
|
||||
BACKUP_DIR="/var/backups/openclaw"
|
||||
|
||||
# Default options
|
||||
OS_TYPE="auto"
|
||||
GPU_TYPE="auto"
|
||||
CLOUD_PROVIDER="auto"
|
||||
|
||||
# Logging functions
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
log_step() {
|
||||
echo -e "${CYAN}[STEP]${NC} $1"
|
||||
}
|
||||
|
||||
log_cloud() {
|
||||
echo -e "${MAGENTA}[CLOUD]${NC} $1"
|
||||
}
|
||||
|
||||
# Check if running as root
|
||||
check_root() {
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
log_error "This script must be run as root (use sudo)"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Detect cloud provider
|
||||
detect_cloud() {
|
||||
if [[ "$CLOUD_PROVIDER" != "auto" ]]; then
|
||||
log_info "Using specified cloud provider: $CLOUD_PROVIDER"
|
||||
return
|
||||
fi
|
||||
|
||||
log_step "Detecting cloud provider..."
|
||||
|
||||
# AWS EC2
|
||||
if [[ -f /sys/hypervisor/uuid ]] && grep -qi "ec2" /sys/hypervisor/uuid 2>/dev/null; then
|
||||
CLOUD_PROVIDER="aws"
|
||||
elif dmidecode -s system-product-name 2>/dev/null | grep -qi "amazon ec2"; then
|
||||
CLOUD_PROVIDER="aws"
|
||||
fi
|
||||
|
||||
# GCP Compute
|
||||
if dmidecode -s system-product-name 2>/dev/null | grep -qi "google compute"; then
|
||||
CLOUD_PROVIDER="gcp"
|
||||
fi
|
||||
|
||||
# Azure VM
|
||||
if dmidecode -s system-manufacturer 2>/dev/null | grep -qi "microsoft corporation"; then
|
||||
CLOUD_PROVIDER="azure"
|
||||
fi
|
||||
|
||||
# DigitalOcean
|
||||
if dmidecode -s system-product-name 2>/dev/null | grep -qi "digitalocean"; then
|
||||
CLOUD_PROVIDER="digitalocean"
|
||||
fi
|
||||
|
||||
# Linode
|
||||
if dmidecode -s system-product-name 2>/dev/null | grep -qi "linode"; then
|
||||
CLOUD_PROVIDER="linode"
|
||||
fi
|
||||
|
||||
if [[ "$CLOUD_PROVIDER" == "auto" ]]; then
|
||||
CLOUD_PROVIDER="bare-metal"
|
||||
log_info "No cloud provider detected. Assuming bare metal."
|
||||
else
|
||||
log_cloud "Detected cloud provider: $CLOUD_PROVIDER"
|
||||
fi
|
||||
}
|
||||
|
||||
# Detect OS
|
||||
detect_os() {
|
||||
if [[ "$OS_TYPE" != "auto" ]]; then
|
||||
log_info "Using specified OS type: $OS_TYPE"
|
||||
return
|
||||
fi
|
||||
|
||||
log_step "Detecting operating system..."
|
||||
|
||||
if [[ -f /etc/os-release ]]; then
|
||||
. /etc/os-release
|
||||
OS=$ID
|
||||
VERSION=$VERSION_ID
|
||||
OS_FAMILY=$ID_LIKE
|
||||
|
||||
log_info "Detected: $OS $VERSION"
|
||||
|
||||
if [[ "$OS" == "ubuntu" || "$OS" == "debian" || "$OS_FAMILY" == *"debian"* ]]; then
|
||||
OS_TYPE="ubuntu"
|
||||
INSTALLER_SCRIPT="ubuntu-deps.sh"
|
||||
elif [[ "$OS" == "rhel" || "$OS" == "centos" || "$OS" == "rocky" || "$OS" == "almalinux" || "$OS_FAMILY" == *"rhel"* ]]; then
|
||||
OS_TYPE="rhel"
|
||||
INSTALLER_SCRIPT="rhel-deps.sh"
|
||||
else
|
||||
log_error "Unsupported OS: $OS"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
log_error "Cannot detect OS. /etc/os-release not found."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Detect GPU
|
||||
detect_gpu() {
|
||||
if [[ "$GPU_TYPE" != "auto" ]]; then
|
||||
log_info "Using specified GPU type: $GPU_TYPE"
|
||||
return
|
||||
fi
|
||||
|
||||
log_step "Detecting GPU..."
|
||||
|
||||
GPU_TYPE="none"
|
||||
|
||||
# Check for AMD GPU
|
||||
if lspci | grep -i "vga.*amd\|vga.*advanced micro devices" &>/dev/null; then
|
||||
GPU_TYPE="amd"
|
||||
log_info "Detected AMD GPU"
|
||||
fi
|
||||
|
||||
# Check for NVIDIA GPU
|
||||
if lspci | grep -i "vga.*nvidia\|3d.*nvidia" &>/dev/null; then
|
||||
GPU_TYPE="nvidia"
|
||||
log_info "Detected NVIDIA GPU"
|
||||
fi
|
||||
|
||||
# Check for ROCm devices
|
||||
if [[ -e /dev/kfd && -e /dev/dri ]]; then
|
||||
GPU_TYPE="amd"
|
||||
log_info "Detected AMD ROCm devices"
|
||||
fi
|
||||
|
||||
# Check for NVIDIA devices
|
||||
if [[ -e /dev/nvidia0 ]]; then
|
||||
GPU_TYPE="nvidia"
|
||||
log_info "Detected NVIDIA CUDA devices"
|
||||
fi
|
||||
|
||||
log_info "GPU Type: $GPU_TYPE"
|
||||
}
|
||||
|
||||
# Check system requirements
|
||||
check_requirements() {
|
||||
log_step "Checking system requirements..."
|
||||
|
||||
# Check CPU cores
|
||||
CPU_CORES=$(nproc)
|
||||
log_info "CPU Cores: $CPU_CORES"
|
||||
|
||||
# Check RAM
|
||||
RAM_GB=$(free -g | awk '/^Mem:/{print $2}')
|
||||
log_info "RAM: ${RAM_GB}GB"
|
||||
|
||||
# Check disk space
|
||||
DISK_GB=$(df -BG / | awk 'NR==2 {print $4}' | tr -d 'G')
|
||||
log_info "Free Disk Space: ${DISK_GB}GB"
|
||||
|
||||
# Cloud-specific checks
|
||||
case $CLOUD_PROVIDER in
|
||||
aws)
|
||||
log_cloud "AWS EC2 detected. Verifying instance type..."
|
||||
;;
|
||||
gcp)
|
||||
log_cloud "GCP Compute detected. Verifying machine type..."
|
||||
;;
|
||||
azure)
|
||||
log_cloud "Azure VM detected. Verifying VM size..."
|
||||
;;
|
||||
esac
|
||||
|
||||
# Warnings
|
||||
if [[ $CPU_CORES -lt 2 ]]; then
|
||||
log_warning "Minimum 2 CPU cores recommended. Found: $CPU_CORES"
|
||||
fi
|
||||
if [[ $RAM_GB -lt 4 ]]; then
|
||||
log_warning "Minimum 4GB RAM recommended. Found: ${RAM_GB}GB"
|
||||
fi
|
||||
if [[ $DISK_GB -lt 10 ]]; then
|
||||
log_warning "Minimum 10GB free disk space recommended. Found: ${DISK_GB}GB"
|
||||
fi
|
||||
}
|
||||
|
||||
# Configure cloud-specific settings
|
||||
configure_cloud() {
|
||||
log_step "Configuring cloud-specific settings..."
|
||||
|
||||
case $CLOUD_PROVIDER in
|
||||
aws)
|
||||
configure_aws
|
||||
;;
|
||||
gcp)
|
||||
configure_gcp
|
||||
;;
|
||||
azure)
|
||||
configure_azure
|
||||
;;
|
||||
digitalocean)
|
||||
configure_digitalocean
|
||||
;;
|
||||
linode)
|
||||
configure_linode
|
||||
;;
|
||||
bare-metal)
|
||||
log_info "Bare metal deployment. Skipping cloud configuration."
|
||||
;;
|
||||
esac
|
||||
|
||||
log_success "Cloud configuration complete"
|
||||
}
|
||||
|
||||
# Configure AWS-specific settings
|
||||
configure_aws() {
|
||||
log_cloud "Configuring AWS-specific settings..."
|
||||
|
||||
# Install AWS CLI if not present
|
||||
if ! command -v aws &>/dev/null; then
|
||||
log_info "Installing AWS CLI..."
|
||||
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "/tmp/awscliv2.zip"
|
||||
unzip -q /tmp/awscliv2.zip -d /tmp
|
||||
/tmp/aws/install
|
||||
rm -rf /tmp/aws /tmp/awscliv2.zip
|
||||
fi
|
||||
|
||||
# Check instance metadata
|
||||
if curl -s http://169.254.169.254/latest/meta-data/instance-type &>/dev/null; then
|
||||
INSTANCE_TYPE=$(curl -s http://169.254.169.254/latest/meta-data/instance-type)
|
||||
log_cloud "Instance Type: $INSTANCE_TYPE"
|
||||
fi
|
||||
}
|
||||
|
||||
# Configure GCP-specific settings
|
||||
configure_gcp() {
|
||||
log_cloud "Configuring GCP-specific settings..."
|
||||
|
||||
# Install GCloud CLI if not present
|
||||
if ! command -v gcloud &>/dev/null; then
|
||||
log_info "Installing GCloud CLI..."
|
||||
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq google-cloud-cli
|
||||
fi
|
||||
|
||||
# Check instance metadata
|
||||
if curl -s -H "Metadata-Flavor: Google" http://metadata.google.internal/computeMetadata/v1/instance/machine-type &>/dev/null; then
|
||||
MACHINE_TYPE=$(curl -s -H "Metadata-Flavor: Google" http://metadata.google.internal/computeMetadata/v1/instance/machine-type)
|
||||
log_cloud "Machine Type: $MACHINE_TYPE"
|
||||
fi
|
||||
}
|
||||
|
||||
# Configure Azure-specific settings
|
||||
configure_azure() {
|
||||
log_cloud "Configuring Azure-specific settings..."
|
||||
|
||||
# Install Azure CLI if not present
|
||||
if ! command -v az &>/dev/null; then
|
||||
log_info "Installing Azure CLI..."
|
||||
curl -sL https://aka.ms/InstallAzureCLIDeb | bash
|
||||
fi
|
||||
|
||||
# Check instance metadata
|
||||
if curl -s -H "Metadata:true" "http://169.254.169.254/metadata/instance/compute/vmSize?api-version=2021-02-01" &>/dev/null; then
|
||||
VM_SIZE=$(curl -s -H "Metadata:true" "http://169.254.169.254/metadata/instance/compute/vmSize?api-version=2021-02-01")
|
||||
log_cloud "VM Size: $VM_SIZE"
|
||||
fi
|
||||
}
|
||||
|
||||
# Configure DigitalOcean-specific settings
|
||||
configure_digitalocean() {
|
||||
log_cloud "Configuring DigitalOcean-specific settings..."
|
||||
|
||||
# Check droplet metadata
|
||||
if curl -s http://169.254.169.254/metadata/v1/id &>/dev/null; then
|
||||
DROPLET_ID=$(curl -s http://169.254.169.254/metadata/v1/id)
|
||||
log_cloud "Droplet ID: $DROPLET_ID"
|
||||
fi
|
||||
}
|
||||
|
||||
# Configure Linode-specific settings
|
||||
configure_linode() {
|
||||
log_cloud "Configuring Linode-specific settings..."
|
||||
|
||||
# Check instance metadata
|
||||
if curl -s http://169.254.169.254/metadata/linode/instance &>/dev/null; then
|
||||
INSTANCE_ID=$(curl -s http://169.254.169.254/metadata/linode/instance)
|
||||
log_cloud "Instance ID: $INSTANCE_ID"
|
||||
fi
|
||||
}
|
||||
|
||||
# Install dependencies
|
||||
install_dependencies() {
|
||||
log_step "Installing dependencies..."
|
||||
|
||||
# Download and run the appropriate dependencies script
|
||||
DEPS_URL="https://raw.githubusercontent.com/Heretek-AI/heretek-openclaw/main/scripts/install/$INSTALLER_SCRIPT"
|
||||
|
||||
log_info "Downloading $INSTALLER_SCRIPT..."
|
||||
curl -fsSL "$DEPS_URL" -o "/tmp/$INSTALLER_SCRIPT"
|
||||
chmod +x "/tmp/$INSTALLER_SCRIPT"
|
||||
|
||||
log_info "Running $INSTALLER_SCRIPT..."
|
||||
"/tmp/$INSTALLER_SCRIPT"
|
||||
|
||||
log_success "Dependencies installed"
|
||||
}
|
||||
|
||||
# Clone repository
|
||||
clone_repository() {
|
||||
log_step "Cloning OpenClaw repository..."
|
||||
|
||||
if [[ -d "$OPENCLAW_DIR" ]]; then
|
||||
log_warning "Directory $OPENCLAW_DIR already exists"
|
||||
log_info "Pulling latest changes..."
|
||||
cd "$OPENCLAW_DIR"
|
||||
git pull
|
||||
else
|
||||
log_info "Cloning repository to $OPENCLAW_DIR..."
|
||||
git clone https://github.com/Heretek-AI/heretek-openclaw.git "$OPENCLAW_DIR"
|
||||
fi
|
||||
|
||||
log_success "Repository cloned"
|
||||
}
|
||||
|
||||
# Create directories
|
||||
create_directories() {
|
||||
log_step "Creating directories..."
|
||||
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
mkdir -p "$LOG_DIR"
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
mkdir -p /etc/litellm
|
||||
mkdir -p /var/log/litellm
|
||||
|
||||
# Set permissions
|
||||
chmod 755 "$CONFIG_DIR"
|
||||
chmod 755 "$LOG_DIR"
|
||||
chmod 755 "$BACKUP_DIR"
|
||||
chmod 755 /etc/litellm
|
||||
chmod 755 /var/log/litellm
|
||||
|
||||
log_success "Directories created"
|
||||
}
|
||||
|
||||
# Configure environment
|
||||
configure_environment() {
|
||||
log_step "Configuring environment..."
|
||||
|
||||
cd "$OPENCLAW_DIR"
|
||||
|
||||
# Copy environment template
|
||||
if [[ -f ".env.vm.example" ]]; then
|
||||
cp ".env.vm.example" "$CONFIG_DIR/.env"
|
||||
elif [[ -f ".env.bare-metal.example" ]]; then
|
||||
cp ".env.bare-metal.example" "$CONFIG_DIR/.env"
|
||||
elif [[ -f ".env.example" ]]; then
|
||||
cp ".env.example" "$CONFIG_DIR/.env"
|
||||
else
|
||||
log_warning "No environment template found. Creating basic template..."
|
||||
create_basic_env
|
||||
fi
|
||||
|
||||
# Set permissions
|
||||
chmod 600 "$CONFIG_DIR/.env"
|
||||
|
||||
log_success "Environment configured"
|
||||
}
|
||||
|
||||
# Create basic environment file
|
||||
create_basic_env() {
|
||||
cat > "$CONFIG_DIR/.env" << 'EOF'
|
||||
# Heretek OpenClaw - Environment Configuration
|
||||
# Generated by vm-install.sh
|
||||
|
||||
# LiteLLM Gateway
|
||||
LITELLM_MASTER_KEY=change-me-openssl-rand-hex-32
|
||||
LITELLM_SALT_KEY=change-me-openssl-rand-hex-32
|
||||
LITELLM_PORT=4000
|
||||
|
||||
# Model Providers
|
||||
MINIMAX_API_KEY=your-minimax-key-here
|
||||
ZAI_API_KEY=your-zai-key-here
|
||||
|
||||
# Database
|
||||
POSTGRES_USER=openclaw
|
||||
POSTGRES_PASSWORD=change-me-secure-password
|
||||
POSTGRES_DB=openclaw
|
||||
DATABASE_URL=postgresql://openclaw:change-me-secure-password@localhost:5432/openclaw
|
||||
|
||||
# Redis
|
||||
REDIS_URL=redis://localhost:6379/0
|
||||
|
||||
# Ollama
|
||||
OLLAMA_HOST=http://localhost:11434
|
||||
|
||||
# OpenClaw
|
||||
OPENCLAW_DIR=/root/.openclaw
|
||||
OPENCLAW_WORKSPACE=/root/.openclaw/agents
|
||||
EOF
|
||||
}
|
||||
|
||||
# Configure Ollama for GPU
|
||||
configure_ollama() {
|
||||
if [[ "$GPU_TYPE" == "amd" ]]; then
|
||||
log_step "Configuring Ollama for AMD ROCm..."
|
||||
|
||||
mkdir -p /etc/systemd/system/ollama.service.d
|
||||
cat > /etc/systemd/system/ollama.service.d/rocm.conf << EOF
|
||||
[Service]
|
||||
Environment="HSA_OVERRIDE_GFX_VERSION=10.3.0"
|
||||
Environment="OLLAMA_HOST=0.0.0.0:11434"
|
||||
DevicePolicy=closed
|
||||
DeviceAllow=/dev/kfd rw
|
||||
DeviceAllow=/dev/dri rw
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl restart ollama
|
||||
|
||||
log_success "Ollama configured for AMD ROCm"
|
||||
|
||||
elif [[ "$GPU_TYPE" == "nvidia" ]]; then
|
||||
log_step "Configuring Ollama for NVIDIA CUDA..."
|
||||
|
||||
mkdir -p /etc/systemd/system/ollama.service.d
|
||||
cat > /etc/systemd/system/ollama.service.d/cuda.conf << EOF
|
||||
[Service]
|
||||
Environment="OLLAMA_HOST=0.0.0.0:11434"
|
||||
DevicePolicy=closed
|
||||
DeviceAllow=/dev/nvidia0 rw
|
||||
DeviceAllow=/dev/nvidiactl rw
|
||||
DeviceAllow=/dev/nvidia-uvm rw
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl restart ollama
|
||||
|
||||
log_success "Ollama configured for NVIDIA CUDA"
|
||||
else
|
||||
log_info "No GPU detected. Ollama will run on CPU."
|
||||
fi
|
||||
|
||||
# Pull embedding model
|
||||
log_info "Pulling embedding model..."
|
||||
ollama pull nomic-embed-text-v2-moe 2>/dev/null || log_warning "Failed to pull embedding model"
|
||||
}
|
||||
|
||||
# Configure firewall for cloud
|
||||
configure_firewall() {
|
||||
log_step "Configuring firewall..."
|
||||
|
||||
case $CLOUD_PROVIDER in
|
||||
aws)
|
||||
log_cloud "For AWS, configure Security Groups in the AWS Console:"
|
||||
log_info " - Allow SSH (22) from your IP"
|
||||
log_info " - Allow LiteLLM (4000) from your IP"
|
||||
log_info " - Allow OpenClaw Gateway (18789) from your IP"
|
||||
;;
|
||||
gcp)
|
||||
log_cloud "For GCP, configure Firewall Rules in the GCP Console:"
|
||||
log_info " - Allow SSH (22) from your IP"
|
||||
log_info " - Allow LiteLLM (4000) from your IP"
|
||||
log_info " - Allow OpenClaw Gateway (18789) from your IP"
|
||||
;;
|
||||
azure)
|
||||
log_cloud "For Azure, configure NSG Rules in the Azure Portal:"
|
||||
log_info " - Allow SSH (22) from your IP"
|
||||
log_info " - Allow LiteLLM (4000) from your IP"
|
||||
log_info " - Allow OpenClaw Gateway (18789) from your IP"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Configure local firewall
|
||||
if command -v ufw &> /dev/null; then
|
||||
if ufw status | grep -q "Status: active"; then
|
||||
ufw allow ssh
|
||||
ufw allow 4000/tcp
|
||||
ufw allow 18789/tcp
|
||||
ufw allow 3000/tcp
|
||||
log_success "UFW firewall configured"
|
||||
fi
|
||||
elif command -v firewall-cmd &> /dev/null; then
|
||||
if systemctl is-active --quiet firewalld; then
|
||||
firewall-cmd --permanent --add-service=ssh
|
||||
firewall-cmd --permanent --add-port=4000/tcp
|
||||
firewall-cmd --permanent --add-port=18789/tcp
|
||||
firewall-cmd --permanent --add-port=3000/tcp
|
||||
firewall-cmd --reload
|
||||
log_success "firewalld configured"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Print installation summary
|
||||
print_summary() {
|
||||
echo ""
|
||||
echo "=============================================="
|
||||
echo " Heretek OpenClaw - VM Installation Complete"
|
||||
echo "=============================================="
|
||||
echo ""
|
||||
log_success "OpenClaw has been installed successfully!"
|
||||
echo ""
|
||||
echo "Deployment Details:"
|
||||
echo " - Cloud Provider: $CLOUD_PROVIDER"
|
||||
echo " - OS Type: $OS_TYPE"
|
||||
echo " - GPU Type: $GPU_TYPE"
|
||||
echo " - OpenClaw Directory: $OPENCLAW_DIR"
|
||||
echo " - Configuration Directory: $CONFIG_DIR"
|
||||
echo " - Log Directory: $LOG_DIR"
|
||||
echo ""
|
||||
echo "Next Steps:"
|
||||
echo ""
|
||||
echo " 1. Edit environment file with your API keys:"
|
||||
echo " nano $CONFIG_DIR/.env"
|
||||
echo ""
|
||||
echo " 2. Configure cloud firewall/Security Groups:"
|
||||
case $CLOUD_PROVIDER in
|
||||
aws)
|
||||
echo " AWS Console > EC2 > Security Groups > Add rules"
|
||||
;;
|
||||
gcp)
|
||||
echo " GCP Console > VPC Network > Firewall > Add rule"
|
||||
;;
|
||||
azure)
|
||||
echo " Azure Portal > Network Security Group > Add rule"
|
||||
;;
|
||||
esac
|
||||
echo ""
|
||||
echo " 3. Start services:"
|
||||
echo " sudo systemctl start postgresql redis ollama litellm openclaw-gateway"
|
||||
echo ""
|
||||
echo " 4. Verify installation:"
|
||||
echo " cd $OPENCLAW_DIR"
|
||||
echo " ./scripts/health-check.sh"
|
||||
echo ""
|
||||
echo "Documentation:"
|
||||
echo " - VM Deployment: $OPENCLAW_DIR/docs/deployment/VM_DEPLOYMENT.md"
|
||||
echo " - Troubleshooting: $OPENCLAW_DIR/docs/deployment/NON_DOCKER_TROUBLESHOOTING.md"
|
||||
echo ""
|
||||
echo "=============================================="
|
||||
}
|
||||
|
||||
# Main installation function
|
||||
main() {
|
||||
echo ""
|
||||
echo "=============================================="
|
||||
echo " Heretek OpenClaw - VM Installer"
|
||||
echo "=============================================="
|
||||
echo ""
|
||||
|
||||
check_root
|
||||
detect_cloud
|
||||
detect_os
|
||||
detect_gpu
|
||||
check_requirements
|
||||
configure_cloud
|
||||
install_dependencies
|
||||
clone_repository
|
||||
create_directories
|
||||
configure_environment
|
||||
configure_ollama
|
||||
configure_firewall
|
||||
print_summary
|
||||
}
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--os)
|
||||
OS_TYPE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--gpu)
|
||||
GPU_TYPE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--cloud)
|
||||
CLOUD_PROVIDER="$2"
|
||||
shift 2
|
||||
;;
|
||||
--no-gpu)
|
||||
GPU_TYPE="none"
|
||||
shift
|
||||
;;
|
||||
--help)
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --os OS_TYPE Specify OS type (ubuntu, rhel, auto)"
|
||||
echo " --gpu GPU_TYPE Specify GPU type (amd, nvidia, none, auto)"
|
||||
echo " --cloud PROVIDER Specify cloud provider (aws, gcp, azure, auto)"
|
||||
echo " --no-gpu Skip GPU configuration"
|
||||
echo " --help Show this help message"
|
||||
echo ""
|
||||
echo "Cloud Providers:"
|
||||
echo " aws, gcp, azure, digitalocean, linode, bare-metal"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
@@ -0,0 +1,46 @@
|
||||
[Unit]
|
||||
Description=LiteLLM Proxy Service
|
||||
Documentation=https://docs.litellm.ai
|
||||
After=network.target postgresql.service redis.service
|
||||
Wants=postgresql.service redis.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=litellm
|
||||
Group=litellm
|
||||
|
||||
# Environment configuration
|
||||
Environment="LITELLM_CONFIG_PATH=/etc/litellm/litellm_config.yaml"
|
||||
Environment="DATABASE_URL=postgresql://openclaw:password@localhost:5432/openclaw"
|
||||
Environment="REDIS_URL=redis://localhost:6379/0"
|
||||
EnvironmentFile=-/etc/openclaw/.env
|
||||
|
||||
# Working directory
|
||||
WorkingDirectory=/opt/litellm
|
||||
|
||||
# Main execution
|
||||
ExecStart=/opt/litellm/venv/bin/litellm --config /etc/litellm/litellm_config.yaml --port 4000 --num_workers 4
|
||||
|
||||
# Restart configuration
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
# Resource limits
|
||||
LimitNOFILE=65535
|
||||
|
||||
# Security hardening
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=read-only
|
||||
PrivateTmp=true
|
||||
|
||||
# Allow write access to log directory
|
||||
ReadWritePaths=/var/log/litellm
|
||||
|
||||
# Logging
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=litellm
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,58 @@
|
||||
[Unit]
|
||||
Description=Ollama Service
|
||||
Documentation=https://ollama.ai
|
||||
After=network.target
|
||||
Wants=network.target
|
||||
|
||||
[Service]
|
||||
Type=notify
|
||||
User=ollama
|
||||
Group=ollama
|
||||
|
||||
# Environment configuration
|
||||
Environment="OLLAMA_HOST=0.0.0.0:11434"
|
||||
Environment="OLLAMA_MODELS=/var/lib/ollama/models"
|
||||
|
||||
# Working directory
|
||||
WorkingDirectory=/var/lib/ollama
|
||||
|
||||
# Main execution
|
||||
ExecStart=/usr/local/bin/ollama serve
|
||||
|
||||
# Restart configuration
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
# Resource limits
|
||||
LimitNOFILE=65535
|
||||
LimitNPROC=unlimited
|
||||
|
||||
# Security hardening
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=read-only
|
||||
PrivateTmp=true
|
||||
|
||||
# Allow write access to Ollama directories
|
||||
ReadWritePaths=/var/lib/ollama
|
||||
|
||||
# GPU access (AMD ROCm)
|
||||
# Uncomment the following lines for AMD GPU support
|
||||
# DevicePolicy=closed
|
||||
# DeviceAllow=/dev/kfd rw
|
||||
# DeviceAllow=/dev/dri rw
|
||||
|
||||
# GPU access (NVIDIA CUDA)
|
||||
# Uncomment the following lines for NVIDIA GPU support
|
||||
# DevicePolicy=closed
|
||||
# DeviceAllow=/dev/nvidia0 rw
|
||||
# DeviceAllow=/dev/nvidiactl rw
|
||||
# DeviceAllow=/dev/nvidia-uvm rw
|
||||
|
||||
# Logging
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=ollama
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,46 @@
|
||||
[Unit]
|
||||
Description=OpenClaw Gateway Service
|
||||
Documentation=https://github.com/Heretek-AI/heretek-openclaw
|
||||
After=network.target litellm.service postgresql.service redis.service
|
||||
Wants=litellm.service postgresql.service redis.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Group=root
|
||||
|
||||
# Environment configuration
|
||||
Environment="OPENCLAW_DIR=/root/.openclaw"
|
||||
Environment="OPENCLAW_WORKSPACE=/root/.openclaw/agents"
|
||||
Environment="NODE_ENV=production"
|
||||
EnvironmentFile=-/etc/openclaw/.env
|
||||
|
||||
# Working directory
|
||||
WorkingDirectory=/root/heretek/heretek-openclaw
|
||||
|
||||
# Main execution
|
||||
ExecStart=/usr/bin/node /root/heretek/heretek-openclaw/dist/gateway/index.js
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
|
||||
# Resource limits
|
||||
LimitNOFILE=65535
|
||||
LimitNPROC=4096
|
||||
|
||||
# Security hardening
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=read-only
|
||||
PrivateTmp=true
|
||||
|
||||
# Allow write access to OpenClaw directories
|
||||
ReadWritePaths=/root/.openclaw
|
||||
ReadWritePaths=/var/log/openclaw
|
||||
|
||||
# Logging
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=openclaw-gateway
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,48 @@
|
||||
[Unit]
|
||||
Description=PostgreSQL RDBMS with pgvector
|
||||
Documentation=https://www.postgresql.org/docs/
|
||||
After=network.target
|
||||
Wants=network.target
|
||||
|
||||
[Service]
|
||||
Type=forking
|
||||
User=postgres
|
||||
Group=postgres
|
||||
|
||||
# Environment configuration
|
||||
Environment="PGDATA=/var/lib/postgresql/15/main"
|
||||
Environment="PGVERSION=15"
|
||||
|
||||
# Working directory
|
||||
WorkingDirectory=/var/lib/postgresql
|
||||
|
||||
# Main execution
|
||||
ExecStart=/usr/lib/postgresql/15/bin/pg_ctl -D /var/lib/postgresql/15/main -o "-c config_file=/etc/postgresql/15/main/postgresql.conf" start
|
||||
ExecReload=/usr/lib/postgresql/15/bin/pg_ctl -D /var/lib/postgresql/15/main reload
|
||||
ExecStop=/usr/lib/postgresql/15/bin/pg_ctl -D /var/lib/postgresql/15/main -m fast stop
|
||||
|
||||
# Restart configuration
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
# Timeout for startup
|
||||
TimeoutSec=300
|
||||
|
||||
# Security hardening
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=read-only
|
||||
PrivateTmp=true
|
||||
|
||||
# Allow write access to PostgreSQL directories
|
||||
ReadWritePaths=/var/lib/postgresql
|
||||
ReadWritePaths=/var/log/postgresql
|
||||
ReadWritePaths=/run/postgresql
|
||||
|
||||
# Logging
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=postgresql
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,43 @@
|
||||
[Unit]
|
||||
Description=Redis In-Memory Data Store
|
||||
Documentation=https://redis.io
|
||||
After=network.target
|
||||
Wants=network.target
|
||||
|
||||
[Service]
|
||||
Type=notify
|
||||
User=redis
|
||||
Group=redis
|
||||
|
||||
# Environment configuration
|
||||
Environment="REDIS_PORT=6379"
|
||||
Environment="REDIS_BIND_ADDRESS=127.0.0.1"
|
||||
|
||||
# Working directory
|
||||
WorkingDirectory=/var/lib/redis
|
||||
|
||||
# Main execution
|
||||
ExecStart=/usr/bin/redis-server /etc/redis/redis.conf --daemonize no
|
||||
|
||||
# Restart configuration
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
# Resource limits
|
||||
LimitNOFILE=65535
|
||||
|
||||
# Security hardening
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=read-only
|
||||
PrivateTmp=true
|
||||
ReadWritePaths=/var/lib/redis
|
||||
ReadWritePaths=/var/log/redis
|
||||
|
||||
# Logging
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=redis
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Reference in New Issue
Block a user