@mesh-tech/mesh-cli
Version:
CLI for Mesh platform development utilities
246 lines (245 loc) • 9.78 kB
JavaScript
import * as fs from "fs";
import * as path from "path";
import { isServiceMode, openapiMockCommand } from "./mocks.js";
import { probeTcp } from "./helpers.js";
import { MeshCliError } from "../../utils/errors.js";
import { localAwsEnv, LOCAL_ENV } from "./seed.js";
import { TEMPORAL_ADDRESS } from "./seed.js";
import { LOGIN_CONTEXT, ZITADEL_ISSUER } from "./seed-zitadel.js";
import { authSecretPath } from "./auth-provision.js";
import { findPackageRoot } from "./stack.js";
import { getContextConfig } from "../login.js";
import { findMeshJson } from "../../utils/mesh-json.js";
export const LOCAL_STACK_NAME = "local";
const BASE_PORT = 3000;
export function localAppNamespace(tenant, appName) {
return `${tenant}-${LOCAL_ENV}-${appName}`;
}
export function logShipperPath() {
return path.join(findPackageRoot(), "assets", "log-shipper.mjs");
}
export function autoInstrumentationEnv(appRoot, src) {
const pkgDir = path.join("node_modules", "@opentelemetry", "auto-instrumentations-node");
const present = fs.existsSync(path.join(appRoot, src, pkgDir)) || fs.existsSync(path.join(appRoot, pkgDir));
if (!present)
return {};
return {
NODE_OPTIONS: { value: "--import @opentelemetry/auto-instrumentations-node/register" },
OTEL_TRACES_EXPORTER: { value: "otlp" },
OTEL_METRICS_EXPORTER: { value: "none" },
OTEL_LOGS_EXPORTER: { value: "none" },
OTEL_NODE_DISABLED_INSTRUMENTATIONS: { value: "fs,dns,net" },
};
}
export function otelEnv(tenant, appName, serviceName) {
const namespace = localAppNamespace(tenant, appName);
return {
OTEL_EXPORTER_OTLP_ENDPOINT: { value: "http://localhost:4318" },
OTEL_SERVICE_NAME: { value: serviceName },
OTEL_RESOURCE_ATTRIBUTES: {
value: `k8s.namespace.name=${namespace},k8s.deployment.name=${serviceName},k8s.container.name=${serviceName},service.namespace=${appName}`,
},
};
}
export function localPlatformEnv(tenant, appName, serviceName) {
const namespace = localAppNamespace(tenant, appName);
const env = {
TEMPORAL_ADDRESS: { value: TEMPORAL_ADDRESS },
TEMPORAL_NAMESPACE: { value: namespace },
TEMPORAL_TASK_QUEUE: { value: appName },
...otelEnv(tenant, appName, serviceName),
DATABASE_URL: { value: "postgres://postgres:postgres@localhost:5433/app" },
ZITADEL_ISSUER: { value: ZITADEL_ISSUER },
ZITADEL_SECRET_PREFIX: { value: authSecretPath(tenant, appName, serviceName) },
SPICEDB_ENDPOINT: { value: "localhost:50051" },
SPICEDB_HTTP_ENDPOINT: { value: "http://localhost:8443" },
SPICEDB_PRESHARED_KEY: { value: "local-dev-key" },
MEMCACHED_SERVERS: { value: "localhost:11211" },
MESH_LOCAL: { value: "1" },
MESH_SERVICE: { value: serviceName },
};
const cliClientId = getContextConfig(LOGIN_CONTEXT)?.clientId;
if (cliClientId) {
env.AUTH_AUDIENCE = { value: cliClientId };
}
for (const [key, value] of Object.entries(localAwsEnv())) {
env[key] = { value };
}
return env;
}
export function isDagsterWorkspaceRoot(appRoot) {
if (fs.existsSync(path.join(appRoot, "dg.toml")))
return true;
const pyproject = path.join(appRoot, "pyproject.toml");
if (!fs.existsSync(pyproject))
return false;
try {
return /^\s*\[tool\.dg[\].]/m.test(fs.readFileSync(pyproject, "utf-8"));
}
catch {
return false;
}
}
export function resolveDgBinary(appRoot) {
for (const candidate of ["deployments/local/.venv/bin/dg", ".venv/bin/dg"]) {
if (fs.existsSync(path.join(appRoot, candidate)))
return candidate;
}
return "dg";
}
function hasDevScript(dir) {
const pkgPath = path.join(dir, "package.json");
if (!fs.existsSync(pkgPath))
return false;
try {
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
return typeof pkg?.scripts?.dev === "string";
}
catch {
return false;
}
}
export function detectLocalServices(appRoot) {
const services = {};
for (const entry of fs.readdirSync(appRoot, { withFileTypes: true })) {
if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name === "node_modules")
continue;
if (hasDevScript(path.join(appRoot, entry.name))) {
services[entry.name] = { src: entry.name };
}
}
if (Object.keys(services).length === 0 && hasDevScript(appRoot)) {
services[path.basename(appRoot)] = { src: "." };
}
return services;
}
export function buildLocalDevOutput(appRoot, tenant, opts = {}) {
const detected = detectLocalServices(appRoot);
const names = Object.keys(detected);
const isDagsterWorkspace = isDagsterWorkspaceRoot(appRoot);
if (names.length === 0 && !isDagsterWorkspace) {
throw new MeshCliError(`No runnable services found under ${appRoot} — expected subdirectories (api/, worker/, …) with a package.json 'dev' script, or a Dagster workspace (dg.toml, or [tool.dg] in pyproject.toml).`, { remediation: { command: "mesh create-app" } });
}
let appName = path.basename(appRoot);
try {
const pkg = JSON.parse(fs.readFileSync(path.join(appRoot, "package.json"), "utf-8"));
if (typeof pkg?.name === "string" && pkg.name)
appName = pkg.name.replace(/^@[^/]+\//, "");
}
catch {
}
const pulumiYaml = path.join(appRoot, "Pulumi.yaml");
if (fs.existsSync(pulumiYaml)) {
const match = fs.readFileSync(pulumiYaml, "utf-8").match(/^name:\s*(.+)$/m);
if (match?.[1]?.trim())
appName = match[1].trim();
}
const services = {};
const mocks = opts.mocks ?? {};
const mockNames = Object.keys(mocks).filter((name) => isServiceMode(name, mocks[name]));
const externalEnv = {};
for (const decl of Object.values(mocks)) {
if (!decl.external)
continue;
const prefix = decl.external.toUpperCase().replace(/-/g, "_");
externalEnv[`${prefix}_SECRET_PREFIX`] = {
value: `mesh/${tenant}/${LOCAL_ENV}/external/${decl.external}`,
};
}
names.forEach((name, i) => {
const port = BASE_PORT + i;
services[name] = {
src: detected[name].src,
port,
command: ["pnpm", "dev"],
env: {
PORT: { value: String(port) },
...localPlatformEnv(tenant, appName, name),
...autoInstrumentationEnv(appRoot, detected[name].src),
...externalEnv,
},
};
});
for (const name of names) {
const key = `SERVICE_${name.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_URL`;
const url = `http://localhost:${services[name].port}`;
for (const other of names) {
services[other].env[key] = { value: url };
}
}
if (isDagsterWorkspace) {
const port = BASE_PORT + names.length + mockNames.length;
services["dagster"] = {
src: appRoot,
kind: "dagster",
port,
command: [resolveDgBinary(appRoot), "dev", "-h", "127.0.0.1", "-p", '"$PORT"'],
env: {
PORT: { value: String(port) },
DAGSTER_HOME: { value: path.join(appRoot, ".dagster") },
...localPlatformEnv(tenant, appName, "dagster"),
},
};
}
mockNames.forEach((name, i) => {
const decl = mocks[name];
const serviceName = `mock-${name}`;
const port = decl.port ?? BASE_PORT + names.length + i;
services[serviceName] = {
src: decl.src ?? ".",
port,
command: decl.openapi ? openapiMockCommand(decl) : (decl.command ?? ["pnpm", "dev"]),
env: {
PORT: { value: String(port) },
...otelEnv(tenant, appName, serviceName),
...Object.fromEntries(Object.entries(decl.env ?? {}).map(([key, value]) => [key, { value }])),
},
};
});
return {
platform: { tenant: "local", env: "dev" },
app: appName,
stack: LOCAL_STACK_NAME,
services,
tunnels: {},
};
}
export function hasStackBacking(appRoot) {
const hasOwn = fs.existsSync(path.join(appRoot, "Pulumi.yaml")) ||
fs.readdirSync(appRoot).some((f) => /^Pulumi\..+\.yaml$/.test(f));
if (hasOwn)
return true;
let dir = path.dirname(appRoot);
while (true) {
if (fs.existsSync(path.join(dir, "Pulumi.yaml")))
return true;
if (fs.existsSync(path.join(dir, ".git")) || fs.existsSync(path.join(dir, "pnpm-workspace.yaml"))) {
return fs.existsSync(path.join(dir, "Pulumi.yaml"));
}
const parent = path.dirname(dir);
if (parent === dir)
return false;
dir = parent;
}
}
export function detectLocalTenant(appRoot) {
for (const file of fs.readdirSync(appRoot)) {
if (!/^Pulumi\..*\.yaml$/.test(file))
continue;
const match = fs
.readFileSync(path.join(appRoot, file), "utf-8")
.match(/^\s*mesh:tenant:\s*["']?([A-Za-z0-9-]+)["']?\s*$/m);
if (match)
return match[1];
}
const recorded = findMeshJson(appRoot);
if (recorded)
return recorded.data.tenant;
return "local";
}
export async function ensureLocalPlatformRunning() {
const [temporalUp, fabricUp] = await Promise.all([probeTcp(7233), probeTcp(4566)]);
if (!temporalUp || !fabricUp) {
throw new MeshCliError("The local Mesh platform is not running (mesh dev local mode wires services to it).", { remediation: { command: "mesh start" } });
}
}