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
180 lines
7.5 KiB
TypeScript
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')
|
|
}
|