@mesh-tech/mesh-cli
Version:
CLI for Mesh platform development utilities
552 lines (551 loc) • 20.2 kB
JavaScript
import { execFileSync } from "node:child_process";
import * as fs from "node:fs";
import * as net from "node:net";
import * as path from "node:path";
import { isLinkedDependencyDir, isPortFree } from "./dev.js";
import { appUsesMeshPackages, probeRegistryToken, registryLoginFix, } from "../utils/auth-preflight.js";
import { probeCredentials, isRemoteEnvironment } from "./login.js";
export function aggregateStatus(results) {
if (results.some((r) => r.status === "error"))
return "error";
if (results.some((r) => r.status === "warn"))
return "warn";
return "ok";
}
export async function runChecks(ctx, phase, checks) {
const applicable = checks.filter((c) => c.phases.includes(phase));
return Promise.all(applicable.map(async (check) => {
try {
return { check, result: await check.run(ctx, phase) };
}
catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return {
check,
result: {
status: "error",
summary: `check '${check.id}' threw: ${msg}`,
},
};
}
}));
}
export const ICONS = { ok: "✓", warn: "⚠", error: "✗" };
export function renderHuman(results) {
const lines = [];
for (const { check, result } of results) {
lines.push(`${ICONS[result.status]} ${check.title} — ${result.summary}`);
if (result.detail)
lines.push(` ${result.detail}`);
if (result.remediation)
lines.push(` Fix: ${result.remediation}`);
}
return lines.join("\n");
}
export function jsonReport(results) {
return {
status: aggregateStatus(results.map((r) => r.result)),
checks: results.map(({ check, result }) => ({
id: check.id,
status: result.status,
summary: result.summary,
remediation: result.remediation ?? null,
detail: result.detail ?? null,
})),
};
}
export function renderJson(results) {
return JSON.stringify(jsonReport(results), null, 2);
}
function fmtTtl(seconds) {
if (seconds <= 0)
return "expired";
const totalMin = Math.round(seconds / 60);
const h = Math.floor(totalMin / 60);
const m = totalMin % 60;
if (h > 0)
return m > 0 ? `${h}h ${m}m` : `${h}h`;
return `${m}m`;
}
export function credProbeToResult(probe, context, headless) {
const loginCmd = `mesh login ${context}${headless ? " --device" : ""}`;
switch (probe.state) {
case "ok":
return {
status: probe.ttlSeconds < 300 ? "warn" : "ok",
summary: `deployer creds valid ${fmtTtl(probe.ttlSeconds)}${probe.email ? ` (${probe.email})` : ""}`,
detail: `expires ${probe.expiresAt}`,
};
case "no-session":
return {
status: "error",
summary: `no Zitadel session for ${context}`,
remediation: loginCmd,
};
case "expired-session":
return {
status: "error",
summary: `Zitadel session for ${context} expired and could not refresh`,
remediation: loginCmd,
};
case "assume-denied":
return {
status: "error",
summary: "session valid but AssumeRole denied — wrong IAM role/policy",
detail: probe.detail,
remediation: `check mesh:deployerRole in the stack config and the role's trust/permissions; re-login if role changed: ${loginCmd}`,
};
case "stale-env-override":
return {
status: "error",
summary: "stale AWS_* env vars would override the self-refreshing login profile",
remediation: "unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN",
};
}
}
export const credsCheck = {
id: "creds",
title: "Credentials",
phases: ["preflight", "ondemand"],
async run(ctx) {
switch (ctx.credMethod) {
case "sso":
return {
status: "ok",
summary: "using AWS SSO credentials (auto-refresh from SSO cache)",
};
case "environment":
return { status: "ok", summary: "using AWS credentials from environment" };
case "zitadel":
return {
status: "ok",
summary: "using Zitadel deployer credentials (credential_process auto-refresh)",
};
}
if (!ctx.platformContext || !ctx.deployerRole) {
return {
status: "warn",
summary: "no deployer role/context resolved — using ambient AWS credentials",
detail: "add mesh:deployerRole to the Pulumi stack config to use mesh login credentials",
};
}
const probe = await probeCredentials(ctx.platformContext, ctx.deployerRole);
return credProbeToResult(probe, ctx.platformContext, isRemoteEnvironment());
},
};
export const registryCheck = {
id: "registry",
title: "Registry auth",
phases: ["preflight", "ondemand"],
async run(ctx, phase) {
const probe = await probeRegistryToken(phase === "preflight" ? { timeoutMs: 3_000 } : undefined);
switch (probe.state) {
case "fresh":
return { status: "ok", summary: "CodeArtifact token accepted by the @mesh-tech registry" };
case "expired":
return {
status: "warn",
summary: "CodeArtifact token expired/rejected — installing @mesh-tech packages will fail with E401",
detail: probe.detail,
remediation: registryLoginFix(),
};
case "unreachable":
return {
status: "ok",
summary: "registry unreachable — CodeArtifact token not verified (offline?)",
detail: probe.detail,
};
case "missing":
return appUsesMeshPackages(ctx.appRoot)
? {
status: "warn",
summary: "no CodeArtifact auth in ~/.npmrc, but this app depends on @mesh-tech packages — pnpm install will fail with E401",
remediation: registryLoginFix(),
}
: {
status: "ok",
summary: "no CodeArtifact auth configured (needed only to install @mesh-tech packages)",
};
}
},
};
export function parsePortSquatter(lsofOut, psOut) {
const firstLine = lsofOut.trim().split("\n")[0]?.trim();
const pid = Number(firstLine);
if (!firstLine || !Number.isInteger(pid))
return null;
const command = psOut.trim().split("\n")[0]?.trim() || "unknown";
return { pid, command };
}
export function sharesWorktreeRoot(serviceSrc, repoRoot) {
const src = path.resolve(serviceSrc);
const root = path.resolve(repoRoot);
return src === root || src.startsWith(root + path.sep);
}
export function classifyPortListener(listenerPid, sessionPids) {
if (listenerPid === null)
return "dead";
return sessionPids.has(listenerPid) ? "owned" : "foreign";
}
export function collectSessionPids(panePids, psOut) {
const children = new Map();
for (const line of psOut.trim().split("\n")) {
const [pidStr, ppidStr] = line.trim().split(/\s+/);
const pid = Number(pidStr);
const ppid = Number(ppidStr);
if (!Number.isInteger(pid) || !Number.isInteger(ppid))
continue;
const kids = children.get(ppid) ?? [];
kids.push(pid);
children.set(ppid, kids);
}
const result = new Set();
const queue = panePids.filter((p) => Number.isInteger(p));
while (queue.length > 0) {
const pid = queue.shift();
if (result.has(pid))
continue;
result.add(pid);
for (const child of children.get(pid) ?? [])
queue.push(child);
}
return result;
}
function whoHasPort(port) {
try {
const lsof = execFileSync("lsof", ["-ti", `:${port}`], {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
});
const pid = lsof.trim().split("\n")[0]?.trim();
if (!pid)
return null;
let ps = "";
try {
ps = execFileSync("ps", ["-o", "comm=", "-p", pid], {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
});
}
catch {
}
return parsePortSquatter(lsof, ps);
}
catch {
return null;
}
}
function sessionProcessTree(sessionName) {
let panePids;
try {
const out = execFileSync("tmux", ["list-panes", "-s", "-t", sessionName, "-F", "#{pane_pid}"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
panePids = out
.trim()
.split("\n")
.map((l) => Number(l.trim()))
.filter((n) => Number.isInteger(n));
}
catch {
return new Set();
}
if (panePids.length === 0)
return new Set();
let psOut = "";
try {
psOut = execFileSync("ps", ["-eo", "pid=,ppid="], {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
});
}
catch {
return new Set(panePids);
}
return collectSessionPids(panePids, psOut);
}
export const tmuxCheck = {
id: "tmux",
title: "tmux",
phases: ["preflight", "ondemand"],
async run() {
try {
execFileSync("which", ["tmux"], { stdio: "ignore" });
return { status: "ok", summary: "installed" };
}
catch {
return {
status: "error",
summary: "tmux is not installed",
remediation: "brew install tmux",
};
}
},
};
export const portsCheck = {
id: "ports",
title: "Ports",
phases: ["preflight", "ondemand"],
async run(ctx) {
const services = ctx.sessionState?.devOutput.services;
if (!services || Object.keys(services).length === 0) {
return {
status: "ok",
summary: "no session — ports are checked at allocation time",
};
}
const sessionPids = sessionProcessTree(ctx.sessionName);
const foreign = [];
const dead = [];
const unknown = [];
for (const [name, svc] of Object.entries(services)) {
const free = await isPortFree(svc.port);
const who = free ? null : whoHasPort(svc.port);
const ownership = classifyPortListener(who?.pid ?? null, sessionPids);
if (ownership === "owned")
continue;
if (ownership === "foreign") {
foreign.push(`${name} :${svc.port} held by ${who.command} (pid ${who.pid})`);
continue;
}
if (free) {
dead.push(`${name} :${svc.port} not listening`);
}
else {
unknown.push(`${name} :${svc.port} in use (owner unknown)`);
}
}
const total = Object.keys(services).length;
if (foreign.length === 0 && dead.length === 0 && unknown.length === 0) {
return {
status: "ok",
summary: `all ${total} service port(s) owned by this session`,
};
}
const parts = [];
if (foreign.length)
parts.push(`${foreign.length} squatted by another process`);
if (dead.length)
parts.push(`${dead.length} not listening (service down)`);
if (unknown.length)
parts.push(`${unknown.length} in use by an unidentified process`);
const remediation = foreign.length > 0
? "stop the squatting process (or its SSH tunnel), then `mesh dev restart <svc>`"
: dead.length > 0
? "restart the stopped service: `mesh dev restart <svc>`"
: "identify the port owner (`lsof -i :<port>`), then `mesh dev restart <svc>`";
return {
status: foreign.length > 0 || dead.length > 0 ? "error" : "warn",
summary: `service ports: ${parts.join(", ")}`,
detail: [...foreign, ...dead, ...unknown].join("; "),
remediation,
};
},
};
export function isConfigStale(configMtimeMs, startedAtIso) {
return configMtimeMs > Date.parse(startedAtIso);
}
export const configStalenessCheck = {
id: "config-staleness",
title: "Config freshness",
phases: ["ondemand"],
async run(ctx) {
const state = ctx.sessionState;
if (!state) {
return { status: "ok", summary: "no session — config read fresh at launch" };
}
const configPath = path.join(ctx.appRoot, `Pulumi.${ctx.stack}.yaml`);
if (!fs.existsSync(configPath)) {
return {
status: "ok",
summary: `no Pulumi.${ctx.stack}.yaml to compare`,
};
}
const mtimeMs = fs.statSync(configPath).mtimeMs;
if (isConfigStale(mtimeMs, state.startedAt)) {
return {
status: "warn",
summary: `Pulumi.${ctx.stack}.yaml changed since launch — a restart won't pick this up`,
detail: `config mtime ${new Date(mtimeMs).toISOString()} > session start ${state.startedAt}`,
remediation: `mesh deploy up --stack ${ctx.stack} && mesh dev`,
};
}
return { status: "ok", summary: "stack config unchanged since launch" };
},
};
export const worktreeCheck = {
id: "worktree",
title: "Worktree",
phases: ["preflight", "ondemand"],
async run(ctx) {
const services = ctx.sessionState?.devOutput.services;
if (!services) {
return { status: "ok", summary: "no session — worktree checked at launch" };
}
let repoRoot;
try {
repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
cwd: ctx.appRoot,
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
}).trim();
}
catch {
return {
status: "warn",
summary: "could not resolve git worktree root for the app",
};
}
const foreign = Object.entries(services)
.filter(([, svc]) => svc.src &&
!sharesWorktreeRoot(svc.src, repoRoot) &&
!isLinkedDependencyDir(svc.src, ctx.appRoot))
.map(([name, svc]) => `${name} → ${svc.src}`);
if (foreign.length === 0) {
return {
status: "ok",
summary: `all services resolve under ${repoRoot}`,
};
}
return {
status: "warn",
summary: `${foreign.length} service(s) resolve outside this worktree — you may be running another worktree's code`,
detail: foreign.join("; "),
remediation: "relaunch from this worktree: `mesh dev` (re-reads stack output paths)",
};
},
};
export function canConnect(host, port, timeoutMs = 800) {
return new Promise((resolve) => {
const socket = new net.Socket();
let settled = false;
const done = (ok) => {
if (settled)
return;
settled = true;
socket.destroy();
resolve(ok);
};
socket.setTimeout(timeoutMs);
socket.once("connect", () => done(true));
socket.once("timeout", () => done(false));
socket.once("error", () => done(false));
socket.connect(port, host);
});
}
function isLocalHost(host) {
return host === "localhost" || host === "127.0.0.1" || host === "::1";
}
export const tunnelsCheck = {
id: "tunnels",
title: "Tunnels",
phases: ["ondemand"],
async run(ctx) {
const tunnels = ctx.sessionState?.devOutput.tunnels;
if (!tunnels || Object.keys(tunnels).length === 0) {
return { status: "ok", summary: "no tunnels in this session" };
}
const local = Object.entries(tunnels).filter(([, t]) => isLocalHost(t.host));
if (local.length === 0) {
return {
status: "ok",
summary: `${Object.keys(tunnels).length} tunnel(s), all VPC-direct (not dialed)`,
};
}
const dead = [];
for (const [name, t] of local) {
if (!(await canConnect(t.host, t.port)))
dead.push(`${name} (${t.host}:${t.port})`);
}
if (dead.length === 0) {
return { status: "ok", summary: `${local.length} SSM tunnel(s) live` };
}
return {
status: "error",
summary: `${dead.length} SSM tunnel(s) down`,
detail: dead.join("; "),
remediation: "relaunch to re-establish tunnels: `mesh dev`",
};
},
};
export const temporalCheck = {
id: "temporal",
title: "Temporal",
phases: ["ondemand"],
async run(ctx) {
const state = ctx.sessionState;
const t = state?.devOutput.tunnels["temporal"];
if (!state || !t) {
return { status: "ok", summary: "no temporal tunnel in this session" };
}
const addr = `${t.host}:${t.port}`;
if (!(await canConnect(t.host, t.port))) {
return {
status: "error",
summary: `Temporal frontend unreachable at ${addr}`,
remediation: "tunnel likely down — relaunch: `mesh dev`",
};
}
const p = state.devOutput.platform;
const app = state.devOutput.app;
if (!p || !app) {
return { status: "ok", summary: `frontend reachable at ${addr} (namespace unknown)` };
}
const namespace = `${p.tenant}-${p.env}-${app}`;
try {
const { Connection } = await import("@temporalio/client");
const connection = await Connection.connect({
address: addr,
connectTimeout: "3s",
});
try {
await connection.workflowService.describeNamespace({ namespace });
return { status: "ok", summary: `frontend reachable; namespace ${namespace} present` };
}
finally {
await connection.close().catch(() => { });
}
}
catch (err) {
const code = err.code;
if (code === 5) {
return {
status: "error",
summary: `namespace ${namespace} not found on the server`,
remediation: `verify the app is deployed to this env (mesh deploy up --stack ${ctx.stack})`,
};
}
if (code === 7 || code === 16) {
return {
status: "ok",
summary: `frontend reachable; namespace ${namespace} auth-gated (not verified)`,
};
}
return {
status: "warn",
summary: "frontend reachable but namespace check errored",
detail: err instanceof Error ? err.message : String(err),
};
}
},
};
export const ALL_CHECKS = [
tmuxCheck,
credsCheck,
registryCheck,
configStalenessCheck,
portsCheck,
worktreeCheck,
tunnelsCheck,
temporalCheck,
];
export async function runDoctor(ctx, opts) {
const results = await runChecks(ctx, "ondemand", ALL_CHECKS);
const status = aggregateStatus(results.map((r) => r.result));
if (opts.json) {
console.log(renderJson(results));
}
else {
console.log(renderHuman(results));
console.log(`\n${ICONS[status]} overall: ${status}`);
}
return status;
}