Files
WeHub Mirror 6bf8bebf51
CI / Test and Build (push) Failing after 1s
CI / Migrate Dev DB (push) Has been skipped
CI / Migrate DB (push) Has been skipped
CodeQL / Analyze actions (push) Has been cancelled
CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Detect Version (push) Has been cancelled
CI / Detect Desktop Changes (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/cron.Dockerfile, ubuntu-latest, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build AMD64 (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/cron.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/db.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/pii.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/realtime.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-8vcpu-ubuntu-2404-arm, ./docker/app.Dockerfile, linux-arm64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
Helm Chart / Lint, test, and validate chart (push) Has been cancelled
Helm Chart / Chart version bumped (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Build Dev ECR (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core) (push) Has been cancelled
CI / Promote Images (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Check Desktop Signing Secrets (push) Has been cancelled
CI / Desktop Release (push) Has been cancelled
CI / Create Desktop Prerelease (push) Has been cancelled
CI / Desktop Prerelease Build (push) Has been cancelled
CI / Publish Desktop Prerelease (push) Has been cancelled
CI / Prune Desktop Prereleases (push) Has been cancelled
Helm Chart / Install on kind and run helm test (push) Has been cancelled
WeHub snapshot of cb28d14c6f2c081de7a0d8729a8c816c9adef67a
2026-08-10 11:17:50 +08:00

171 lines
6.3 KiB
TypeScript

import { spawnSync } from 'node:child_process'
import { docker } from './db.ts'
import { type Detection, REDIS_CONTAINER } from './detect.ts'
import { ensureDocker } from './docker.ts'
import { SetupError } from './errors.ts'
import { redisPing, waitFor } from './probes.ts'
import * as p from './prompter.ts'
import { glyph, theme } from './theme.ts'
const LOCAL_URL = 'redis://localhost:6379'
/**
* Host port the managed container actually publishes. `startManagedRedis` falls
* back to 6380 when 6379 is taken, so assuming the default adopts the wrong
* Redis — or fails the ping and then collides on the container name.
*/
function managedRedisUrl(): string | null {
const result = spawnSync('docker', ['port', REDIS_CONTAINER, '6379/tcp'], { encoding: 'utf8' })
if (result.status !== 0) return null
const port = result.stdout.trim().split('\n')[0]?.split(':').pop()
return port ? `redis://localhost:${port}` : null
}
async function pingWithSpinner(url: string, label: string): Promise<boolean> {
const spin = p.spinner()
spin.start(label)
const ping = await redisPing(url)
spin.stop(ping.ok ? 'Redis reachable' : `${glyph.warn} ${ping.error}`)
return ping.ok
}
async function startManagedRedis(detection: Detection): Promise<string> {
const hostPort = detection.redisPortOpen ? 6380 : 6379
const url = `redis://localhost:${hostPort}`
// A prior sim-redis (unhealthy, or bound to a stale port) would collide on the
// name. Unlike Postgres this container has no data volume, so removing it
// loses nothing — no prompt needed. `rm -f` is a no-op when none exists.
spawnSync('docker', ['rm', '-f', REDIS_CONTAINER], { stdio: 'ignore' })
docker([
'run',
'-d',
'--name',
REDIS_CONTAINER,
'--label',
'managed-by=sim-setup',
'-p',
`${hostPort}:6379`,
'redis:7-alpine',
])
const spin = p.spinner()
spin.start(`Starting ${REDIS_CONTAINER} container on :${hostPort}…`)
const healthy = await waitFor(async () => (await redisPing(url)).ok, 30_000, 1000)
spin.stop(
healthy
? `Redis running in ${REDIS_CONTAINER} on :${hostPort}`
: `${glyph.fail} container did not become healthy`
)
if (!healthy) {
throw new SetupError('the Redis container failed to start.', [
`inspect: ${theme.command(`docker logs ${REDIS_CONTAINER}`)}`,
`remove and retry: ${theme.command(`docker rm -f ${REDIS_CONTAINER}`)} then re-run the wizard`,
])
}
return url
}
async function promptRedisUrl(existing?: string): Promise<string> {
const url = await p.text({
message: 'REDIS_URL',
initialValue: existing,
placeholder: LOCAL_URL,
validate: (value) => (value ? undefined : 'required'),
})
if (!(await pingWithSpinner(url, 'Pinging Redis…'))) {
throw new SetupError(`Redis at ${url} is not answering.`, [
'fix the URL and re-run, or skip Redis — single-instance runs fine without it',
])
}
return url
}
/**
* Non-interactive Redis for quick mode: adopt whatever already answers, else
* start the managed container. Quick's contract is sensible defaults with no
* questions, and Redis is not optional enough to skip — pub/sub (live Chat
* status, table events) has no PostgreSQL fallback. Returns null only when
* nothing answers and Docker is unavailable, so the caller can warn instead of
* failing the whole setup.
*/
export async function ensureRedis(detection: Detection, existing?: string): Promise<string | null> {
if (existing && (await redisPing(existing)).ok) return existing
if (detection.redisPortOpen && (await redisPing(LOCAL_URL)).ok) {
p.log.step(`Using the Redis already on :6379`)
return LOCAL_URL
}
if (detection.redisContainer?.managed) {
if (detection.redisContainer.state === 'stopped') docker(['start', REDIS_CONTAINER])
const managedUrl = managedRedisUrl()
if (managedUrl && (await waitFor(async () => (await redisPing(managedUrl)).ok, 15_000, 500))) {
p.log.step(`Reusing ${REDIS_CONTAINER}`)
return managedUrl
}
}
if (!(await ensureDocker(false))) {
p.log.warn(
'Docker is unavailable, so Redis was not configured — live Chat status and table events will not stream.'
)
return null
}
return startManagedRedis(detection)
}
/**
* Redis ladder, mirroring the Postgres one: reuse what's running (with
* consent), restart/start a wizard-managed container, or take a URL.
*/
export async function resolveRedis(detection: Detection, existing?: string): Promise<string> {
// A configured REDIS_URL wins over the port scan: it may point at a non-default
// port, or at a host that isn't this machine at all.
if (
existing &&
existing !== LOCAL_URL &&
(await pingWithSpinner(existing, `Pinging ${existing}…`))
) {
const keep = await p.confirm({ message: `Keep using ${existing}?`, initialValue: true })
if (keep) return existing
}
if (
detection.redisPortOpen &&
(await pingWithSpinner(LOCAL_URL, 'Redis found on :6379 — pinging…'))
) {
const adopt = await p.confirm({
message: 'Use the Redis already running on localhost:6379?',
initialValue: true,
})
if (adopt) return LOCAL_URL
}
if (detection.redisContainer?.managed) {
if (detection.redisContainer.state === 'stopped') docker(['start', REDIS_CONTAINER])
// Read the published port back rather than assuming the default.
const managedUrl = managedRedisUrl()
if (
managedUrl &&
(await pingWithSpinner(managedUrl, `Starting existing ${REDIS_CONTAINER} container…`))
) {
return managedUrl
}
p.log.warn(
`${REDIS_CONTAINER} exists but is not answering — starting a fresh one will replace it (it holds no data).`
)
}
const dockerAvailable = await ensureDocker(false)
const options: p.SelectOption<'container' | 'url'>[] = []
if (dockerAvailable) {
options.push({
value: 'container',
label: 'Start a Redis container for me',
hint: `redis:7-alpine, named ${REDIS_CONTAINER} — recommended`,
})
}
options.push({ value: 'url', label: 'Use an existing Redis', hint: 'paste a redis:// URL' })
if (!dockerAvailable) {
p.log.warn('Docker is not available, so the wizard cannot manage a Redis container for you.')
}
const choice = await p.select({ message: 'Where should Redis live?', options })
return choice === 'container' ? startManagedRedis(detection) : promptRedisUrl(existing)
}