@mesh-tech/mesh-cli
Version:
CLI for Mesh platform development utilities
116 lines (115 loc) • 4.37 kB
JavaScript
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);
});
}