UNPKG

trellis

Version:

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

315 lines (308 loc) 9.92 kB
import { DeployNameError, SPRITE_PUBLIC_HTTP_PORT, buildDeployUrl, validateDeployName } from "./chunk-3XCOAP6G.js"; import { SPRITE_ENSURE_BUN_SH, assertSpriteCli, ensureSprite, ensureSpritePublicAccess, resolveSpritePublicUrl, runSpriteCopy, runSpriteExec, spriteStartServiceSh, spriteStopServiceSh } from "./chunk-HQD7LBM4.js"; import { gatewayConfigPath, listRegisteredRooms, writeGatewayConfig } from "./chunk-YC5I32PS.js"; import "./chunk-UZRUP7QW.js"; import "./chunk-JA7AIHRK.js"; import "./chunk-2ESYSVXG.js"; // src/cli/gateway-deploy-cli.ts import chalk from "chalk"; // src/server/deploy-gateway.ts import { execFile } from "child_process"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; import { dirname, join, resolve } from "path"; import { fileURLToPath } from "url"; import { promisify } from "util"; var GATEWAY_REMOTE_DIR = "/home/sprite/trellis-mcp-gateway"; var GATEWAY_SERVICE = "trellis-mcp-gateway"; async function deployMcpGateway(opts) { const { configDir = ".", onProgress = () => { } } = opts; const listenPort = opts.port ?? SPRITE_PUBLIC_HTTP_PORT; const name = validateDeployName(opts.name); const publicUrl = (opts.publicUrl ?? "https://mcp.trellis.computer").replace( /\/$/, "" ); const rooms = buildRoomsManifest(configDir, opts.roomsFile); onProgress(`Room registry: ${rooms.length} room(s)`); if (opts.stub) { const url2 = buildDeployUrl(name); onProgress("Stub deploy \u2014 skipping Sprites provisioning"); writeGatewayConfig( { url: url2, publicUrl, spriteName: name, deployedAt: (/* @__PURE__ */ new Date()).toISOString(), port: listenPort }, configDir ); writeRoomsFile(configDir, rooms); return { url: url2, publicUrl, name, roomCount: rooms.length }; } onProgress("Checking Sprites CLI..."); await assertSpriteCli(); const tmpDir = resolve(configDir, ".trellis-deploy-gateway"); if (!existsSync(tmpDir)) mkdirSync(tmpDir, { recursive: true }); const entrypoint = join(tmpDir, "gateway-entry.ts"); writeFileSync( entrypoint, generateGatewayEntrypoint({ port: listenPort, publicUrl }) ); onProgress("Bundling gateway with Bun..."); const bundlePath = join(tmpDir, "gateway.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 gateway bundle + room registry..."); await runSpriteExec(name, `mkdir -p ${GATEWAY_REMOTE_DIR}`); await runSpriteCopy(bundlePath, name, `${GATEWAY_REMOTE_DIR}/gateway.js`); const roomsPath = join(tmpDir, "rooms.json"); writeFileSync(roomsPath, JSON.stringify(rooms, null, 2) + "\n", "utf8"); await runSpriteCopy(roomsPath, name, `${GATEWAY_REMOTE_DIR}/.trellis-rooms.json`); 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 gateway..."); await runSpriteExec( name, spriteStopServiceSh(GATEWAY_SERVICE, "[t]rellis-gateway/gateway.js") ); onProgress("Starting gateway (sprite-env service)..."); const bun = bunPath.trim().split("\n").pop().trim(); await runSpriteExec( name, spriteStartServiceSh({ service: GATEWAY_SERVICE, bun, script: "gateway.js", dir: GATEWAY_REMOTE_DIR, port: listenPort }) ); onProgress("Waiting for health check..."); await waitForHealth(url, 6e4); writeGatewayConfig( { url, publicUrl, spriteName: name, deployedAt: (/* @__PURE__ */ new Date()).toISOString(), port: listenPort }, configDir ); writeRoomsFile(configDir, rooms); try { const { trackSprite } = await import("./vm-config-YZRJ3KUB.js"); trackSprite(name, { url, hasTrellis: false }); } catch { } return { url, publicUrl, name, roomCount: rooms.length }; } function buildRoomsManifest(configDir, roomsFile) { const byUrl = /* @__PURE__ */ new Map(); for (const room of listRegisteredRooms({ configDir })) { byUrl.set(room.url, room); } if (roomsFile && existsSync(roomsFile)) { try { const extra = JSON.parse(readFileSync(roomsFile, "utf8")); const list = Array.isArray(extra) ? extra : extra.rooms ?? []; for (const room of list) { if (room.name && room.url) { byUrl.set(room.url, { ...room, mcpUrl: room.mcpUrl ?? `${room.url.replace(/\/$/, "")}/mcp`, source: room.source ?? "registry" }); } } } catch { } } return [...byUrl.values()].sort((a, b) => a.name.localeCompare(b.name)); } function writeRoomsFile(configDir, rooms) { const path = resolve(configDir, ".trellis-rooms.json"); writeFileSync(path, JSON.stringify(rooms, null, 2) + "\n", "utf8"); } function resolveGatewayImport() { const here = dirname(fileURLToPath(import.meta.url)); const packageRoots = [ resolve(here, ".."), resolve(here, "..", "..") ]; const tried = []; for (const packageRoot of packageRoots) { const candidates = [ join(packageRoot, "dist", "mcp", "gateway-serve.js"), join(packageRoot, "src", "mcp", "gateway-serve.ts") ]; for (const candidate of candidates) { tried.push(candidate); if (existsSync(candidate)) { return resolve(candidate).replace(/\\/g, "/"); } } } throw new Error( `gateway-serve module not found (tried: ${tried.join(", ")})` ); } function generateGatewayEntrypoint(opts) { const mod = resolveGatewayImport(); return ` import { startGatewayServer } from '${mod}'; process.env.TRELLIS_MCP_GATEWAY_PUBLIC_URL = '${opts.publicUrl}'; process.env.TRELLIS_ROOM_REGISTRY = '${GATEWAY_REMOTE_DIR}/.trellis-rooms.json'; await startGatewayServer({ port: ${opts.port}, configDir: '${GATEWAY_REMOTE_DIR}', publicUrl: '${opts.publicUrl}', }); console.log('Trellis MCP gateway listening on port ${opts.port}'); console.log('Public URL: ${opts.publicUrl}'); `; } async function waitForHealth(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) { const body = await res.json(); if (body.service === "trellis-mcp-gateway") return; } } catch { } await new Promise((r) => setTimeout(r, 2e3)); } throw new Error( `Gateway health check timed out (${healthUrl}${lastStatus ? `, last status ${lastStatus}` : ""})` ); } 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}` ); } } // src/cli/gateway-deploy-cli.ts function printGatewayDeploySuccess(result, configDir, stub) { const spriteHost = new URL(result.url).hostname; console.log(""); console.log( chalk.green( stub ? "\u2713 Stub gateway deploy \u2014 config written (no Sprite provisioned)" : "\u2713 Trellis MCP discovery gateway deployed" ) ); console.log(chalk.dim(` Sprite: ${result.name}`)); console.log(chalk.dim(` Origin: ${result.url}`)); console.log(chalk.dim(` Public: ${result.publicUrl}`)); console.log(chalk.dim(` Rooms: ${result.roomCount}`)); console.log(chalk.dim(` Config: ${gatewayConfigPath(configDir)}`)); console.log(""); console.log(chalk.bold("MCP (use public URL in Cursor):")); console.log(chalk.cyan(` ${result.publicUrl}/gateway/mcp`)); console.log( chalk.dim( " On Sprites, /gateway/mcp is canonical (platform reserves /mcp)." ) ); console.log(""); console.log(chalk.bold("DNS (mcp.trellis.computer):")); console.log( chalk.dim( " Vercel proxy: deploy trellis-node/apps/mcp-gateway-proxy, add domain, CNAME mcp \u2192 Vercel" ) ); console.log( chalk.dim( ` Or direct CNAME \u2192 ${spriteHost} (TLS on vanity host needs Sprites custom domains)` ) ); console.log(""); console.log(chalk.bold("Until DNS propagates, use the Sprites origin:")); console.log(chalk.cyan(` ${result.url}/mcp`)); console.log(""); console.log(chalk.bold("Cursor:")); console.log( chalk.cyan( ` { "url": "${result.publicUrl}/mcp" } // or ${result.url}/mcp` ) ); } async function runGatewayDeployCli(opts) { const configDir = opts.configDir ?? "."; const port = opts.port === void 0 ? void 0 : typeof opts.port === "string" ? parseInt(opts.port, 10) : opts.port; const label = opts.stub ? "Stub gateway deploy" : "Deploying MCP gateway"; console.log(chalk.bold(`${label}: ${opts.name}...`)); try { const result = await deployMcpGateway({ name: opts.name, publicUrl: opts.publicUrl, port, configDir, roomsFile: opts.roomsFile, stub: !!opts.stub, onProgress: (msg) => console.log(chalk.dim(` ${msg}`)) }); printGatewayDeploySuccess(result, configDir, !!opts.stub); } catch (err) { if (err instanceof DeployNameError) { console.error(chalk.red(`Invalid name: ${err.message}`)); process.exit(1); } const message = err instanceof Error ? err.message : String(err); console.error(chalk.red(`Gateway deploy failed: ${message}`)); process.exit(1); } } export { printGatewayDeploySuccess, runGatewayDeployCli };