// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 // // `pnpm dev`: the whole app on this machine, against the fake platform, with demo data. // // 1. Postgres with the three roles a linked managed database exposes, in Docker (compose.dev.yaml), // unless POSTGRES_URL, POSTGRES_READER_URL and POSTGRES_MIGRATE_URL are set in your shell // 2. the fake platform of @conusai-kit/testing -- Blob, OCR, the AI gateway, audit, usage -- in this process // 3. the libraries and the worker, built; then the migrate job, then the demo data (`pnpm seed`) // 4. the worker, and the web shell under `next dev` // // It reads no file: a value you want different is a variable in your shell (`DEV_WEB_PORT=3200 pnpm dev`, // `POSTGRES_URL=... pnpm dev`). The values it uses are printed. The fake platform is selected on purpose // (CONUSAI_FAKE_PLATFORM=1) and the processes run with NODE_ENV=development, which is the only mode // the app accepts that switch in (config/src/env.ts). Ctrl+C stops everything but Postgres, whose data // stays; `docker compose -f compose.yaml -f compose.dev.yaml down` stops that too (add -v to drop the data). import { spawn, spawnSync } from "node:child_process" import { createHash, randomBytes } from "node:crypto" import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs" import { basename, dirname, join } from "node:path" import { fileURLToPath } from "node:url" import { startFakePlatform, validReply } from "@conusai-kit/testing/fake-platform" const root = join(dirname(fileURLToPath(import.meta.url)), "..") const say = (line) => console.log(`dev: ${line}`) const port = (name, fallback) => { const value = Number.parseInt(process.env[name] ?? "", 10) return Number.isInteger(value) && value > 0 && value < 65_536 ? value : fallback } const ports = { web: port("DEV_WEB_PORT", 3000), platform: port("DEV_PLATFORM_PORT", 4010), postgres: port("DEV_POSTGRES_PORT", 55_433), } /** The session secret, made once and kept (`.dev/` is ignored), so a restart does not sign everybody out. */ function sessionSecret() { if (process.env.AUTH_SESSION_SECRET) return process.env.AUTH_SESSION_SECRET const file = join(root, ".dev", "session-secret") if (existsSync(file)) return readFileSync(file, "utf8").trim() mkdirSync(dirname(file), { recursive: true }) const secret = randomBytes(32).toString("base64url") writeFileSync(file, `${secret}\n`, { mode: 0o600 }) return secret } const url = (role) => `postgresql://appdev_${role}:appdev_${role}@127.0.0.1:${ports.postgres}/appdev` const ownDatabase = ["POSTGRES_URL", "POSTGRES_READER_URL", "POSTGRES_MIGRATE_URL"].every((name) => process.env[name]) const platformUrl = `http://127.0.0.1:${ports.platform}` const environment = { ...process.env, NODE_ENV: "development", CONUSAI_FAKE_PLATFORM: "1", CONUSAI_API_URL: `${platformUrl}/api`, CONUSAI_API_TOKEN: "dev-token", CONUSAI_PROJECT_ID: "1", CONUSAI_ORG_ID: "dev", CONUSAI_PUBLIC_URL: `http://127.0.0.1:${ports.web}`, CONUSAI_RELEASE: "dev", POSTGRES_URL: process.env.POSTGRES_URL ?? url("app"), POSTGRES_READER_URL: process.env.POSTGRES_READER_URL ?? url("reader"), POSTGRES_MIGRATE_URL: process.env.POSTGRES_MIGRATE_URL ?? url("owner"), AUTH_SESSION_SECRET: sessionSecret(), AUTH_DEV_LOGIN: "1", AUTH_INSECURE_COOKIES: "1", DEV_POSTGRES_PORT: String(ports.postgres), // The development Postgres is the pgvector image (compose.dev.yaml), so the knowledge module's // vectors live there unless the shell chose otherwise. KNOWLEDGE_VECTOR_STORE: process.env.KNOWLEDGE_VECTOR_STORE ?? "pgvector", } /** The environment of every process but the migrate job: the owner's URL is the migrate job's alone. */ const { POSTGRES_MIGRATE_URL: ownerUrl, ...withoutOwner } = environment /** Runs a command to its end, printing what it prints; stops the run when it fails. */ function run(label, command, args, options = {}) { say(label) const result = spawnSync(command, args, { cwd: root, env: withoutOwner, stdio: "inherit", ...options }) if (result.status !== 0) { console.error(`dev: ${label} failed${result.error ? `: ${result.error.message}` : ""}`) process.exit(result.status ?? 1) } } const compose = [ "compose", "-p", `${basename(root)}-dev-${createHash("sha1").update(root).digest("hex").slice(0, 6)}`, "-f", "compose.yaml", "-f", "compose.dev.yaml", ] const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) async function startPostgres() { if (ownDatabase) { say("using the Postgres your POSTGRES_URL, POSTGRES_READER_URL and POSTGRES_MIGRATE_URL name") return } if (spawnSync("docker", ["info"], { stdio: "ignore" }).status !== 0) { console.error( "dev: Docker is not running, and Postgres is started in Docker. Start it, or set POSTGRES_URL, POSTGRES_READER_URL and POSTGRES_MIGRATE_URL to a database with the three roles of dev/postgres-roles.sql.", ) process.exit(1) } run(`starting Postgres on 127.0.0.1:${ports.postgres}`, "docker", [...compose, "up", "-d", "--wait", "postgres"], { env: environment, // compose interpolates every service of the file, the migrate job's owner URL included }) // Healthy means the server answers; the roles are created by the init script, which restarts the // server. "Ready" is the app's own login working. for (let attempt = 0; ; attempt += 1) { const probe = spawnSync( "docker", [ ...compose, "exec", "-T", "postgres", "psql", "-U", "appdev_app", "-h", "127.0.0.1", "-d", "appdev", "-c", "select 1", ], { cwd: root, env: withoutOwner, stdio: "ignore" }, ) if (probe.status === 0) return if (attempt > 120) { console.error("dev: Postgres did not accept the app's login in a minute") process.exit(1) } await sleep(500) } } const children = [] function start(name, command, args, cwd) { const child = spawn(command, args, { cwd, env: withoutOwner, detached: true, stdio: ["ignore", "pipe", "pipe"] }) for (const stream of [child.stdout, child.stderr]) { let pending = "" stream.on("data", (chunk) => { pending += chunk.toString("utf8") const lines = pending.split("\n") pending = lines.pop() ?? "" for (const line of lines) console.log(`[${name}] ${line}`) }) } child.on("exit", (code, signal) => { if (!stopping) { console.error(`dev: ${name} stopped (${signal ?? code}); stopping the rest`) void stop(1) } }) children.push(child) } let stopping = false let platform async function stop(code = 0) { if (stopping) return stopping = true for (const child of children) { try { if (child.pid !== undefined) process.kill(-child.pid, "SIGTERM") } catch { // already gone } } await platform?.close().catch(() => undefined) process.exit(code) } process.on("SIGINT", () => void stop(0)) process.on("SIGTERM", () => void stop(0)) await startPostgres() platform = await startFakePlatform(ports.platform) // Nothing scripts the gateway in a development run, so a structured request is answered with the // smallest value its schema accepts and what is built on a model runs to its end. platform.ai.respond(validReply) say(`the fake platform is at ${platform.url}`) run("building the libraries and the worker", "pnpm", ["run", "build:libs"]) run("building the worker", "pnpm", ["--filter", "@app/worker", "run", "build"]) run("migrating the database", "node", ["apps/worker/dist/migrate.js"], { env: { ...withoutOwner, POSTGRES_MIGRATE_URL: ownerUrl }, }) run("writing the demo data", "node", ["apps/worker/dist/seed-main.js"]) start("worker", "node", ["apps/worker/dist/main.js"], root) start( "web", process.execPath, ["node_modules/next/dist/bin/next", "dev", "-p", String(ports.web), "-H", "127.0.0.1"], join(root, "apps", "web"), ) for (let attempt = 0; ; attempt += 1) { try { if ((await fetch(`${environment.CONUSAI_PUBLIC_URL}/api/ready`)).ok) break } catch { // not yet } if (attempt > 240) { console.error("dev: the web shell was not ready in two minutes") await stop(1) } await sleep(500) } say("ready. Sign in as one of the demo people:") for (const [name, email] of [ ["Ada, owner of Acme Review and admin of Northwind", "ada@example.test"], ["Grace, owner of Northwind", "grace@example.test"], ["Linus, a viewer in Acme Review", "linus@example.test"], ]) { say(` ${environment.CONUSAI_PUBLIC_URL}/auth/dev-login?email=${email} ${name}`) } say("Ctrl+C stops it.")