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