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