|
|
@@ -0,0 +1,471 @@
|
|
|
+# Claude Code Hub - One-Click Deployment Script for Windows
|
|
|
+# PowerShell 5.1+ required
|
|
|
+
|
|
|
+#Requires -Version 5.1
|
|
|
+
|
|
|
+# Script version
|
|
|
+$VERSION = "1.0.0"
|
|
|
+
|
|
|
+# Global variables
|
|
|
+$SUFFIX = ""
|
|
|
+$ADMIN_TOKEN = ""
|
|
|
+$DB_PASSWORD = ""
|
|
|
+$DEPLOY_DIR = "C:\ProgramData\claude-code-hub"
|
|
|
+$IMAGE_TAG = "latest"
|
|
|
+$BRANCH_NAME = "main"
|
|
|
+
|
|
|
+function Write-ColorOutput {
|
|
|
+ param(
|
|
|
+ [string]$Message,
|
|
|
+ [string]$Type = "Info"
|
|
|
+ )
|
|
|
+
|
|
|
+ switch ($Type) {
|
|
|
+ "Header" { Write-Host $Message -ForegroundColor Cyan }
|
|
|
+ "Info" { Write-Host "[INFO] $Message" -ForegroundColor Blue }
|
|
|
+ "Success" { Write-Host "[SUCCESS] $Message" -ForegroundColor Green }
|
|
|
+ "Warning" { Write-Host "[WARNING] $Message" -ForegroundColor Yellow }
|
|
|
+ "Error" { Write-Host "[ERROR] $Message" -ForegroundColor Red }
|
|
|
+ default { Write-Host $Message }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function Show-Header {
|
|
|
+ Write-ColorOutput "╔════════════════════════════════════════════════════════════════╗" -Type Header
|
|
|
+ Write-ColorOutput "║ ║" -Type Header
|
|
|
+ Write-ColorOutput "║ Claude Code Hub - One-Click Deployment ║" -Type Header
|
|
|
+ Write-ColorOutput "║ Version $VERSION ║" -Type Header
|
|
|
+ Write-ColorOutput "║ ║" -Type Header
|
|
|
+ Write-ColorOutput "╚════════════════════════════════════════════════════════════════╝" -Type Header
|
|
|
+ Write-Host ""
|
|
|
+}
|
|
|
+
|
|
|
+function Test-DockerInstalled {
|
|
|
+ Write-ColorOutput "Checking Docker installation..." -Type Info
|
|
|
+
|
|
|
+ try {
|
|
|
+ $dockerVersion = docker --version 2>$null
|
|
|
+ if ($LASTEXITCODE -ne 0) {
|
|
|
+ Write-ColorOutput "Docker is not installed" -Type Warning
|
|
|
+ return $false
|
|
|
+ }
|
|
|
+
|
|
|
+ $composeVersion = docker compose version 2>$null
|
|
|
+ if ($LASTEXITCODE -ne 0) {
|
|
|
+ Write-ColorOutput "Docker Compose is not installed" -Type Warning
|
|
|
+ return $false
|
|
|
+ }
|
|
|
+
|
|
|
+ Write-ColorOutput "Docker and Docker Compose are installed" -Type Success
|
|
|
+ Write-Host $dockerVersion
|
|
|
+ Write-Host $composeVersion
|
|
|
+ return $true
|
|
|
+ }
|
|
|
+ catch {
|
|
|
+ Write-ColorOutput "Docker is not installed" -Type Warning
|
|
|
+ return $false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function Show-DockerInstallInstructions {
|
|
|
+ Write-ColorOutput "Docker is not installed on this system" -Type Error
|
|
|
+ Write-Host ""
|
|
|
+ Write-ColorOutput "Please install Docker Desktop for Windows:" -Type Info
|
|
|
+ Write-Host " 1. Download from: https://www.docker.com/products/docker-desktop/" -ForegroundColor Cyan
|
|
|
+ Write-Host " 2. Run the installer and follow the instructions"
|
|
|
+ Write-Host " 3. Restart your computer after installation"
|
|
|
+ Write-Host " 4. Run this script again"
|
|
|
+ Write-Host ""
|
|
|
+ Write-ColorOutput "Press any key to open Docker Desktop download page..." -Type Info
|
|
|
+ $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
|
|
|
+ Start-Process "https://www.docker.com/products/docker-desktop/"
|
|
|
+ exit 1
|
|
|
+}
|
|
|
+
|
|
|
+function Select-Branch {
|
|
|
+ Write-Host ""
|
|
|
+ Write-ColorOutput "Please select the branch to deploy:" -Type Info
|
|
|
+ Write-Host " 1) main (Stable release - recommended for production)" -ForegroundColor Green
|
|
|
+ Write-Host " 2) dev (Latest features - for testing)" -ForegroundColor Yellow
|
|
|
+ Write-Host ""
|
|
|
+
|
|
|
+ while ($true) {
|
|
|
+ $choice = Read-Host "Enter your choice [1]"
|
|
|
+ if ([string]::IsNullOrWhiteSpace($choice)) {
|
|
|
+ $choice = "1"
|
|
|
+ }
|
|
|
+
|
|
|
+ switch ($choice) {
|
|
|
+ "1" {
|
|
|
+ $script:IMAGE_TAG = "latest"
|
|
|
+ $script:BRANCH_NAME = "main"
|
|
|
+ Write-ColorOutput "Selected branch: main (image tag: latest)" -Type Success
|
|
|
+ break
|
|
|
+ }
|
|
|
+ "2" {
|
|
|
+ $script:IMAGE_TAG = "dev"
|
|
|
+ $script:BRANCH_NAME = "dev"
|
|
|
+ Write-ColorOutput "Selected branch: dev (image tag: dev)" -Type Success
|
|
|
+ break
|
|
|
+ }
|
|
|
+ default {
|
|
|
+ Write-ColorOutput "Invalid choice. Please enter 1 or 2." -Type Error
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ }
|
|
|
+ break
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function New-RandomSuffix {
|
|
|
+ $chars = "abcdefghijklmnopqrstuvwxyz0123456789"
|
|
|
+ $script:SUFFIX = -join ((1..4) | ForEach-Object { $chars[(Get-Random -Maximum $chars.Length)] })
|
|
|
+ Write-ColorOutput "Generated random suffix: $SUFFIX" -Type Info
|
|
|
+}
|
|
|
+
|
|
|
+function New-AdminToken {
|
|
|
+ $bytes = New-Object byte[] 24
|
|
|
+ $rng = [System.Security.Cryptography.RNGCryptoServiceProvider]::Create()
|
|
|
+ $rng.GetBytes($bytes)
|
|
|
+ $script:ADMIN_TOKEN = [Convert]::ToBase64String($bytes) -replace '[/+=]', '' | Select-Object -First 32
|
|
|
+ $script:ADMIN_TOKEN = $script:ADMIN_TOKEN.Substring(0, 32)
|
|
|
+ Write-ColorOutput "Generated secure admin token" -Type Info
|
|
|
+}
|
|
|
+
|
|
|
+function New-DbPassword {
|
|
|
+ $bytes = New-Object byte[] 18
|
|
|
+ $rng = [System.Security.Cryptography.RNGCryptoServiceProvider]::Create()
|
|
|
+ $rng.GetBytes($bytes)
|
|
|
+ $script:DB_PASSWORD = [Convert]::ToBase64String($bytes) -replace '[/+=]', '' | Select-Object -First 24
|
|
|
+ $script:DB_PASSWORD = $script:DB_PASSWORD.Substring(0, 24)
|
|
|
+ Write-ColorOutput "Generated secure database password" -Type Info
|
|
|
+}
|
|
|
+
|
|
|
+function New-DeploymentDirectory {
|
|
|
+ Write-ColorOutput "Creating deployment directory: $DEPLOY_DIR" -Type Info
|
|
|
+
|
|
|
+ try {
|
|
|
+ if (-not (Test-Path $DEPLOY_DIR)) {
|
|
|
+ New-Item -ItemType Directory -Path $DEPLOY_DIR -Force | Out-Null
|
|
|
+ }
|
|
|
+
|
|
|
+ New-Item -ItemType Directory -Path "$DEPLOY_DIR\data\postgres" -Force | Out-Null
|
|
|
+ New-Item -ItemType Directory -Path "$DEPLOY_DIR\data\redis" -Force | Out-Null
|
|
|
+
|
|
|
+ Write-ColorOutput "Deployment directory created" -Type Success
|
|
|
+ }
|
|
|
+ catch {
|
|
|
+ Write-ColorOutput "Failed to create deployment directory: $_" -Type Error
|
|
|
+ exit 1
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function Write-ComposeFile {
|
|
|
+ Write-ColorOutput "Writing docker-compose.yaml..." -Type Info
|
|
|
+
|
|
|
+ $composeContent = @"
|
|
|
+services:
|
|
|
+ postgres:
|
|
|
+ image: postgres:18
|
|
|
+ container_name: claude-code-hub-db-$SUFFIX
|
|
|
+ restart: unless-stopped
|
|
|
+ ports:
|
|
|
+ - "35432:5432"
|
|
|
+ env_file:
|
|
|
+ - ./.env
|
|
|
+ environment:
|
|
|
+ POSTGRES_USER: `${DB_USER:-postgres}
|
|
|
+ POSTGRES_PASSWORD: `${DB_PASSWORD:-postgres}
|
|
|
+ POSTGRES_DB: `${DB_NAME:-claude_code_hub}
|
|
|
+ PGDATA: /data/pgdata
|
|
|
+ TZ: Asia/Shanghai
|
|
|
+ PGTZ: Asia/Shanghai
|
|
|
+ volumes:
|
|
|
+ - ./data/postgres:/data
|
|
|
+ networks:
|
|
|
+ - claude-code-hub-net-$SUFFIX
|
|
|
+ healthcheck:
|
|
|
+ test: ["CMD-SHELL", "pg_isready -U `${DB_USER:-postgres} -d `${DB_NAME:-claude_code_hub}"]
|
|
|
+ interval: 5s
|
|
|
+ timeout: 5s
|
|
|
+ retries: 10
|
|
|
+ start_period: 10s
|
|
|
+
|
|
|
+ redis:
|
|
|
+ image: redis:7-alpine
|
|
|
+ container_name: claude-code-hub-redis-$SUFFIX
|
|
|
+ restart: unless-stopped
|
|
|
+ volumes:
|
|
|
+ - ./data/redis:/data
|
|
|
+ command: redis-server --appendonly yes
|
|
|
+ networks:
|
|
|
+ - claude-code-hub-net-$SUFFIX
|
|
|
+ healthcheck:
|
|
|
+ test: ["CMD", "redis-cli", "ping"]
|
|
|
+ interval: 5s
|
|
|
+ timeout: 3s
|
|
|
+ retries: 5
|
|
|
+ start_period: 5s
|
|
|
+
|
|
|
+ app:
|
|
|
+ image: ghcr.io/ding113/claude-code-hub:$IMAGE_TAG
|
|
|
+ container_name: claude-code-hub-app-$SUFFIX
|
|
|
+ depends_on:
|
|
|
+ postgres:
|
|
|
+ condition: service_healthy
|
|
|
+ redis:
|
|
|
+ condition: service_healthy
|
|
|
+ env_file:
|
|
|
+ - ./.env
|
|
|
+ environment:
|
|
|
+ NODE_ENV: production
|
|
|
+ PORT: `${APP_PORT:-23000}
|
|
|
+ DSN: postgresql://`${DB_USER:-postgres}:`${DB_PASSWORD:-postgres}@claude-code-hub-db-${SUFFIX}:5432/`${DB_NAME:-claude_code_hub}
|
|
|
+ REDIS_URL: redis://claude-code-hub-redis-${SUFFIX}:6379
|
|
|
+ AUTO_MIGRATE: `${AUTO_MIGRATE:-true}
|
|
|
+ ENABLE_RATE_LIMIT: `${ENABLE_RATE_LIMIT:-true}
|
|
|
+ SESSION_TTL: `${SESSION_TTL:-300}
|
|
|
+ TZ: Asia/Shanghai
|
|
|
+ ports:
|
|
|
+ - "`${APP_PORT:-23000}:`${APP_PORT:-23000}"
|
|
|
+ restart: unless-stopped
|
|
|
+ networks:
|
|
|
+ - claude-code-hub-net-$SUFFIX
|
|
|
+ healthcheck:
|
|
|
+ test: ["CMD-SHELL", "curl -f http://localhost:`${APP_PORT:-23000}/api/actions/health || exit 1"]
|
|
|
+ interval: 30s
|
|
|
+ timeout: 5s
|
|
|
+ retries: 3
|
|
|
+ start_period: 30s
|
|
|
+
|
|
|
+networks:
|
|
|
+ claude-code-hub-net-${SUFFIX}:
|
|
|
+ driver: bridge
|
|
|
+ name: claude-code-hub-net-$SUFFIX
|
|
|
+"@
|
|
|
+
|
|
|
+ try {
|
|
|
+ Set-Content -Path "$DEPLOY_DIR\docker-compose.yaml" -Value $composeContent -Encoding UTF8
|
|
|
+ Write-ColorOutput "docker-compose.yaml created" -Type Success
|
|
|
+ }
|
|
|
+ catch {
|
|
|
+ Write-ColorOutput "Failed to write docker-compose.yaml: $_" -Type Error
|
|
|
+ exit 1
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function Write-EnvFile {
|
|
|
+ Write-ColorOutput "Writing .env file..." -Type Info
|
|
|
+
|
|
|
+ $envContent = @"
|
|
|
+# Admin Token (KEEP THIS SECRET!)
|
|
|
+ADMIN_TOKEN=$ADMIN_TOKEN
|
|
|
+
|
|
|
+# Database Configuration
|
|
|
+DB_USER=postgres
|
|
|
+DB_PASSWORD=$DB_PASSWORD
|
|
|
+DB_NAME=claude_code_hub
|
|
|
+
|
|
|
+# Application Configuration
|
|
|
+APP_PORT=23000
|
|
|
+APP_URL=
|
|
|
+
|
|
|
+# Auto Migration (enabled for first-time setup)
|
|
|
+AUTO_MIGRATE=true
|
|
|
+
|
|
|
+# Redis Configuration
|
|
|
+ENABLE_RATE_LIMIT=true
|
|
|
+
|
|
|
+# Session Configuration
|
|
|
+SESSION_TTL=300
|
|
|
+STORE_SESSION_MESSAGES=false
|
|
|
+
|
|
|
+# Cookie Security
|
|
|
+ENABLE_SECURE_COOKIES=true
|
|
|
+
|
|
|
+# Circuit Breaker Configuration
|
|
|
+ENABLE_CIRCUIT_BREAKER_ON_NETWORK_ERRORS=false
|
|
|
+
|
|
|
+# Environment
|
|
|
+NODE_ENV=production
|
|
|
+TZ=Asia/Shanghai
|
|
|
+LOG_LEVEL=info
|
|
|
+"@
|
|
|
+
|
|
|
+ try {
|
|
|
+ Set-Content -Path "$DEPLOY_DIR\.env" -Value $envContent -Encoding UTF8
|
|
|
+ Write-ColorOutput ".env file created" -Type Success
|
|
|
+ }
|
|
|
+ catch {
|
|
|
+ Write-ColorOutput "Failed to write .env file: $_" -Type Error
|
|
|
+ exit 1
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function Start-Services {
|
|
|
+ Write-ColorOutput "Starting Docker services..." -Type Info
|
|
|
+
|
|
|
+ try {
|
|
|
+ Push-Location $DEPLOY_DIR
|
|
|
+
|
|
|
+ docker compose pull
|
|
|
+ if ($LASTEXITCODE -ne 0) {
|
|
|
+ throw "Failed to pull Docker images"
|
|
|
+ }
|
|
|
+
|
|
|
+ docker compose up -d
|
|
|
+ if ($LASTEXITCODE -ne 0) {
|
|
|
+ throw "Failed to start services"
|
|
|
+ }
|
|
|
+
|
|
|
+ Pop-Location
|
|
|
+ Write-ColorOutput "Docker services started" -Type Success
|
|
|
+ }
|
|
|
+ catch {
|
|
|
+ Pop-Location
|
|
|
+ Write-ColorOutput "Failed to start services: $_" -Type Error
|
|
|
+ exit 1
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function Wait-ForHealth {
|
|
|
+ Write-ColorOutput "Waiting for services to become healthy (max 60 seconds)..." -Type Info
|
|
|
+
|
|
|
+ $maxAttempts = 12
|
|
|
+ $attempt = 0
|
|
|
+
|
|
|
+ Push-Location $DEPLOY_DIR
|
|
|
+
|
|
|
+ while ($attempt -lt $maxAttempts) {
|
|
|
+ $attempt++
|
|
|
+
|
|
|
+ try {
|
|
|
+ $postgresHealth = (docker inspect --format='{{.State.Health.Status}}' "claude-code-hub-db-$SUFFIX" 2>$null)
|
|
|
+ $redisHealth = (docker inspect --format='{{.State.Health.Status}}' "claude-code-hub-redis-$SUFFIX" 2>$null)
|
|
|
+ $appHealth = (docker inspect --format='{{.State.Health.Status}}' "claude-code-hub-app-$SUFFIX" 2>$null)
|
|
|
+
|
|
|
+ if (-not $postgresHealth) { $postgresHealth = "unknown" }
|
|
|
+ if (-not $redisHealth) { $redisHealth = "unknown" }
|
|
|
+ if (-not $appHealth) { $appHealth = "unknown" }
|
|
|
+
|
|
|
+ Write-ColorOutput "Health status - Postgres: $postgresHealth, Redis: $redisHealth, App: $appHealth" -Type Info
|
|
|
+
|
|
|
+ if ($postgresHealth -eq "healthy" -and $redisHealth -eq "healthy" -and $appHealth -eq "healthy") {
|
|
|
+ Pop-Location
|
|
|
+ Write-ColorOutput "All services are healthy!" -Type Success
|
|
|
+ return $true
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch {
|
|
|
+ # Continue waiting
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($attempt -lt $maxAttempts) {
|
|
|
+ Start-Sleep -Seconds 5
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Pop-Location
|
|
|
+ Write-ColorOutput "Services did not become healthy within 60 seconds" -Type Warning
|
|
|
+ Write-ColorOutput "You can check the logs with: cd $DEPLOY_DIR; docker compose logs -f" -Type Info
|
|
|
+ return $false
|
|
|
+}
|
|
|
+
|
|
|
+function Get-NetworkAddresses {
|
|
|
+ $addresses = @()
|
|
|
+
|
|
|
+ try {
|
|
|
+ $adapters = Get-NetIPAddress -AddressFamily IPv4 |
|
|
|
+ Where-Object {
|
|
|
+ $_.InterfaceAlias -notlike '*Loopback*' -and
|
|
|
+ $_.InterfaceAlias -notlike '*Docker*' -and
|
|
|
+ $_.IPAddress -notlike '169.254.*'
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach ($adapter in $adapters) {
|
|
|
+ $addresses += $adapter.IPAddress
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch {
|
|
|
+ # Silently continue
|
|
|
+ }
|
|
|
+
|
|
|
+ $addresses += "localhost"
|
|
|
+ return $addresses
|
|
|
+}
|
|
|
+
|
|
|
+function Show-SuccessMessage {
|
|
|
+ $addresses = Get-NetworkAddresses
|
|
|
+
|
|
|
+ Write-Host ""
|
|
|
+ Write-Host "╔════════════════════════════════════════════════════════════════╗" -ForegroundColor Green
|
|
|
+ Write-Host "║ ║" -ForegroundColor Green
|
|
|
+ Write-Host "║ 🎉 Claude Code Hub Deployed Successfully! 🎉 ║" -ForegroundColor Green
|
|
|
+ Write-Host "║ ║" -ForegroundColor Green
|
|
|
+ Write-Host "╚════════════════════════════════════════════════════════════════╝" -ForegroundColor Green
|
|
|
+ Write-Host ""
|
|
|
+
|
|
|
+ Write-Host "📍 Deployment Directory:" -ForegroundColor Blue
|
|
|
+ Write-Host " $DEPLOY_DIR"
|
|
|
+ Write-Host ""
|
|
|
+
|
|
|
+ Write-Host "🌐 Access URLs:" -ForegroundColor Blue
|
|
|
+ foreach ($addr in $addresses) {
|
|
|
+ Write-Host " http://${addr}:23000" -ForegroundColor Green
|
|
|
+ }
|
|
|
+ Write-Host ""
|
|
|
+
|
|
|
+ Write-Host "🔑 Admin Token (KEEP THIS SECRET!):" -ForegroundColor Blue
|
|
|
+ Write-Host " $ADMIN_TOKEN" -ForegroundColor Yellow
|
|
|
+ Write-Host ""
|
|
|
+
|
|
|
+ Write-Host "📚 Usage Documentation:" -ForegroundColor Blue
|
|
|
+ $firstAddr = $addresses[0]
|
|
|
+ Write-Host " Chinese: http://${firstAddr}:23000/zh-CN/usage-doc" -ForegroundColor Green
|
|
|
+ Write-Host " English: http://${firstAddr}:23000/en-US/usage-doc" -ForegroundColor Green
|
|
|
+ Write-Host ""
|
|
|
+
|
|
|
+ Write-Host "🔧 Useful Commands:" -ForegroundColor Blue
|
|
|
+ Write-Host " View logs: cd $DEPLOY_DIR; docker compose logs -f" -ForegroundColor Yellow
|
|
|
+ Write-Host " Stop services: cd $DEPLOY_DIR; docker compose down" -ForegroundColor Yellow
|
|
|
+ Write-Host " Restart: cd $DEPLOY_DIR; docker compose restart" -ForegroundColor Yellow
|
|
|
+ Write-Host ""
|
|
|
+
|
|
|
+ Write-Host "⚠️ IMPORTANT: Please save the admin token in a secure location!" -ForegroundColor Red
|
|
|
+ Write-Host ""
|
|
|
+}
|
|
|
+
|
|
|
+function Main {
|
|
|
+ Show-Header
|
|
|
+
|
|
|
+ if (-not (Test-DockerInstalled)) {
|
|
|
+ Show-DockerInstallInstructions
|
|
|
+ exit 1
|
|
|
+ }
|
|
|
+
|
|
|
+ Select-Branch
|
|
|
+
|
|
|
+ New-RandomSuffix
|
|
|
+ New-AdminToken
|
|
|
+ New-DbPassword
|
|
|
+
|
|
|
+ New-DeploymentDirectory
|
|
|
+ Write-ComposeFile
|
|
|
+ Write-EnvFile
|
|
|
+
|
|
|
+ Start-Services
|
|
|
+
|
|
|
+ $isHealthy = Wait-ForHealth
|
|
|
+
|
|
|
+ if ($isHealthy) {
|
|
|
+ Show-SuccessMessage
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ Write-ColorOutput "Deployment completed but some services may not be fully healthy yet" -Type Warning
|
|
|
+ Write-ColorOutput "Please check the logs: cd $DEPLOY_DIR; docker compose logs -f" -Type Info
|
|
|
+ Show-SuccessMessage
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+# Run main function
|
|
|
+Main
|