@mesh-tech/mesh-cli
Version:
CLI for Mesh platform development utilities
380 lines (379 loc) • 19.6 kB
JavaScript
import * as fs from "fs";
import * as path from "path";
import chalk from "chalk";
import { logError, logInfo, logSuccess, logWarn } from "../../utils/log.js";
import { MeshCliError } from "../../utils/errors.js";
import { compose, COMPOSE_PROJECT, composeStreamed, crashedServices, DEFAULT_HUB_PORT, ensureDockerAvailable, findPackageRoot, hubApiRunning, hubEndpoints, hubOverlayRunning, hubOverlayServices, hubPort, ONE_SHOT_SERVICES, probeEndpoint, STACK_ENDPOINTS, stackOwnedElsewhere, stackServices, } from "./stack.js";
import { probeRegistryToken, REGISTRY_LOGIN_FIX } from "../../utils/auth-preflight.js";
import { ensureHubAuthImage, ensureHubImages, buildHubImagesFromSource, readHubCompiledAuthz, hasRegistryAuth, localHubVersion, planWithHubRefresh, } from "./hub-local.js";
import { reconcileRegistryFromZitadel } from "./auth-provision.js";
import { LOCAL_AWS_CONFIG, localAwsEnv, seedLocalPlatform, verifyFabric } from "./seed.js";
import { LOGIN_CONTEXT, readHubAuth, seedZitadel, TEST_USERS } from "./seed-zitadel.js";
import { publishHubAuthzCatalog } from "./seed-hub-catalog.js";
import { describePortConflicts, findPortConflicts, hostPortsOf } from "./helpers.js";
const WAIT_TIMEOUT_MS = 180_000;
const WAIT_POLL_MS = 3_000;
function crashError(crashed) {
return new MeshCliError(`Service(s) exited during startup: ${crashed.map((s) => s.name).join(", ")}`, {
remediation: {
command: `docker compose -p ${COMPOSE_PROJECT} logs ${crashed[0].name}`,
},
});
}
export async function waitForStack(io = {
probe: probeEndpoint,
services: stackServices,
overlayServices: hubOverlayServices,
}) {
const deadline = Date.now() + WAIT_TIMEOUT_MS;
let pending = STACK_ENDPOINTS.filter((e) => e.probe.kind !== "none");
while (true) {
const results = await Promise.all(pending.map((e) => io.probe(e)));
const stillDown = [];
for (let i = 0; i < pending.length; i++) {
if (results[i]) {
logSuccess(`${pending[i].label} is up (${pending[i].url})`);
}
else {
stillDown.push(pending[i]);
}
}
pending = stillDown;
const crashed = crashedServices(io.services(), {
overlayServices: io.overlayServices(),
overlayStarted: false,
});
if (crashed.length > 0)
throw crashError(crashed);
if (pending.length === 0)
break;
if (Date.now() > deadline) {
throw new MeshCliError(`Timed out waiting for: ${pending.map((e) => e.label).join(", ")}`, { remediation: { command: "mesh status" } });
}
logInfo(`Waiting for ${pending.map((e) => e.label).join(", ")} …`);
await new Promise((resolve) => setTimeout(resolve, WAIT_POLL_MS));
}
}
function printEndpoints(hubRunning) {
console.log("");
if (hubRunning) {
console.log(chalk.bold.cyan("★ Mesh Hub — your operations dashboard (start here)"));
console.log(` ${"Hub UI".padEnd(34)} ${chalk.bold.cyan(`http://localhost:${hubPort()}`)}`);
console.log(` ${"".padEnd(34)} ${chalk.dim("sign in: admin@local.mesh or dev@local.mesh / LocalDev1!")}`);
console.log(` ${"Hub API".padEnd(34)} ${chalk.cyan("http://localhost:4568")}`);
console.log(` ${"".padEnd(34)} ${chalk.dim("tenants, apps, workflows, logs, traces — local runs self-register here")}`);
}
else {
logWarn("Hub is not running — it is the platform's front door. Enable it with:");
logInfo(` ${REGISTRY_LOGIN_FIX} (one-time, to build the Hub images — no AWS account needed)`);
logInfo(" mesh start (the Hub joins automatically)");
}
console.log("");
console.log(chalk.bold("Local platform endpoints"));
for (const endpoint of STACK_ENDPOINTS) {
console.log(` ${endpoint.label.padEnd(34)} ${chalk.cyan(endpoint.url)}`);
if (endpoint.hint) {
console.log(` ${"".padEnd(34)} ${chalk.dim(endpoint.hint)}`);
}
}
console.log("");
console.log(chalk.bold("AWS SDK wiring (local fabric)"));
console.log(chalk.dim(" Point any AWS SDK process at the local registry — no code changes:"));
for (const [key, value] of Object.entries(localAwsEnv())) {
console.log(` export ${key}=${chalk.cyan(value)}`);
}
console.log("");
console.log(chalk.bold("Login"));
console.log(` mesh login ${LOGIN_CONTEXT}`);
console.log(` ${chalk.dim(`test users: ${TEST_USERS.map((u) => u.email).join(", ")} (password: ${TEST_USERS[0].password})`)}`);
printStartHere(hubRunning);
}
export function startHereLines(hubRunning, up) {
const dev = TEST_USERS[0];
const mailbox = STACK_ENDPOINTS.find((e) => e.service === "mailpit");
const flag = (service) => up?.get(service) === false ? chalk.red(" (not responding — see Endpoints below)") : "";
const lines = ["", chalk.bold.cyan("★ Start here")];
if (hubRunning) {
lines.push(` ${"Hub".padEnd(10)} ${chalk.bold.cyan(`http://localhost:${hubPort()}`)}${flag("hub-ui")}`, ` ${"".padEnd(10)} ${chalk.dim(`sign in as ${dev.email} / ${dev.password} — an account already exists; do not Register`)}`);
}
if (mailbox) {
lines.push(` ${"Mailbox".padEnd(10)} ${chalk.cyan(mailbox.url)}${flag("mailpit")}`, ` ${"".padEnd(10)} ${chalk.dim("every email the local platform sends (sign-up, verification, password reset) lands here — nothing leaves your machine")}`);
}
lines.push(` ${"Your app".padEnd(10)} ${chalk.cyan("cd apps/<name> && mesh dev")} ${chalk.dim("no app yet? mesh create-app")}`, ` ${"Check-up".padEnd(10)} ${chalk.cyan("mesh status")}`, "");
return lines;
}
function printStartHere(hubRunning, up) {
for (const line of startHereLines(hubRunning, up))
console.log(line);
}
function portMoveHints() {
return new Map([
[
Number(hubPort()),
"MESH_HUB_PORT=<free port> mesh start (or mesh start --no-hub, which starts everything else)",
],
]);
}
function repoRootForHubSource() {
for (let dir = process.cwd();;) {
if (fs.existsSync(path.join(dir, "apps", "hub", "package.json")))
return dir;
const parent = path.dirname(dir);
if (parent === dir)
break;
dir = parent;
}
return path.resolve(findPackageRoot(), "..", "..");
}
export function registerLocalCommands(program) {
program
.command("start")
.description("Start the full-local Mesh platform (Docker only — no AWS, no VPN)")
.option("--no-seed", "skip first-boot seeding (tenant registry, artifacts bucket)")
.option("--no-hub", "start without the Hub (API + UI)")
.option("--with-hub", "force-refresh the Hub to the latest published @mesh-tech/hub (the Hub is included by default when its images exist or registry auth is available)")
.option("--hub-from-source", "build the Hub images from THIS checkout's apps/hub instead of the published tarball (the only way to exercise a Hub change locally)")
.option("--takeover", "re-own a stack that another checkout started (may recreate shared containers with THIS checkout's config)", false)
.option("--skip-port-check", "start even though a host port the stack wants is already held (the service that loses the bind will be broken — use when the holder is something you cannot stop)", false)
.action(async (opts) => {
ensureDockerAvailable();
const foreign = stackOwnedElsewhere();
if (foreign && !opts.takeover) {
throw new MeshCliError(`The local platform is already running, started from a different checkout:\n ${foreign}\n` +
`Re-running start from here may recreate shared containers with this checkout's config and disrupt work using the current stack.`, {
remediation: {
command: "mesh status # inspect the running stack (apps deploy tenant-scoped via `mesh dev --local`)\n" +
" mesh start --takeover # intentionally re-own the stack from this checkout",
},
});
}
if (foreign && opts.takeover) {
logWarn(`Taking over the running stack (previously driven from ${foreign}).`);
}
if (!opts.skipPortCheck && !stackServices().some((s) => s.state === "running")) {
const ports = hostPortsOf([...STACK_ENDPOINTS, ...(opts.hub ? hubEndpoints() : [])]);
const conflicts = await findPortConflicts(ports, `${COMPOSE_PROJECT}-`);
if (conflicts.length > 0) {
throw new MeshCliError(describePortConflicts(conflicts, portMoveHints()), {
remediation: { command: "mesh start # once the port is free" },
});
}
}
let hubVersion;
if (opts.hub) {
if (opts.hubFromSource) {
hubVersion = await buildHubImagesFromSource(repoRootForHubSource());
}
else if (opts.withHub) {
const probe = await probeRegistryToken();
const plan = planWithHubRefresh(probe.state, localHubVersion());
if (plan.action === "fail") {
throw new MeshCliError(plan.message, { remediation: { command: plan.remediation } });
}
if (plan.action === "use-local") {
logWarn(plan.warning);
hubVersion = plan.version;
}
else {
hubVersion = await ensureHubImages();
}
}
else {
hubVersion = localHubVersion();
if (!hubVersion && hasRegistryAuth()) {
const probe = await probeRegistryToken();
if (probe.state === "expired") {
logWarn(`CodeArtifact token is expired — starting without the Hub. Fix: ${REGISTRY_LOGIN_FIX}, then: mesh start --with-hub`);
}
else {
try {
hubVersion = await ensureHubImages();
}
catch (err) {
logWarn(`Hub image build failed (${err instanceof Error ? err.message : err}) — starting without the Hub.`);
}
}
}
}
}
logInfo("Starting the local Mesh platform (docker compose project: mesh-local)…");
await composeStreamed(["up", "-d"]);
compose(["restart", "prometheus", "blackbox-exporter"], { inherit: true });
await waitForStack();
let hubAuth = readHubAuth();
if (opts.seed) {
logInfo("Seeding local platform (tenant registry, artifacts bucket)…");
await seedLocalPlatform();
logInfo("Seeding Zitadel (Platform project, Mesh CLI app, Hub auth, test users)…");
const zitadel = await seedZitadel(LOCAL_AWS_CONFIG);
hubAuth = zitadel.hubAuth;
if (hubVersion) {
const compiled = readHubCompiledAuthz(hubVersion);
if (compiled) {
const { bundles } = await publishHubAuthzCatalog(compiled, LOCAL_AWS_CONFIG);
logSuccess(`Hub role catalog seeded: ${bundles.length} role(s) (${bundles.join(", ")})`);
}
else {
logWarn(`Hub v${hubVersion} ships no compiled role catalog (api/dist/authz/compiled.json) — Users → Roles will be empty. Hub ≥ 2.1.0 publishes one.`);
}
}
try {
const reconciled = await reconcileRegistryFromZitadel();
if (reconciled.tenants.length > 0) {
logSuccess(`Hub registry reconciled from Zitadel: ${reconciled.tenants.length} app tenant(s) (${reconciled.tenants.join(", ")}), ${reconciled.apps} app(s)`);
}
}
catch (err) {
logWarn(`Registry reconcile skipped: ${err instanceof Error ? err.message : err}`);
}
logInfo("Verifying the local AWS fabric (8 KB Advanced-tier round-trip)…");
await verifyFabric();
}
else {
logWarn("Skipping seed (--no-seed).");
}
if (hubVersion) {
if (!hubAuth) {
logWarn("Hub auth is not seeded yet (started with --no-seed?) — Hub sign-in needs one seeded run: mesh start");
}
if (!opts.seed && hubPort() !== DEFAULT_HUB_PORT) {
logWarn(`--no-seed with MESH_HUB_PORT=${hubPort()}: this port's redirect URI is registered during seeding — if Hub sign-in fails with "redirect_uri is missing in the client configuration", re-run without --no-seed.`);
}
ensureHubAuthImage();
const hubEnv = { MESH_HUB_VERSION: hubVersion };
if (hubAuth) {
hubEnv.MESH_HUB_OAUTH2_CLIENT_ID = hubAuth.clientId;
hubEnv.MESH_HUB_OAUTH2_CLIENT_SECRET = hubAuth.clientSecret;
hubEnv.MESH_HUB_OAUTH2_COOKIE_SECRET = hubAuth.cookieSecret;
}
await composeStreamed(["up", "-d"], { hub: true, env: hubEnv });
if (opts.seed) {
compose(["restart", "hub-api"], { hub: true, env: hubEnv });
}
const overlayServices = hubOverlayServices();
for (const endpoint of hubEndpoints()) {
logInfo(`Waiting for ${endpoint.label}…`);
let up = await probeEndpoint(endpoint);
for (let i = 0; i < 60 && !up; i++) {
const crashed = crashedServices(stackServices(), {
overlayServices,
overlayStarted: true,
});
if (crashed.length > 0)
throw crashError(crashed);
await new Promise((resolve) => setTimeout(resolve, 2000));
up = await probeEndpoint(endpoint);
}
if (!up) {
throw new MeshCliError(`Timed out waiting for ${endpoint.label} (${endpoint.url}).`, {
remediation: {
command: `docker compose -p ${COMPOSE_PROJECT} logs ${endpoint.service}`,
},
});
}
logSuccess(`${endpoint.label} is up (${endpoint.url})`);
}
}
logSuccess("Local Mesh platform is running.");
printEndpoints(!!hubVersion);
});
program
.command("stop")
.description("Stop the local Mesh platform")
.option("--destroy", "also remove volumes (resets databases and seeds)", false)
.option("--force", "stop even when the stack was started from a different checkout", false)
.action((opts) => {
ensureDockerAvailable();
const foreign = stackOwnedElsewhere();
if (foreign && !opts.force) {
throw new MeshCliError(`The running local platform was started from a different checkout (${foreign}) — another project may be using it.`, { remediation: { command: "mesh stop --force # stop it anyway" } });
}
const args = ["down"];
if (opts.destroy)
args.push("-v");
compose(args, { inherit: true, hub: true });
logSuccess(opts.destroy ? "Local platform stopped and volumes destroyed." : "Local platform stopped.");
});
program
.command("status")
.description("Show local platform component health, endpoints, and ports")
.option("--json", "machine-readable output", false)
.action(async (opts) => {
ensureDockerAvailable();
const services = stackServices();
if (services.length === 0) {
if (opts.json) {
console.log(JSON.stringify({ running: false, services: [], endpoints: [] }));
return;
}
throw new MeshCliError("The local Mesh platform is not running.", {
remediation: { command: "mesh start" },
});
}
const overlayServices = hubOverlayServices();
const overlayStarted = hubOverlayRunning(services, overlayServices);
const notStarted = (s) => !overlayStarted && overlayServices.has(s.name) && s.state === "exited";
const hasHub = hubApiRunning(services);
const endpoints = hasHub ? [...STACK_ENDPOINTS, ...hubEndpoints()] : STACK_ENDPOINTS;
const probes = await Promise.all(endpoints.map(async (endpoint) => ({
service: endpoint.service,
label: endpoint.label,
url: endpoint.url,
up: await probeEndpoint(endpoint),
})));
let fabric = null;
if (probes.find((p) => p.service === "ministack")?.up) {
try {
await verifyFabric();
fabric = { ok: true };
}
catch (err) {
fabric = { ok: false, error: err instanceof Error ? err.message : String(err) };
}
}
if (opts.json) {
console.log(JSON.stringify({
running: true,
services,
endpoints: probes,
fabric,
awsEnv: localAwsEnv(),
}));
if (fabric && !fabric.ok)
process.exitCode = 1;
return;
}
printStartHere(hasHub, new Map(probes.map((p) => [p.service, p.up])));
console.log(chalk.bold("Containers"));
for (const service of services) {
const ok = service.state === "running" &&
(service.health === undefined || service.health === "healthy");
const oneShotDone = ONE_SHOT_SERVICES.has(service.name) && service.state === "exited";
const idle = notStarted(service);
const icon = ok || oneShotDone ? chalk.green("●") : idle ? chalk.dim("○") : chalk.red("●");
const health = service.health ? ` (${service.health})` : "";
const note = idle ? chalk.dim(" (Hub not started this run)") : "";
console.log(` ${icon} ${service.name.padEnd(22)} ${service.state}${health}${note}`);
}
console.log("");
console.log(chalk.bold("Endpoints"));
for (const probe of probes) {
const icon = probe.up ? chalk.green("●") : chalk.red("●");
console.log(` ${icon} ${probe.label.padEnd(34)} ${chalk.cyan(probe.url)}`);
}
if (fabric) {
const icon = fabric.ok ? chalk.green("●") : chalk.red("●");
const detail = fabric.ok ? "8 KB Advanced-tier round-trip OK" : fabric.error;
console.log(` ${icon} ${"AWS fabric check".padEnd(34)} ${chalk.dim(detail ?? "")}`);
}
const down = probes.filter((p) => !p.up);
const crashed = crashedServices(services, { overlayServices, overlayStarted });
if (fabric && !fabric.ok)
process.exitCode = 1;
if (down.length > 0 || crashed.length > 0) {
console.log("");
logError(`${down.length + crashed.length} component(s) unhealthy — inspect with: docker compose -p mesh-local logs <service>`);
process.exitCode = 1;
}
});
}