UNPKG

trellis

Version:

Agentic State Engine — event-sourced causal graph with branching, decision traces, and realtime sync for AI-native applications

209 lines (201 loc) 6.79 kB
import { SPRITE_PUBLIC_HTTP_PORT, buildDeployUrl, validateDeployName } from "./chunk-3XCOAP6G.js"; import { SPRITE_ENSURE_BUN_SH, SPRITE_STOP_TRELLIS_DB_SH, assertSpriteCli, ensureSprite, ensureSpritePublicAccess, resolveSpritePublicUrl, runSpriteCopy, runSpriteExec, spriteStartTrellisDbSh } from "./chunk-HQD7LBM4.js"; import { updateConfig } from "./chunk-JA7AIHRK.js"; // src/server/deploy.ts import { execFile } from "child_process"; import { existsSync, writeFileSync, mkdirSync } from "fs"; import { dirname, join, resolve } from "path"; import { fileURLToPath } from "url"; import { promisify } from "util"; async function deploy(opts) { const { configDir = ".", onProgress = () => { } } = opts; const listenPort = opts.port ?? SPRITE_PUBLIC_HTTP_PORT; const name = validateDeployName(opts.name); const apiKey = opts.apiKey ?? generateSecret("spk_"); const jwtSecret = opts.jwtSecret ?? generateSecret("jws_"); if (opts.stub) { const url2 = buildDeployUrl(name); onProgress("Stub deploy \u2014 skipping Sprites provisioning"); onProgress("Writing .trellis-db.json..."); writeRemoteDeployConfig({ name, url: url2, apiKey, configDir }); return { url: url2, name, apiKey }; } onProgress("Checking Sprites CLI..."); await assertSpriteCli(); onProgress("Writing server entrypoint..."); const tmpDir = resolve(configDir, ".trellis-deploy"); if (!existsSync(tmpDir)) mkdirSync(tmpDir, { recursive: true }); const entrypoint = join(tmpDir, "server-entry.ts"); writeFileSync( entrypoint, generateServerEntrypoint({ port: listenPort, apiKey, jwtSecret }) ); onProgress("Bundling server with Bun..."); const bundlePath = join(tmpDir, "server.js"); await runBun([ "build", entrypoint, "--outfile", bundlePath, "--target", "bun", "--format", "esm" ]); onProgress(`Ensuring Sprite: ${name}...`); await ensureSprite(name); onProgress("Configuring public URL..."); await ensureSpritePublicAccess(name); const url = await resolveSpritePublicUrl(name); onProgress(`Uploading to Sprite: ${name}...`); await runSpriteExec(name, "mkdir -p /home/sprite/trellis-db"); await runSpriteCopy(bundlePath, name, "/home/sprite/trellis-db/server.js"); onProgress("Ensuring Bun is installed..."); const bunPath = await runSpriteExec(name, SPRITE_ENSURE_BUN_SH); if (!bunPath.includes("bun")) { throw new Error(`Bun install failed on sprite ${name}: ${bunPath || "(no output)"}`); } onProgress("Stopping previous server..."); await runSpriteExec(name, SPRITE_STOP_TRELLIS_DB_SH); onProgress("Starting server (sprite-env service)..."); const bun = bunPath.trim().split("\n").pop().trim(); await runSpriteExec( name, spriteStartTrellisDbSh({ bun, port: listenPort }) ); onProgress("Waiting for health check..."); await waitForDeployHealth(url, 6e4); onProgress("Writing .trellis-db.json..."); writeRemoteDeployConfig({ name, url, apiKey, configDir }); return { url, name, apiKey }; } function writeRemoteDeployConfig(opts) { updateConfig( { mode: "remote", url: opts.url, apiKey: opts.apiKey, spriteName: opts.name, deployedAt: (/* @__PURE__ */ new Date()).toISOString() }, opts.configDir ); } async function installSpriteDeviceKey(name, local, onProgress = () => { }) { const remoteDir = "/home/sprite/trellis-db/.trellis/devices"; const staged = join(resolve(process.cwd(), ".trellis-deploy"), "sprite-device.json"); if (!existsSync(dirname(staged))) mkdirSync(dirname(staged), { recursive: true }); writeFileSync(staged, JSON.stringify(local, null, 2)); onProgress(`Installing device key on sprite ${name}...`); await runSpriteExec(name, `mkdir -p ${remoteDir}`); await runSpriteCopy(staged, name, `${remoteDir}/local.json`); } async function waitForDeployHealth(url, timeoutMs) { const deadline = Date.now() + timeoutMs; const healthUrl = `${url.replace(/\/$/, "")}/health`; let lastStatus = 0; while (Date.now() < deadline) { try { const res = await fetch(healthUrl); lastStatus = res.status; if (res.ok) return; } catch { } await new Promise((r) => setTimeout(r, 2e3)); } throw new Error( `Deploy health check timed out (${healthUrl}${lastStatus ? `, last status ${lastStatus}` : ""})` ); } function generateSecret(prefix) { const bytes = new Uint8Array(24); crypto.getRandomValues(bytes); const b64 = btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); return `${prefix}${b64}`; } async function runBun(args) { const execFileAsync = promisify(execFile); try { await execFileAsync("bun", args); } catch (err) { throw new Error( `bun ${args[0]} failed (exit ${err.code ?? "?"}): ${err.stderr ?? err.message}` ); } } function resolveTrellisImports() { const root = process.env.TRELLIS_NODE_ROOT?.trim(); if (root) { return { server: join(root, "dist/server/index.js").replace(/\\/g, "/"), client: join(root, "dist/client/index.js").replace(/\\/g, "/") }; } const here = dirname(fileURLToPath(import.meta.url)); const packageRoots = [resolve(here, ".."), resolve(here, "..", "..")]; for (const packageRoot of packageRoots) { const server = join(packageRoot, "dist/server/index.js"); const client = join(packageRoot, "dist/client/index.js"); if (existsSync(server) && existsSync(client)) { return { server: server.replace(/\\/g, "/"), client: client.replace(/\\/g, "/") }; } } return { server: "trellis/server", client: "trellis/client" }; } function generateServerEntrypoint(opts) { const { server, client } = resolveTrellisImports(); return ` import { TenantPool, startServer, BlobStore } from '${server}'; import { readConfig, defaultLocalConfig, writeConfig } from '${client}'; import { join } from 'path'; import { existsSync } from 'fs'; const dbPath = '/home/sprite/trellis-db/data'; const configDir = '/home/sprite/trellis-db'; writeConfig(defaultLocalConfig(dbPath, { apiKey: '${opts.apiKey}', jwtSecret: '${opts.jwtSecret}', port: ${opts.port}, }), configDir); const config = readConfig(configDir)!; // Sprites VMs lack better-sqlite3 native bindings \u2014 use WASM sql.js backend. const pool = new TenantPool(dbPath, { backend: { backend: 'sqljs' } }); await pool.preload(); await startServer({ port: ${opts.port}, config, pool, presenceRelay: { path: '/rt', blobStore: () => new BlobStore('/home/sprite/trellis-db'), }, }); console.log('Trellis DB running on port ${opts.port}'); console.log(\`Listening on port ${opts.port}\`); `; } export { deploy, installSpriteDeviceKey, generateServerEntrypoint };