mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-26 11:09:42 +00:00
07e9a35044
Brings up a fully local storyteller-web: portable MySQL 8.4 and Redis under a gitignored .devstack/ (plain user processes -- no admin, no services, no WSL, no Docker), all 281 migrations, role/badge seeds, and a demo login with credits. Windows and Linux variants, plus a doctor that reports what is and isn't healthy. Scripts and docs only. Nothing here is compiled into any binary, and no application behaviour changes: the backend it starts is the one that ships. Integrations that cannot be provided locally -- object storage, generation providers, email -- get placeholder credentials, so those endpoints fail when exercised. Swap in a real dev credential for the one you are working on. The doc records exactly which areas that leaves working. An earlier revision closed those gaps with development-only behaviour inside storyteller-web (DEV_FAKE_GENERATION, a filesystem bucket mode, a CDN_BASE_URL override). That is deliberately not here: a switch that exists in the shipped binary can be flipped. Frontend work that wants a populated, working stack should use frontend/apps/fake-storyteller-web instead.
235 lines
9.6 KiB
PowerShell
235 lines
9.6 KiB
PowerShell
# Shared helpers for the Windows dev-stack bootstrap scripts.
|
|
#
|
|
# Dot-source from a sibling script:
|
|
# . (Join-Path $PSScriptRoot "common.ps1")
|
|
#
|
|
# Design: the stack is PORTABLE - MySQL and Redis live under the gitignored
|
|
# .devstack/ directory and run as plain user processes. No admin rights, no
|
|
# Windows services, no system-wide installs (build tools excepted).
|
|
# See _docs/dev_setup_local_stack.md.
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
$script:RootDir = (Resolve-Path (Join-Path $PSScriptRoot "..\..\..")).Path
|
|
|
|
# Canonical local-dev database identity - must match _docs/dev_setup_server.md,
|
|
# the repo-root .env DATABASE_URL, and the server's MYSQL_URL default.
|
|
$script:DevMySqlDb = if ($env:DEV_MYSQL_DB) { $env:DEV_MYSQL_DB } else { "storyteller" }
|
|
$script:DevMySqlUser = if ($env:DEV_MYSQL_USER) { $env:DEV_MYSQL_USER } else { "storyteller" }
|
|
$script:DevMySqlPassword = if ($env:DEV_MYSQL_PASSWORD) { $env:DEV_MYSQL_PASSWORD } else { "password" }
|
|
|
|
$script:DevBackendUrl = if ($env:DEV_BACKEND_URL) { $env:DEV_BACKEND_URL } else { "http://localhost:12345" }
|
|
|
|
# Default demo account (see seed_demo_user.ps1). "demo"/"admin"/"test"/"dev"
|
|
# are on the backend's reserved-usernames list - don't use those.
|
|
$script:DemoUsername = if ($env:DEMO_USERNAME) { $env:DEMO_USERNAME } else { "localdev1" }
|
|
$script:DemoPassword = if ($env:DEMO_PASSWORD) { $env:DEMO_PASSWORD } else { "localdev1pass" }
|
|
$script:DemoEmail = if ($env:DEMO_EMAIL) { $env:DEMO_EMAIL } else { "localdev1@example.com" }
|
|
|
|
# Banked credits the demo user's artcraft wallet is seeded/topped-up to.
|
|
# Generation submits are gated on wallet balance, so 0 credits means the
|
|
# generate button silently refuses in the webapp.
|
|
$script:DemoCredits = if ($env:DEMO_CREDITS) { [uint32]$env:DEMO_CREDITS } else { 100000 }
|
|
|
|
# --- Portable service pins -------------------------------------------------
|
|
# MySQL 8.4 LTS (the 8.x series is required; 9.x is unsupported by tooling).
|
|
# cdn.mysql.com hosts only some patch versions; 8.4.11 verified available.
|
|
$script:MySqlVersion = "8.4.11"
|
|
$script:MySqlZipUrl = "https://cdn.mysql.com/Downloads/MySQL-8.4/mysql-$MySqlVersion-winx64.zip"
|
|
|
|
# Redis for Windows (tporadowski build). Old (5.0) but stable and sufficient
|
|
# for the backend's usage (keepalive/TTL/rate-limiter commands). Swap to
|
|
# Memurai or Microsoft Garnet by pointing REDIS_ZIP_URL elsewhere.
|
|
$script:RedisZipUrl = if ($env:REDIS_ZIP_URL) { $env:REDIS_ZIP_URL } else {
|
|
"https://github.com/tporadowski/redis/releases/download/v5.0.14.1/Redis-x64-5.0.14.1.zip"
|
|
}
|
|
|
|
$script:DevStackDir = Join-Path $RootDir ".devstack"
|
|
$script:DownloadsDir = Join-Path $DevStackDir "downloads"
|
|
$script:MySqlBaseDir = Join-Path $DevStackDir "mysql\mysql-$MySqlVersion-winx64"
|
|
$script:MySqlDataDir = Join-Path $DevStackDir "mysql-data"
|
|
$script:MySqlIniPath = Join-Path $DevStackDir "my.ini"
|
|
$script:RedisDir = Join-Path $DevStackDir "redis"
|
|
$script:LogsDir = Join-Path $DevStackDir "logs"
|
|
$script:PidsDir = Join-Path $DevStackDir "pids"
|
|
|
|
$script:SecretsEnvFile = Join-Path $RootDir "crates\service\web\storyteller_web\config\storyteller-web.development-secrets.env"
|
|
|
|
function Write-Log([string]$Message) { Write-Host "[bootstrap] $Message" }
|
|
function Write-Warn2([string]$Message) { Write-Host "[bootstrap] WARNING: $Message" -ForegroundColor Yellow }
|
|
function Die([string]$Message) { Write-Host "[bootstrap] ERROR: $Message" -ForegroundColor Red; exit 1 }
|
|
function Step([string]$Title) { Write-Host ""; Write-Host "=== $Title ===" -ForegroundColor Cyan }
|
|
|
|
function Confirm-Or-Skip([string]$Prompt) {
|
|
if ($script:AssumeYes) { return $true }
|
|
$reply = Read-Host "$Prompt [y/N]"
|
|
return ($reply -match '^[Yy]')
|
|
}
|
|
|
|
function Test-TcpPort([int]$Port) {
|
|
$client = New-Object System.Net.Sockets.TcpClient
|
|
try {
|
|
$async = $client.BeginConnect("127.0.0.1", $Port, $null, $null)
|
|
if (-not $async.AsyncWaitHandle.WaitOne(1500)) { return $false }
|
|
$client.EndConnect($async)
|
|
return $true
|
|
} catch {
|
|
return $false
|
|
} finally {
|
|
$client.Close()
|
|
}
|
|
}
|
|
|
|
# Resolve a client/server exe: prefer the portable install, fall back to PATH.
|
|
function Get-MySqlTool([string]$Name) {
|
|
$portable = Join-Path $MySqlBaseDir "bin\$Name.exe"
|
|
if (Test-Path $portable) { return $portable }
|
|
$cmd = Get-Command $Name -ErrorAction SilentlyContinue
|
|
if ($cmd) { return $cmd.Source }
|
|
return $null
|
|
}
|
|
|
|
function Get-RedisTool([string]$Name) {
|
|
$portable = Join-Path $RedisDir "$Name.exe"
|
|
if (Test-Path $portable) { return $portable }
|
|
$cmd = Get-Command $Name -ErrorAction SilentlyContinue
|
|
if ($cmd) { return $cmd.Source }
|
|
return $null
|
|
}
|
|
|
|
# Generate a token in the backend's format: {prefix}{crockford-lower entropy}
|
|
# padded to the same TOTAL length the Rust generators use (tokens crate,
|
|
# impl_crockford_generator!). Alphabet excludes i/l/o/u like Crockford base32.
|
|
function New-DevToken([string]$Prefix, [int]$TotalLength) {
|
|
$charset = "0123456789abcdefghjkmnpqrstvwxyz"
|
|
$count = $TotalLength - $Prefix.Length
|
|
$chars = 1..$count | ForEach-Object { $charset[(Get-Random -Maximum $charset.Length)] }
|
|
return "$Prefix$(-join $chars)"
|
|
}
|
|
|
|
# Run SQL as the app user against the dev database. Returns output lines
|
|
# (stdout and stderr merged via Invoke-Native, so a caller capturing our
|
|
# streams can never turn a mysql warning into a terminating error).
|
|
# Password goes via MYSQL_PWD so it stays off the command line.
|
|
# -Quiet discards output (for probes that are expected to fail, e.g.
|
|
# Test-MySqlApp before provisioning). Check $LASTEXITCODE for success.
|
|
function Invoke-MySqlApp([string]$Sql, [string]$SourceFile, [switch]$Quiet) {
|
|
$mysql = Get-MySqlTool "mysql"
|
|
if (-not $mysql) { Die "mysql client not found (run bootstrap_dev_stack.ps1 first)." }
|
|
$prev = $env:MYSQL_PWD
|
|
$env:MYSQL_PWD = $DevMySqlPassword
|
|
try {
|
|
if ($SourceFile) {
|
|
$forward = $SourceFile -replace '\\', '/'
|
|
$out = Invoke-Native "`"$mysql`" -u $DevMySqlUser -h 127.0.0.1 -D $DevMySqlDb -e `"source $forward`""
|
|
} else {
|
|
$out = Invoke-Native "`"$mysql`" -u $DevMySqlUser -h 127.0.0.1 -D $DevMySqlDb -N -e `"$Sql`""
|
|
}
|
|
if (-not $Quiet) { return $out }
|
|
} finally {
|
|
$env:MYSQL_PWD = $prev
|
|
}
|
|
}
|
|
|
|
# Run a query expected to return a single non-negative integer (e.g. COUNT(*)).
|
|
# Returns -1 if the query failed or produced no numeric line, so callers can
|
|
# distinguish "table is empty" (0) from "query failed" (-1).
|
|
function Get-MySqlCount([string]$Sql) {
|
|
$out = @(Invoke-MySqlApp -Sql $Sql)
|
|
if ($LASTEXITCODE -ne 0) { return -1 }
|
|
$line = $out | Where-Object { "$_" -match '^\d+$' } | Select-Object -First 1
|
|
if ($null -eq $line) { return -1 }
|
|
return [int]"$line"
|
|
}
|
|
|
|
function Test-MySqlApp {
|
|
try {
|
|
Invoke-MySqlApp -Sql "SELECT 1" -Quiet
|
|
return ($LASTEXITCODE -eq 0)
|
|
} catch {
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Test-Redis {
|
|
$cli = Get-RedisTool "redis-cli"
|
|
if (-not $cli) { return $false }
|
|
try {
|
|
$pong = Invoke-Native "`"$cli`" -h 127.0.0.1 ping"
|
|
return ("$pong" -match "PONG")
|
|
} catch {
|
|
return $false
|
|
}
|
|
}
|
|
|
|
function Test-Backend {
|
|
try {
|
|
$r = Invoke-WebRequest -Uri "$DevBackendUrl/_status" -UseBasicParsing -TimeoutSec 3
|
|
return ($r.StatusCode -eq 200)
|
|
} catch {
|
|
return $false
|
|
}
|
|
}
|
|
|
|
# Start a detached background process, remembering its PID for stop_dev_services.
|
|
function Start-DevProcess([string]$Name, [string]$Exe, [string[]]$Arguments) {
|
|
New-Item -ItemType Directory -Force $PidsDir | Out-Null
|
|
New-Item -ItemType Directory -Force $LogsDir | Out-Null
|
|
$out = Join-Path $LogsDir "$Name.out.log"
|
|
$err = Join-Path $LogsDir "$Name.err.log"
|
|
$proc = Start-Process -FilePath $Exe -ArgumentList $Arguments -WindowStyle Hidden `
|
|
-RedirectStandardOutput $out -RedirectStandardError $err -PassThru
|
|
Set-Content -Path (Join-Path $PidsDir "$Name.pid") -Value $proc.Id -Encoding ascii
|
|
return $proc
|
|
}
|
|
|
|
function Wait-ForCondition([scriptblock]$Condition, [int]$TimeoutSeconds, [string]$What) {
|
|
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
|
while ((Get-Date) -lt $deadline) {
|
|
if (& $Condition) { return $true }
|
|
Start-Sleep -Seconds 1
|
|
}
|
|
Die "Timed out after ${TimeoutSeconds}s waiting for: $What"
|
|
}
|
|
|
|
# Run a native command line via cmd so its stderr is merged OUTSIDE PowerShell.
|
|
# PS 5.1 wraps captured native stderr lines in ErrorRecords; combined with our
|
|
# $ErrorActionPreference = "Stop", a harmless warning (e.g. from cargo) would
|
|
# otherwise abort the script whenever a caller captures/redirects our output.
|
|
# Quote embedded paths: Invoke-Native "`"$exe`" --flag value"
|
|
function Invoke-Native([string]$CommandLine) {
|
|
cmd /s /c "$CommandLine 2>&1"
|
|
}
|
|
|
|
# Make freshly-installed tools visible in this session without a new shell.
|
|
function Add-SessionPath([string[]]$Dirs) {
|
|
foreach ($d in $Dirs) {
|
|
if ((Test-Path $d) -and (($env:Path -split ';') -notcontains $d)) {
|
|
$env:Path = "$d;$env:Path"
|
|
}
|
|
}
|
|
}
|
|
|
|
# The portable MySQL's client DLL + import lib, needed to build AND run
|
|
# diesel-cli with the mysql feature.
|
|
function Add-MySqlClientToSession {
|
|
Add-SessionPath @((Join-Path $MySqlBaseDir "bin"), (Join-Path $MySqlBaseDir "lib"))
|
|
}
|
|
|
|
# Native build tools (cmake/perl/nasm/llvm) land in these dirs when installed
|
|
# via winget; a shell opened before the install won't have them on PATH yet.
|
|
# bindgen (used by boring2 / aws-lc-sys) additionally wants LIBCLANG_PATH.
|
|
function Add-BuildToolsToSession {
|
|
Add-SessionPath @(
|
|
"$env:ProgramFiles\CMake\bin",
|
|
"C:\Strawberry\perl\bin",
|
|
"$env:ProgramFiles\NASM",
|
|
"$env:LOCALAPPDATA\bin\NASM",
|
|
"$env:ProgramFiles\LLVM\bin"
|
|
)
|
|
if (-not $env:LIBCLANG_PATH) {
|
|
$clang = Get-Command clang -ErrorAction SilentlyContinue
|
|
if ($clang) { $env:LIBCLANG_PATH = Split-Path $clang.Source }
|
|
}
|
|
}
|