UNPKG

@mesh-tech/mesh-cli

Version:

CLI for Mesh platform development utilities

109 lines (108 loc) 3.81 kB
import { spawn } from "child_process"; import { SecretsManagerClient, GetSecretValueCommand, } from "@aws-sdk/client-secrets-manager"; function logInfo(msg) { console.log(`\u001b[36mℹ\u001b[0m ${msg}`); } function logSuccess(msg) { console.log(`\u001b[32m✓\u001b[0m ${msg}`); } function logError(msg) { console.error(`\u001b[31m✗\u001b[0m ${msg}`); } function parseArgs(args) { const options = {}; let commandArgs = []; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === "--") { commandArgs = args.slice(i + 1); break; } if (arg.startsWith("--secret=")) { options.secret = arg.substring("--secret=".length); } else if (arg === "--secret" && args[i + 1]) { options.secret = args[++i]; } else if (arg.startsWith("--db-host=")) { options.dbHost = arg.substring("--db-host=".length); } else if (arg === "--db-host" && args[i + 1]) { options.dbHost = args[++i]; } } return { options, commandArgs }; } async function fetchSecret(client, secretId) { const response = await client.send(new GetSecretValueCommand({ SecretId: secretId })); if (!response.SecretString) { throw new Error("Secret has no string value"); } try { return JSON.parse(response.SecretString); } catch { throw new Error("Secret is not valid JSON"); } } function rewriteDatabaseHost(databaseUrl, newHost) { try { const url = new URL(databaseUrl); const originalHost = url.host; url.hostname = newHost; if (!newHost.includes(":")) { url.port = url.port || "5432"; } logInfo(`Rewrote DATABASE_URL host: ${originalHost}${url.host}`); return url.toString(); } catch { logError(`Failed to parse DATABASE_URL for host rewrite`); return databaseUrl; } } export async function execCommand(args) { const { options, commandArgs } = parseArgs(args); if (!options.secret || commandArgs.length === 0) { logError("Usage: mesh secrets exec --secret=<name-or-arn> [options] -- <command>"); logInfo(""); logInfo("Options:"); logInfo(" --secret=<name-or-arn> Secret name or ARN in Secrets Manager"); logInfo(" --db-host=<host> Rewrite DATABASE_URL host (for dev tunneling)"); logInfo(""); logInfo("Examples:"); logInfo(" mesh secrets exec --secret=mesh/tenant/stage/app/secret -- pnpm dev"); logInfo(" mesh secrets exec --secret=mesh/tenant/stage/app/db --db-host=localhost -- pnpm dev"); process.exit(1); } const secretsManager = new SecretsManagerClient({}); logInfo(`Fetching secret: ${options.secret}`); let secretValues; try { secretValues = await fetchSecret(secretsManager, options.secret); } catch (error) { logError(`Failed to fetch secret: ${error}`); process.exit(1); } const envKeys = Object.keys(secretValues); logSuccess(`Loaded ${envKeys.length} environment variable(s) from secret`); if (options.dbHost && secretValues.DATABASE_URL) { secretValues.DATABASE_URL = rewriteDatabaseHost(secretValues.DATABASE_URL, options.dbHost); } const env = { ...process.env, ...secretValues, }; const [cmd, ...cmdArgs] = commandArgs; logInfo(`Running: ${commandArgs.join(" ")}`); console.log(""); const child = spawn(cmd, cmdArgs, { stdio: "inherit", env }); child.on("exit", (code) => { process.exit(code ?? 0); }); child.on("error", (error) => { logError(`Failed to start command: ${error.message}`); process.exit(1); }); }