// 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, and the // business services, e-mail and notifications of the deployment -- 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 `vite 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 { fakeNativeServices, 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 simulated = fakeNativeServices({ token: "dev-token" }) 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", ...simulated.deployment, 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 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) } } /** * `run` for a script that may call the fake platform, which this process serves (the seed binds the demo * organizations to the business services): a synchronous child would wait forever on this process. */ function runToEnd(label, command, args, options = {}) { say(label) return new Promise((resolve) => { const child = track( spawn(command, args, { cwd: root, env: withoutOwner, stdio: "inherit", ...options, detached: true }), ) child.on("error", (error) => { console.error(`dev: ${label} failed: ${error.message}`) void stop(1) }) child.on("exit", (code) => { // Cancellation owns completion; never advance from migrate to seed while stopping. if (stopping) return if (code !== 0) { console.error(`dev: ${label} failed`) void stop(code ?? 1) return } resolve() }) }) } 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) } } // pg-boss normally allows 30 seconds for in-flight work; leave room for its cleanup. const CHILD_TERM_GRACE_MS = 35_000 const CHILD_KILL_WAIT_MS = 5_000 const children = new Set() const groups = new Set() function groupExists(pid) { try { process.kill(-pid, 0) return true } catch (error) { if (error.code === "ESRCH") return false // Permission denial cannot prove absence; retain the group for the bounded wait. if (error.code === "EPERM") return true throw error } } function track(child) { children.add(child) if (child.pid !== undefined) groups.add(child.pid) child.once("close", () => { children.delete(child) // A leader's exit does not mean its descendants have exited. if (child.pid !== undefined && !groupExists(child.pid)) groups.delete(child.pid) }) return child } function signalGroups(signal) { for (const pid of groups) { try { process.kill(-pid, signal) } catch (error) { if (error.code !== "ESRCH") throw error groups.delete(pid) } } } async function waitForChildren(milliseconds) { const deadline = performance.now() + milliseconds while (true) { for (const pid of groups) if (!groupExists(pid)) groups.delete(pid) if (children.size === 0 && groups.size === 0) return true if (performance.now() >= deadline) return false await sleep(25) } } function start(name, command, args, cwd) { const child = track( 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) } }) } let stopping = false let platform async function stop(code = 0) { if (stopping) return stopping = true let exitCode = code signalGroups("SIGTERM") if (!(await waitForChildren(CHILD_TERM_GRACE_MS))) { // Only detached groups created and recorded by this dev process are signalled. signalGroups("SIGKILL") if (!(await waitForChildren(CHILD_KILL_WAIT_MS))) { console.error("dev: owned children did not terminate after SIGKILL") exitCode = 1 } } await platform?.close().catch(() => undefined) process.exit(exitCode) } process.on("SIGINT", () => void stop(0)) process.on("SIGTERM", () => void stop(0)) await startPostgres() platform = await startFakePlatform(ports.platform, { nativeServices: simulated.nativeServices, deployment: simulated.authority, }) // 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"]) await runToEnd("migrating the database", "node", ["apps/worker/dist/migrate.js"], { env: { ...withoutOwner, POSTGRES_MIGRATE_URL: ownerUrl }, }) await runToEnd("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/vite/bin/vite.js", "dev", "--port", String(ports.web), "--host", "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.")