@mesh-tech/mesh-cli
Version:
CLI for Mesh platform development utilities
625 lines (618 loc) • 20.5 kB
JavaScript
// libs/mesh-cli/src/utils/errors.ts
import chalk from "chalk";
var MeshCliError = class extends Error {
remediation;
exitCode;
constructor(message, options = {}) {
super(message, options.cause !== void 0 ? { cause: options.cause } : void 0);
this.name = "MeshCliError";
this.remediation = options.remediation;
this.exitCode = options.exitCode ?? 1;
}
};
function renderErrorBody(err) {
const message = err instanceof Error ? err.message : String(err);
if (!(err instanceof MeshCliError)) return message;
const lines = [message];
if (err.remediation?.command) {
lines.push(chalk.cyan(" \u2192 run: ") + err.remediation.command);
}
if (err.remediation?.docs) {
lines.push(chalk.cyan(" \u2192 see: ") + err.remediation.docs);
}
return lines.join("\n");
}
function renderError(err) {
return chalk.red("\u2717") + " " + renderErrorBody(err);
}
function renderErrorJson(err) {
if (err instanceof MeshCliError) {
return JSON.stringify({
error: err.message,
...err.remediation ? { remediation: err.remediation } : {}
});
}
return JSON.stringify({ error: err instanceof Error ? err.message : String(err) });
}
var jsonPayloadEmitted = false;
function emitJsonPayload(payload) {
console.log(JSON.stringify(payload, null, 2));
jsonPayloadEmitted = true;
}
function hasEmittedJsonPayload() {
return jsonPayloadEmitted;
}
function resetJsonPayloadEmitted() {
jsonPayloadEmitted = false;
}
function handleCliError(err) {
const wantsJson = process.argv.includes("--json");
if (wantsJson && !hasEmittedJsonPayload()) {
console.log(renderErrorJson(err));
} else {
console.error(renderError(err));
}
process.exit(err instanceof MeshCliError ? err.exitCode : 1);
}
// libs/mesh-cli/src/utils/log.ts
import chalk2 from "chalk";
function logPrefix(opts) {
const enabled = opts.envFlag === "1" || opts.envFlag !== "0" && !opts.isTTY;
return enabled ? `[${opts.now.toISOString().slice(11, 19)}] ` : "";
}
function prefix() {
return chalk2.dim(
logPrefix({
isTTY: !!process.stderr.isTTY,
envFlag: process.env.MESH_LOG_TIMESTAMPS,
now: /* @__PURE__ */ new Date()
})
);
}
function logInfo(message) {
console.error(prefix() + chalk2.blue("\u2139"), message);
}
function logSuccess(message) {
console.error(prefix() + chalk2.green("\u2713"), message);
}
function logWarn(message) {
console.error(prefix() + chalk2.yellow("\u26A0"), message);
}
function logError(message) {
console.error(prefix() + chalk2.red("\u2717"), message);
}
function formatElapsed(ms) {
const totalSec = Math.max(0, Math.round(ms / 1e3));
const h = Math.floor(totalSec / 3600);
const m = Math.floor(totalSec % 3600 / 60);
const s = totalSec % 60;
if (h > 0) return `${h}h ${m}m`;
if (m > 0) return `${m}m ${s}s`;
return `${s}s`;
}
function startHeartbeat(label, intervalMs = 15e3) {
const startedAt = Date.now();
let lastOutputAt = startedAt;
const timer = setInterval(() => {
if (Date.now() - lastOutputAt < intervalMs) return;
logInfo(`\u2026 still working: ${label} (${formatElapsed(Date.now() - startedAt)} elapsed)`);
}, intervalMs);
timer.unref?.();
return {
stop: () => clearInterval(timer),
touch: () => {
lastOutputAt = Date.now();
}
};
}
// libs/mesh-cli/src/utils/context.ts
import * as fs from "fs";
import * as path from "path";
function findFileUpward(filename, startDir = process.cwd()) {
let currentDir = startDir;
const root = path.parse(currentDir).root;
while (currentDir !== root) {
const filePath = path.join(currentDir, filename);
if (fs.existsSync(filePath)) {
return filePath;
}
currentDir = path.dirname(currentDir);
}
return void 0;
}
function detectContext(stageArg) {
let tenant;
let stage;
const cwd = process.cwd();
if (fs.existsSync("Pulumi.yaml")) {
const files = fs.readdirSync(".").filter(
(f) => f.startsWith("Pulumi.") && f.endsWith(".yaml") && f !== "Pulumi.yaml"
);
const configFile = files[0];
if (configFile) {
const content = fs.readFileSync(configFile, "utf-8");
const tenantMatch = content.match(/^\s*mesh:tenant:\s*["']?([^"'\n]+)["']?/m);
if (tenantMatch?.[1]) {
tenant = tenantMatch[1].trim();
}
const stageMatch = configFile.match(/Pulumi\.(.+)\.yaml/);
if (stageMatch?.[1]) {
stage = stageMatch[1];
}
}
}
const sstConfigPath = findFileUpward("sst.config.ts");
let sstDir;
if (sstConfigPath) {
sstDir = path.dirname(sstConfigPath);
const parentConfigPath = path.join(sstDir, "..", "config.ts");
if (fs.existsSync(parentConfigPath)) {
const configContent = fs.readFileSync(parentConfigPath, "utf-8");
const tenantMatch = configContent.match(/tenant:\s*["']([^"']+)["']/);
if (tenantMatch) {
tenant = tenantMatch[1];
}
}
const sstStagePath = path.join(sstDir, ".sst", "stage");
if (fs.existsSync(sstStagePath)) {
stage = fs.readFileSync(sstStagePath, "utf-8").trim();
}
}
if (!tenant) {
const pathMatch = cwd.match(/tenants\/([^/]+)/);
if (pathMatch) {
tenant = pathMatch[1];
}
}
stage = stageArg || process.env.MESH_STAGE || process.env.SST_STAGE || stage || "dev";
tenant = process.env.MESH_TENANT || tenant || "mesh";
let platformEnv = stage;
let platformEnvMap = {};
let defaultPlatformEnv;
const configPath = sstDir ? path.join(sstDir, "..", "config.ts") : "../config.ts";
if (fs.existsSync(configPath)) {
const configContent = fs.readFileSync(configPath, "utf-8");
const mapMatch = configContent.match(/platformEnvMap:\s*\{([^}]+)\}/);
if (mapMatch?.[1]) {
const entries = mapMatch[1].matchAll(/(\w+):\s*["']([^"']+)["']/g);
for (const entry of entries) {
const key = entry[1];
const value = entry[2];
if (key && value) {
platformEnvMap[key] = value;
}
}
}
const defaultMatch = configContent.match(/defaultPlatformEnv:\s*["']([^"']+)["']/);
if (defaultMatch?.[1]) {
defaultPlatformEnv = defaultMatch[1];
}
}
const tenantPrefix = `${tenant}-`;
const parsedEnv = stage.startsWith(tenantPrefix) ? stage.slice(tenantPrefix.length) : stage;
platformEnv = platformEnvMap[parsedEnv] ?? platformEnvMap[stage] ?? defaultPlatformEnv ?? parsedEnv;
logInfo(`Stage: ${stage}, Platform: ${platformEnv}, Tenant: ${tenant}`);
return { tenant, stage, platformEnv };
}
// libs/mesh-cli/src/utils/bastion.ts
import { SSMClient, GetParameterCommand } from "@aws-sdk/client-ssm";
async function getPlatformBastionInfo(tenant, platformEnv, region) {
const ssm = new SSMClient(region ? { region } : {});
let primaryError;
const platformPath = `/mesh-platform/${tenant}/${platformEnv}/platform`;
logInfo(`Looking up platform bastion from ${platformPath}...`);
try {
const response = await ssm.send(
new GetParameterCommand({ Name: platformPath })
);
if (response.Parameter?.Value) {
const platform = JSON.parse(response.Parameter.Value);
if (platform.platformBastion) {
const info = platform.platformBastion;
logSuccess(`Found bastion: ${info.instanceId}`);
const serviceNames = Object.keys(info.services);
if (serviceNames.length > 0) {
logInfo(`Available services: ${serviceNames.join(", ")}`);
}
return info;
}
}
} catch (err) {
primaryError = err;
}
const legacyPath = `/mesh-platform/${tenant}/${platformEnv}/platform-bastion`;
logInfo(`Trying legacy path ${legacyPath}...`);
try {
const response = await ssm.send(
new GetParameterCommand({ Name: legacyPath })
);
if (!response.Parameter?.Value) {
throw new Error(`Platform bastion not found`);
}
const info = JSON.parse(response.Parameter.Value);
logSuccess(`Found bastion: ${info.instanceId}`);
const serviceNames = Object.keys(info.services);
if (serviceNames.length > 0) {
logInfo(`Available services: ${serviceNames.join(", ")}`);
}
return info;
} catch (legacyError) {
const cause = primaryError ?? legacyError;
const name = cause?.name ?? "";
const credsProblem = /Expired|UnrecognizedClient|InvalidClientTokenId|InvalidSignature|CredentialsProviderError|AccessDenied/i.test(
name
);
if (credsProblem) {
logError(
`Could not read the platform bastion from ${platformPath} \u2014 AWS error: ${name}.`
);
logInfo(
"This is almost always a CREDENTIALS problem, not a missing bastion."
);
logInfo(
" \u2022 The read uses the ambient AWS creds of this process; they must be valid AND able to read the HUB param above."
);
logInfo(
" \u2022 Check: `aws sts get-caller-identity` (ExpiredToken \u2192 refresh; AccessDenied \u2192 those creds lack hub read \u2014 use InfraAdmin-grade creds)."
);
logInfo(
" \u2022 Stale creds often hide in the tmux GLOBAL env (`tmux show-environment -g | grep AWS_`); a per-shell `unset` won't clear them."
);
} else {
logError(`Platform bastion not found in ${platformPath} or ${legacyPath}`);
logInfo("Make sure platformBastion is enabled in your platform config and deployed.");
}
throw cause;
}
}
async function getBastionInfo(tenant, platformEnv) {
const info = await getPlatformBastionInfo(tenant, platformEnv);
const rdsService = info.services["rds"];
if (!rdsService) {
throw new Error("RDS service not available in platform bastion. Is RDS enabled?");
}
return {
instanceId: info.instanceId,
rdsEndpoint: rdsService.host,
rdsPort: rdsService.port
};
}
// libs/mesh-cli/src/utils/credentials.ts
import {
SecretsManagerClient,
GetSecretValueCommand
} from "@aws-sdk/client-secrets-manager";
async function getDatabaseUrl(tenant, stage, options) {
const secretsManager = new SecretsManagerClient({});
const secretName = options?.app ? `mesh/${tenant}/${stage}/${options.app}/db-credentials` : `mesh/${tenant}/${stage}/db-credentials`;
logInfo(`Looking up credentials at ${secretName}...`);
try {
const response = await secretsManager.send(
new GetSecretValueCommand({ SecretId: secretName })
);
if (response.SecretString) {
const secret = JSON.parse(response.SecretString);
if (secret.DATABASE_URL) {
const parsed = new URL(secret.DATABASE_URL);
logSuccess(`Got credentials for user: ${parsed.username}`);
return secret.DATABASE_URL;
}
if (secret.username && secret.password) {
logSuccess(`Got credentials for user: ${secret.username}`);
const database = secret.dbname ?? secret.database ?? "postgres";
return `postgresql://${secret.username}:${encodeURIComponent(secret.password)}@${secret.host ?? "localhost"}:${secret.port ?? 5432}/${database}`;
}
throw new Error(`Secret ${secretName} has unexpected format (needs DATABASE_URL or username/password)`);
}
} catch (error) {
if (error.message?.includes("unexpected format")) {
throw error;
}
}
throw new Error(`Could not find credentials at ${secretName}`);
}
async function getDbCredentials(tenant, stage, _platformEnv, rdsHost, rdsPort, options) {
const databaseUrl = await getDatabaseUrl(tenant, stage, options);
const parsed = new URL(databaseUrl);
return {
username: parsed.username,
password: decodeURIComponent(parsed.password),
host: rdsHost,
port: rdsPort,
database: parsed.pathname.slice(1)
};
}
function rewriteDatabaseUrl(url, options) {
try {
const parsed = new URL(url);
if (options.endpoint) {
if (options.endpoint.includes(":")) {
const colonIndex = options.endpoint.lastIndexOf(":");
parsed.hostname = options.endpoint.slice(0, colonIndex);
parsed.port = options.endpoint.slice(colonIndex + 1);
} else {
parsed.hostname = options.endpoint;
parsed.port = parsed.port || "5432";
}
}
if (options.sslMode) {
parsed.searchParams.set("sslmode", options.sslMode);
if (options.sslMode === "require" || options.sslMode === "no-verify") {
parsed.searchParams.set("sslaccept", "accept_invalid_certs");
}
}
return parsed.toString();
} catch {
return url;
}
}
function buildDatabaseUrl(creds, options) {
const host = options?.endpoint?.split(":")[0] ?? creds.host;
const port = options?.endpoint?.split(":")[1] ?? String(creds.port);
let url = `postgresql://${creds.username}:${encodeURIComponent(creds.password)}@${host}:${port}/${creds.database}`;
if (options?.sslMode) {
url += `?sslmode=${options.sslMode}`;
if (options.sslMode === "require" || options.sslMode === "no-verify") {
url += "&sslaccept=accept_invalid_certs";
}
}
return url;
}
async function readSstOutputs() {
const fs3 = await import("fs");
if (!fs3.existsSync(".sst/outputs.json")) {
return null;
}
try {
const content = fs3.readFileSync(".sst/outputs.json", "utf-8");
const outputs = JSON.parse(content);
if (outputs.databaseUrl) {
logInfo("Found SST outputs with database config");
const secretArnMatch = outputs.databaseUrl.match(/secretArn=([^&]+)/);
const secretArn = secretArnMatch ? decodeURIComponent(secretArnMatch[1]) : void 0;
return {
databaseUrl: outputs.databaseUrl,
databaseName: outputs.databaseName,
secretArn
};
}
} catch {
}
return null;
}
// libs/mesh-cli/src/utils/pulumi.ts
import { execFileSync } from "child_process";
import * as path2 from "path";
import * as fs2 from "fs";
function findAppRoot(startDir) {
let dir = startDir;
while (true) {
if (fs2.existsSync(path2.join(dir, "Pulumi.yaml"))) return dir;
const parent = path2.dirname(dir);
if (parent === dir) return null;
dir = parent;
}
}
function findStackConfigs(appRoot) {
return fs2.readdirSync(appRoot).filter((f) => /^Pulumi\..+\.yaml$/.test(f) && f !== "Pulumi.yaml").map((f) => f.replace(/^Pulumi\./, "").replace(/\.yaml$/, ""));
}
function getCurrentStack(appRoot) {
try {
const result = execFileSync("pulumi", ["stack", "--show-name"], {
encoding: "utf-8",
cwd: appRoot,
stdio: ["pipe", "pipe", "pipe"]
});
return result.trim() || null;
} catch {
return null;
}
}
function readStackConfig(appRoot, stack, key) {
const configFile = path2.join(appRoot, `Pulumi.${stack}.yaml`);
if (!fs2.existsSync(configFile)) return null;
const content = fs2.readFileSync(configFile, "utf-8");
const pattern = new RegExp(`^\\s+${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}:\\s*(.+)$`, "m");
const match = content.match(pattern);
if (!match) return null;
return match[1].trim().replace(/^["']|["']$/g, "");
}
function pulumiStackOutput(appRoot, key, extraArgs, env) {
const execEnv = env ? { ...process.env, ...env } : void 0;
try {
const result = execFileSync(
"pulumi",
["stack", "output", key, "--json", ...extraArgs],
{ cwd: appRoot, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], env: execEnv }
);
if (result.includes('"[secret]"')) {
return execFileSync(
"pulumi",
["stack", "output", key, "--json", "--show-secrets", ...extraArgs],
{ cwd: appRoot, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], env: execEnv }
);
}
return result;
} catch (err) {
const errMsg = err?.stderr ?? "";
if (errMsg.includes("kms:") || errMsg.includes("KMS") || errMsg.includes("secrets manager")) {
throw err;
}
return execFileSync(
"pulumi",
["stack", "output", key, "--json", "--show-secrets", ...extraArgs],
{ cwd: appRoot, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], env: execEnv }
);
}
}
// libs/mesh-cli/src/utils/vpn.ts
import { execFile, execFileSync as execFileSync2 } from "node:child_process";
import { existsSync as existsSync3 } from "node:fs";
var TAILSCALE_PATHS = [
"tailscale",
"/Applications/Tailscale.app/Contents/MacOS/Tailscale"
];
async function isVpnConnected() {
try {
const stdout = await execTailscaleCmd(["status", "--json"]);
const status = JSON.parse(stdout);
return status.BackendState === "Running" && status.Self?.Online === true;
} catch {
return false;
}
}
var TAILSCALE_SOCKET_PATHS = ["/var/run/tailscale/tailscaled.sock", "/tmp/tailscale.sock"];
function headscaleDnsConfig(tenant = "mesh", env = "dev", overrides) {
return {
namespace: `${tenant}-${env}-headscale`,
pod: "headscale-0",
container: "dns-writer",
filePath: "/var/lib/headscale/dns/extra-records.json",
...overrides
};
}
function execTailscaleCmd(args) {
const socketPath = TAILSCALE_SOCKET_PATHS.find(existsSync3);
const attempts = [];
for (const binary of TAILSCALE_PATHS) {
if (socketPath) {
attempts.push({ binary, args: ["--socket", socketPath, ...args] });
}
attempts.push({ binary, args });
}
return new Promise((resolve, reject) => {
let index = 0;
function tryNext() {
if (index >= attempts.length) {
reject(new Error("All tailscale binary attempts failed"));
return;
}
const attempt = attempts[index++];
execFile(attempt.binary, attempt.args, (error, stdout) => {
if (error) {
tryNext();
} else {
resolve(stdout);
}
});
}
tryNext();
});
}
async function getTailscaleInfo() {
try {
const stdout = await execTailscaleCmd(["status", "--json"]);
const status = JSON.parse(stdout);
if (status.BackendState === "Running" && status.Self?.Online === true && status.Self.HostName && status.Self.TailscaleIPs?.length) {
return {
hostname: status.Self.HostName,
ip: status.Self.TailscaleIPs[0]
};
}
return null;
} catch {
return null;
}
}
function dnsWriterExec(cfg, cmd, interactive = false) {
return [
"exec",
...interactive ? ["-i"] : [],
"-n",
cfg.namespace,
cfg.pod,
"-c",
cfg.container,
"--",
...cmd
];
}
function readDnsRecords(cfg) {
try {
const raw = execFileSync2("kubectl", dnsWriterExec(cfg, ["cat", cfg.filePath]), {
encoding: "utf-8",
stdio: ["ignore", "pipe", "pipe"]
});
const trimmed = raw.trim();
if (!trimmed || trimmed === "[]") return [];
try {
return JSON.parse(trimmed);
} catch {
console.warn(`[vpn] DNS records file contains invalid JSON, treating as empty`);
return [];
}
} catch (err) {
const msg = err.message ?? String(err);
if (msg.includes("not found") || msg.includes("Unable to connect")) {
throw new Error(
`Cannot reach Headscale DNS writer (namespace=${cfg.namespace}, pod=${cfg.pod}).
Verify kubectl context and that the Headscale pod is running:
kubectl get pods -n ${cfg.namespace}`
);
}
if (msg.includes("No such file")) {
return [];
}
throw err;
}
}
function registerDnsRecords(cfg, records) {
const json = JSON.stringify(records);
try {
execFileSync2(
"kubectl",
dnsWriterExec(cfg, ["sh", "-c", `cat > '${cfg.filePath}'`], true),
{ input: json, stdio: ["pipe", "pipe", "pipe"] }
);
} catch (err) {
const msg = err.message ?? String(err);
throw new Error(
`Failed to write DNS records to Headscale (namespace=${cfg.namespace}, pod=${cfg.pod}).
${msg.includes("not found") ? "Pod not found. " : ""}Verify kubectl context: kubectl get pods -n ${cfg.namespace}`
);
}
}
function unregisterDnsRecords(cfg, suffix) {
try {
const records = readDnsRecords(cfg);
const filtered = records.filter((r) => !r.name.endsWith(suffix));
registerDnsRecords(cfg, filtered);
} catch {
}
}
export {
MeshCliError,
buildDatabaseUrl,
detectContext,
emitJsonPayload,
findAppRoot,
findStackConfigs,
formatElapsed,
getBastionInfo,
getCurrentStack,
getDatabaseUrl,
getDbCredentials,
getPlatformBastionInfo,
getTailscaleInfo,
handleCliError,
hasEmittedJsonPayload,
headscaleDnsConfig,
isVpnConnected,
logError,
logInfo,
logPrefix,
logSuccess,
logWarn,
pulumiStackOutput,
readDnsRecords,
readSstOutputs,
readStackConfig,
registerDnsRecords,
renderError,
renderErrorBody,
renderErrorJson,
resetJsonPayloadEmitted,
rewriteDatabaseUrl,
startHeartbeat,
unregisterDnsRecords
};