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
JavaScript
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
};