UNPKG

@mesh-tech/mesh-cli

Version:

CLI for Mesh platform development utilities

116 lines (115 loc) 4.37 kB
import { spawn } from "child_process"; import * as net from "net"; import { detectContext, getPlatformBastionInfo, getDatabaseUrl, logInfo, logSuccess, logError, } from "../../utils/index.js"; async function startTunnelBackground(instanceId, rdsHost, rdsPort, localPort) { logInfo("Starting tunnel in background..."); const tunnel = spawn("aws", [ "ssm", "start-session", "--target", instanceId, "--document-name", "AWS-StartPortForwardingSessionToRemoteHost", "--parameters", JSON.stringify({ host: [rdsHost], portNumber: [String(rdsPort)], localPortNumber: [String(localPort)], }), ], { stdio: ["ignore", "ignore", "ignore"] }); const maxAttempts = 15; for (let attempt = 0; attempt < maxAttempts; attempt++) { if (tunnel.exitCode !== null) { throw new Error("Tunnel process died"); } const isListening = await checkPort(localPort); if (isListening) { logSuccess(`Tunnel started (PID: ${tunnel.pid})`); return tunnel; } await sleep(1000); } tunnel.kill(); throw new Error(`Tunnel failed to start after ${maxAttempts} seconds`); } function checkPort(port) { return new Promise((resolve) => { const socket = new net.Socket(); socket.setTimeout(500); socket.on("connect", () => { socket.destroy(); resolve(true); }); socket.on("timeout", () => { socket.destroy(); resolve(false); }); socket.on("error", () => { socket.destroy(); resolve(false); }); socket.connect(port, "localhost"); }); } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } export async function psqlCommand(options) { let platformTenant; let platformEnv; if (options.tenant && options.env) { platformTenant = options.tenant; platformEnv = options.env; logInfo(`Platform: Tenant=${platformTenant}, Env=${platformEnv}`); } else if (options.tenant || options.env) { logError("Both --tenant and --env are required"); logInfo("Usage: mesh db psql --tenant mesh --env dev-temporal --app rdc --app-tenant encore --app-stage dev"); process.exit(1); } else { const ctx = detectContext(); platformTenant = ctx.tenant; platformEnv = ctx.platformEnv; logInfo(`Auto-detected: Tenant=${platformTenant}, Env=${platformEnv}`); } const appTenant = options.appTenant ?? platformTenant; const appStage = options.appStage ?? platformEnv; const appName = options.app; if (appName) { logInfo(`App credentials: tenant=${appTenant}, stage=${appStage}, app=${appName}`); } const psqlCheck = spawn("which", ["psql"]); await new Promise((resolve, reject) => { psqlCheck.on("exit", (code) => { if (code !== 0) { logError("psql not found. Install with: brew install postgresql"); reject(new Error("psql not found")); } else { resolve(); } }); }); const bastion = await getPlatformBastionInfo(platformTenant, platformEnv); const rdsService = bastion.services.rds; if (!rdsService) { logError("RDS service not available in platform bastion"); process.exit(1); } const tunnel = await startTunnelBackground(bastion.instanceId, rdsService.host, rdsService.port, 5432); const cleanup = () => { logInfo("Stopping tunnel..."); tunnel.kill(); }; process.on("exit", cleanup); process.on("SIGINT", cleanup); process.on("SIGTERM", cleanup); const databaseUrl = await getDatabaseUrl(appTenant, appStage, { app: appName }); const parsed = new URL(databaseUrl); const username = parsed.username; const password = decodeURIComponent(parsed.password); const database = parsed.pathname.slice(1); console.log(""); logInfo(`Connecting to ${database} as ${username}...`); console.log(""); const psql = spawn("psql", ["-h", "localhost", "-p", "5432", "-U", username, "-d", database], { stdio: "inherit", env: { ...process.env, PGPASSWORD: password }, }); psql.on("exit", (code) => { cleanup(); process.exit(code ?? 0); }); }