|
| 1 | +#Requires -Version 5.1 |
| 2 | +<# |
| 3 | +.SYNOPSIS |
| 4 | + Game Server Manager — Windows setup script (PowerShell equivalent of setup.sh). |
| 5 | +
|
| 6 | +.DESCRIPTION |
| 7 | + Checks prerequisites (Node.js 20+, Terraform, AWS CLI), installs missing tools |
| 8 | + where possible, installs npm workspaces, builds Lambda bundles, bootstraps the |
| 9 | + S3 + DynamoDB Terraform backend, and runs `terraform init`. |
| 10 | +#> |
| 11 | + |
| 12 | +$ErrorActionPreference = 'Stop' |
| 13 | +Set-StrictMode -Version Latest |
| 14 | + |
| 15 | +$ScriptDir = $PSScriptRoot |
| 16 | + |
| 17 | +Write-Host "" |
| 18 | +Write-Host " Game Server Manager - Setup" |
| 19 | +Write-Host " --------------------------------------" |
| 20 | +Write-Host "" |
| 21 | + |
| 22 | +# --------------------------------------------------------------------------- |
| 23 | +# Helpers |
| 24 | +# --------------------------------------------------------------------------- |
| 25 | + |
| 26 | +function Test-Command { |
| 27 | + param([string]$Name) |
| 28 | + return [bool](Get-Command $Name -ErrorAction SilentlyContinue) |
| 29 | +} |
| 30 | + |
| 31 | +# $ErrorActionPreference = 'Stop' only traps cmdlet errors, not native-exe exit codes. |
| 32 | +# Wrap every native call with this so a non-zero exit code throws immediately. |
| 33 | +function Invoke-Native { |
| 34 | + param([scriptblock]$Block) |
| 35 | + & $Block |
| 36 | + if ($LASTEXITCODE -ne 0) { |
| 37 | + throw "Command failed with exit code $LASTEXITCODE." |
| 38 | + } |
| 39 | +} |
| 40 | + |
| 41 | +function Install-WithWinget { |
| 42 | + param([string]$PackageId, [string]$DisplayName) |
| 43 | + if (-not (Test-Command 'winget')) { |
| 44 | + Write-Host " winget is not available. Please install $DisplayName manually." |
| 45 | + exit 1 |
| 46 | + } |
| 47 | + Write-Host " Installing $DisplayName via winget..." |
| 48 | + Invoke-Native { winget install --id $PackageId --silent --accept-package-agreements --accept-source-agreements } |
| 49 | + # Refresh PATH for the current session so subsequent Test-Command calls work. |
| 50 | + $env:PATH = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' + |
| 51 | + [System.Environment]::GetEnvironmentVariable('PATH', 'User') |
| 52 | +} |
| 53 | + |
| 54 | +# --------------------------------------------------------------------------- |
| 55 | +# 1. Prerequisites |
| 56 | +# --------------------------------------------------------------------------- |
| 57 | + |
| 58 | +# Node.js 20+ |
| 59 | +if (-not (Test-Command 'node')) { |
| 60 | + Write-Host " Node.js not found." |
| 61 | + Install-WithWinget 'OpenJS.NodeJS.LTS' 'Node.js LTS' |
| 62 | + if (-not (Test-Command 'node')) { |
| 63 | + Write-Host " Node.js still not found after install. Open a new terminal and re-run." |
| 64 | + exit 1 |
| 65 | + } |
| 66 | +} |
| 67 | + |
| 68 | +$nodeMajor = [int](node -p "process.versions.node.split('.')[0]") |
| 69 | +if ($nodeMajor -lt 20) { |
| 70 | + Write-Host " Node.js 20+ required (detected $nodeMajor). Please upgrade and re-run." |
| 71 | + exit 1 |
| 72 | +} |
| 73 | + |
| 74 | +# Terraform |
| 75 | +if (-not (Test-Command 'terraform')) { |
| 76 | + Write-Host " terraform not found — installing via winget..." |
| 77 | + Install-WithWinget 'HashiCorp.Terraform' 'Terraform' |
| 78 | + if (-not (Test-Command 'terraform')) { |
| 79 | + Write-Host " Terraform still not found after install. Open a new terminal and re-run." |
| 80 | + exit 1 |
| 81 | + } |
| 82 | +} |
| 83 | + |
| 84 | +# AWS CLI |
| 85 | +if (-not (Test-Command 'aws')) { |
| 86 | + Write-Host " AWS CLI not found — downloading and installing v2..." |
| 87 | + $msiPath = Join-Path $env:TEMP 'AWSCLIV2.msi' |
| 88 | + Invoke-WebRequest -Uri 'https://awscli.amazonaws.com/AWSCLIV2.msi' -OutFile $msiPath |
| 89 | + Start-Process msiexec.exe -ArgumentList "/i `"$msiPath`" /qn /norestart" -Wait -Verb RunAs |
| 90 | + Remove-Item $msiPath -ErrorAction SilentlyContinue |
| 91 | + $env:PATH = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' + |
| 92 | + [System.Environment]::GetEnvironmentVariable('PATH', 'User') |
| 93 | + if (-not (Test-Command 'aws')) { |
| 94 | + Write-Host " AWS CLI still not found after install. Open a new terminal and re-run." |
| 95 | + exit 1 |
| 96 | + } |
| 97 | +} |
| 98 | + |
| 99 | +Write-Host " Prerequisites found (node, terraform, aws cli)" |
| 100 | + |
| 101 | +# --------------------------------------------------------------------------- |
| 102 | +# 2. Install JS dependencies and build Lambda bundles |
| 103 | +# --------------------------------------------------------------------------- |
| 104 | + |
| 105 | +Write-Host "" |
| 106 | +Write-Host " Installing Node dependencies..." |
| 107 | +Set-Location (Join-Path $ScriptDir 'app') |
| 108 | +Invoke-Native { npm ci } |
| 109 | + |
| 110 | +Write-Host "" |
| 111 | +Write-Host " Building Lambda bundles..." |
| 112 | +Invoke-Native { npm run build:lambdas } |
| 113 | + |
| 114 | +# --------------------------------------------------------------------------- |
| 115 | +# 3. Bootstrap S3 backend + Terraform init |
| 116 | +# --------------------------------------------------------------------------- |
| 117 | + |
| 118 | +Write-Host "" |
| 119 | +Write-Host " Initializing Terraform..." |
| 120 | +Set-Location (Join-Path $ScriptDir 'terraform') |
| 121 | + |
| 122 | +$tfvarsPath = 'terraform.tfvars' |
| 123 | +if (-not (Test-Path $tfvarsPath)) { |
| 124 | + Copy-Item 'terraform.tfvars.example' $tfvarsPath |
| 125 | + Write-Host " Created terraform.tfvars from example - edit it with your settings." |
| 126 | +} |
| 127 | + |
| 128 | +# Parse project_name and aws_region from terraform.tfvars (fall back to defaults). |
| 129 | +$tfvarsContent = Get-Content $tfvarsPath -Raw |
| 130 | + |
| 131 | +$projectMatch = [regex]::Match($tfvarsContent, '(?m)^\s*project_name\s*=\s*"([^"]+)"') |
| 132 | +$regionMatch = [regex]::Match($tfvarsContent, '(?m)^\s*aws_region\s*=\s*"([^"]+)"') |
| 133 | + |
| 134 | +$TfProject = if ($projectMatch.Success) { $projectMatch.Groups[1].Value } else { 'game-servers' } |
| 135 | +$TfRegion = if ($regionMatch.Success) { $regionMatch.Groups[1].Value } else { 'us-east-1' } |
| 136 | + |
| 137 | +$TfStateBucket = "$TfProject-tf-state" |
| 138 | +$TfLockTable = "$TfProject-tf-locks" |
| 139 | + |
| 140 | +Write-Host "" |
| 141 | +Write-Host " Bootstrapping S3 backend (bucket: $TfStateBucket, region: $TfRegion)..." |
| 142 | + |
| 143 | +# S3 bucket |
| 144 | +$bucketExists = $false |
| 145 | +try { |
| 146 | + aws s3api head-bucket --bucket $TfStateBucket --region $TfRegion 2>$null |
| 147 | + $bucketExists = ($LASTEXITCODE -eq 0) |
| 148 | +} catch { $bucketExists = $false } |
| 149 | + |
| 150 | +if ($bucketExists) { |
| 151 | + Write-Host " S3 bucket $TfStateBucket already exists - skipping." |
| 152 | +} else { |
| 153 | + Write-Host " Creating S3 bucket $TfStateBucket..." |
| 154 | + if ($TfRegion -eq 'us-east-1') { |
| 155 | + Invoke-Native { aws s3api create-bucket --bucket $TfStateBucket --region $TfRegion } |
| 156 | + } else { |
| 157 | + Invoke-Native { |
| 158 | + aws s3api create-bucket ` |
| 159 | + --bucket $TfStateBucket ` |
| 160 | + --region $TfRegion ` |
| 161 | + --create-bucket-configuration "LocationConstraint=$TfRegion" |
| 162 | + } |
| 163 | + } |
| 164 | + Invoke-Native { |
| 165 | + aws s3api put-bucket-versioning ` |
| 166 | + --bucket $TfStateBucket ` |
| 167 | + --versioning-configuration Status=Enabled ` |
| 168 | + --region $TfRegion |
| 169 | + } |
| 170 | + Invoke-Native { |
| 171 | + aws s3api put-public-access-block ` |
| 172 | + --bucket $TfStateBucket ` |
| 173 | + --public-access-block-configuration 'BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true' ` |
| 174 | + --region $TfRegion |
| 175 | + } |
| 176 | + Invoke-Native { |
| 177 | + aws s3api put-bucket-encryption ` |
| 178 | + --bucket $TfStateBucket ` |
| 179 | + --server-side-encryption-configuration '{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"},"BucketKeyEnabled":true}]}' ` |
| 180 | + --region $TfRegion |
| 181 | + } |
| 182 | +} |
| 183 | + |
| 184 | +# DynamoDB lock table |
| 185 | +$tableExists = $false |
| 186 | +try { |
| 187 | + aws dynamodb describe-table --table-name $TfLockTable --region $TfRegion 2>$null |
| 188 | + $tableExists = ($LASTEXITCODE -eq 0) |
| 189 | +} catch { $tableExists = $false } |
| 190 | + |
| 191 | +if ($tableExists) { |
| 192 | + Write-Host " DynamoDB table $TfLockTable already exists - skipping." |
| 193 | +} else { |
| 194 | + Write-Host " Creating DynamoDB lock table $TfLockTable..." |
| 195 | + Invoke-Native { |
| 196 | + aws dynamodb create-table ` |
| 197 | + --table-name $TfLockTable ` |
| 198 | + --attribute-definitions AttributeName=LockID,AttributeType=S ` |
| 199 | + --key-schema AttributeName=LockID,KeyType=HASH ` |
| 200 | + --billing-mode PAY_PER_REQUEST ` |
| 201 | + --region $TfRegion |
| 202 | + } |
| 203 | + Write-Host " Waiting for DynamoDB table to become ACTIVE..." |
| 204 | + Invoke-Native { aws dynamodb wait table-exists --table-name $TfLockTable --region $TfRegion } |
| 205 | +} |
| 206 | + |
| 207 | +# terraform init (with optional state migration) |
| 208 | +$tfInitArgs = @( |
| 209 | + "-backend-config=bucket=$TfStateBucket" |
| 210 | + "-backend-config=key=$TfProject/terraform.tfstate" |
| 211 | + "-backend-config=region=$TfRegion" |
| 212 | + "-backend-config=dynamodb_table=$TfLockTable" |
| 213 | + "-backend-config=encrypt=true" |
| 214 | +) |
| 215 | + |
| 216 | +if (Test-Path 'terraform.tfstate') { |
| 217 | + Write-Host " Local terraform.tfstate detected - migrating state to S3..." |
| 218 | + Invoke-Native { 'yes' | terraform init -migrate-state @tfInitArgs } |
| 219 | +} else { |
| 220 | + Invoke-Native { terraform init @tfInitArgs } |
| 221 | +} |
| 222 | + |
| 223 | +# --------------------------------------------------------------------------- |
| 224 | +# Done |
| 225 | +# --------------------------------------------------------------------------- |
| 226 | + |
| 227 | +Write-Host "" |
| 228 | +Write-Host " Setup complete!" |
| 229 | +Write-Host "" |
| 230 | +Write-Host " Next steps:" |
| 231 | +Write-Host " 1. Edit terraform/terraform.tfvars with your game servers and domain" |
| 232 | +Write-Host " 2. Run: cd terraform; terraform plan" |
| 233 | +Write-Host " 3. Run: cd terraform; terraform apply" |
| 234 | +Write-Host " 4. Run the management app:" |
| 235 | +Write-Host " Dev: cd app; npm run dev" |
| 236 | +Write-Host " Docker: docker compose up --build" |
| 237 | +Write-Host " 5. Open http://localhost:5173 (dev) or http://localhost:5000 (docker)" |
| 238 | +Write-Host "" |
| 239 | +Write-Host " Discord bot setup (serverless):" |
| 240 | +Write-Host " - Open Credentials tab in the web UI and save the Application ID," |
| 241 | +Write-Host " Bot Token, and Application Public Key from the Discord Developer Portal." |
| 242 | +Write-Host " - Copy the 'Interactions Endpoint URL' from the same tab and paste it" |
| 243 | +Write-Host " into the Discord Developer Portal under General Information." |
| 244 | +Write-Host " - Add a guild ID under the Guilds tab and click 'Register commands'." |
| 245 | +Write-Host "" |
0 commit comments