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

180 lines
7.5 KiB
TypeScript

import { spawnSync } from 'node:child_process'
import { EMAIL_SETUP, STORAGE_SETUP } from '../capability-config.ts'
import { promptCapabilitySetup, stageCapabilitySetupTransition } from '../capability-setup.ts'
import type { Detection } from '../detect.ts'
import { ensureDocker } from '../docker.ts'
import { ROOT, readEnvFile, reconcileEnvValues } from '../env-files.ts'
import { SetupError } from '../errors.ts'
import { ensurePortsFree } from '../ports.ts'
import { httpHealth, waitFor } from '../probes.ts'
import * as p from '../prompter.ts'
import {
chatFlagValues,
collectSecrets,
mothershipOverride,
promptCopilotKey,
promptLlmKeys,
promptSecurity,
promptSignInProviders,
promptUnlocks,
} from '../steps.ts'
import { glyph, theme } from '../theme.ts'
import { APP_SIGNUP_URL, APP_URL } from '../urls.ts'
const REQUIRED_PORTS = [3000, 3002] as const
/**
* Host ports this compose project currently publishes. Read from the containers
* rather than assumed from the file, because what matters is what is bound right
* now — a project with only db/redis up publishes neither app port, so those
* still need the conflict check.
*/
function composePublishedPorts(composeFile: string): Set<number> {
const ids = spawnSync('docker', ['compose', '-f', composeFile, 'ps', '-q'], {
cwd: ROOT,
encoding: 'utf8',
})
const containers = ids.status === 0 ? ids.stdout.split('\n').filter(Boolean) : []
if (containers.length === 0) return new Set()
const inspect = spawnSync(
'docker',
[
'inspect',
...containers,
'--format',
'{{range $port, $bindings := .HostConfig.PortBindings}}{{range $bindings}}{{.HostPort}} {{end}}{{end}}',
],
{ encoding: 'utf8' }
)
if (inspect.status !== 0) return new Set()
const published = new Set<number>()
for (const token of inspect.stdout.split(/\s+/)) {
const port = Number(token)
if (Number.isInteger(port) && port > 0) published.add(port)
}
return published
}
/**
* Compose publishes 3000 and 3002 — resolve conflicts before touching docker,
* instead of letting `docker compose up` die halfway through startup. Aborting
* is fatal here: compose can't come up while the ports are held.
*/
async function ensureComposePortsFree(composeFile: string): Promise<void> {
// A port this stack already publishes is not a conflict — `docker compose up
// -d` reconciles its own containers, and reporting the install's own realtime
// container as a blocker (offering to kill Docker's listener) is never right.
// Skip only the ports this project actually publishes: leftover db/redis
// containers must not wave through a foreign process sitting on 3000, which
// would otherwise surface as a raw compose bind error instead of the prompt.
const ours = composePublishedPorts(composeFile)
const toCheck = REQUIRED_PORTS.filter((port) => !ours.has(port))
if (toCheck.length < REQUIRED_PORTS.length) {
const skipped = REQUIRED_PORTS.filter((port) => ours.has(port))
p.log.step(`Existing Sim containers hold :${skipped.join(', :')} — compose will reconcile them`)
}
if (toCheck.length === 0) return
if (await ensurePortsFree(toCheck)) return
throw new SetupError(`ports ${toCheck.map((port) => `:${port}`).join('/')} are in use`, [
`free the ports, then re-run: ${theme.command('bun run setup')}`,
`see what holds them: ${theme.command('lsof -nP -iTCP:3000 -sTCP:LISTEN')}`,
`stop a container publishing them: ${theme.command('docker ps')}`,
`compose file in play: ${composeFile}`,
])
}
export async function runComposeMode(detection: Detection, quick: boolean): Promise<void> {
await ensureDocker(true)
const variant = quick
? 'prod'
: await p.select({
message: 'Which images?',
options: [
{
value: 'prod',
label: 'Published images',
hint: 'pulls ghcr.io/simstudioai/* — fastest',
},
{
value: 'local',
label: 'Build from source',
hint: 'builds docker/*.Dockerfile — for testing local changes',
},
],
initialValue: 'prod',
})
const composeFile = variant === 'prod' ? 'docker-compose.prod.yml' : 'docker-compose.local.yml'
const root = readEnvFile('root')
const values = collectSecrets(root)
const remove = new Set<string>()
// Before the key is minted: a half-set override mints against one environment
// and validates against the other, and warning afterwards is too late — the
// bad key is already stored, and the next run offers to keep it.
Object.assign(values, mothershipOverride())
const copilotKey = await promptCopilotKey(root.vars.get('COPILOT_API_KEY'))
if (copilotKey) values.COPILOT_API_KEY = copilotKey
Object.assign(values, chatFlagValues(copilotKey))
Object.assign(values, await promptLlmKeys(detection, !quick))
if (!quick) {
const stagedVars = new Map(root.vars)
for (const [key, value] of Object.entries(values)) stagedVars.set(key, value)
const storage = await promptCapabilitySetup(STORAGE_SETUP, stagedVars, {
containerized: true,
})
stageCapabilitySetupTransition(stagedVars, values, remove, storage)
const appUrl = root.vars.get('NEXT_PUBLIC_APP_URL') ?? APP_URL
Object.assign(values, await promptSignInProviders(stagedVars, appUrl))
const email = await promptCapabilitySetup(EMAIL_SETUP, stagedVars, {
containerized: true,
})
stageCapabilitySetupTransition(stagedVars, values, remove, email)
const security = await promptSecurity(root.vars)
Object.assign(values, security.sim, security.mirrorToRealtime)
Object.assign(values, await promptUnlocks(root.vars))
}
if (!root.vars.get('LOG_LEVEL')) {
values.LOG_LEVEL = 'INFO'
p.log.step(
'Set LOG_LEVEL=INFO (production containers default to ERROR, which hides startup problems)'
)
}
if (!root.vars.get('NEXT_TELEMETRY_DISABLED')) values.NEXT_TELEMETRY_DISABLED = '1'
for (const key of Object.keys(values)) remove.delete(key)
reconcileEnvValues('root', [...remove], values)
p.log.step('Wrote .env (compose reads it for variable substitution)')
await ensureComposePortsFree(composeFile)
p.log.step(`Running docker compose -f ${composeFile} up -d`)
const result = spawnSync('docker', ['compose', '-f', composeFile, 'up', '-d'], {
cwd: ROOT,
stdio: 'inherit',
})
if (result.status !== 0) {
throw new SetupError(`docker compose exited with ${result.status}.`, [
`inspect what failed: ${theme.command(`docker compose -f ${composeFile} logs --tail 50`)}`,
`container status: ${theme.command(`docker compose -f ${composeFile} ps`)}`,
`clean slate: ${theme.command(`docker compose -f ${composeFile} down`)} then re-run the wizard`,
])
}
const spin = p.spinner()
spin.start('Waiting for Sim to come up (first run pulls images and migrates)…')
const appHealthy = await waitFor(() => httpHealth(`${APP_URL}/api/health`), 300_000, 3000)
const realtimeHealthy =
appHealthy && (await waitFor(() => httpHealth('http://localhost:3002/health'), 60_000, 2000))
if (!appHealthy || !realtimeHealthy) {
spin.stop(`${glyph.fail} services did not become healthy`)
throw new SetupError(
`${!appHealthy ? 'the app (:3000)' : 'realtime (:3002)'} never answered its health check.`,
[
`follow the logs: ${theme.command(`docker compose -f ${composeFile} logs -f`)}`,
`first boots on slow disks can exceed the wait — if containers are still starting, just wait and open ${APP_SIGNUP_URL}`,
]
)
}
spin.stop('App and realtime are healthy')
}