@mesh-tech/mesh-cli
Version:
CLI for Mesh platform development utilities
603 lines (602 loc) • 24.5 kB
JavaScript
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import { fileURLToPath } from "url";
import Handlebars from "handlebars";
import { parse as parseYaml } from "yaml";
import { logInfo, logSuccess, logError, logWarn } from "../utils/index.js";
import { INTENT_RANGE, resolveTargetRoot, seedAppSkill, syncSkills } from "./skills.js";
import { renderReport, runAppCheck } from "./app-check.js";
import { findPackageRoot } from "./local/stack.js";
import { SCAFFOLD_TOOLCHAIN, resolvePublishedRange } from "../utils/scaffold-versions.js";
import { registryPreflight } from "../utils/auth-preflight.js";
import { MeshCliError } from "../utils/errors.js";
import { findMeshJson } from "../utils/mesh-json.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const packageRoot = findPackageRoot(__dirname);
const TEMPLATES = {
workflow: "Hono API + Temporal worker + hello workflow (Pulumi/app-kit)",
"api-auth": "Hono API with platform auth: authn (Zitadel JWT) + authz (SpiceDB)",
"api-role-gating": "Hono API with role-gating authz: authn (Zitadel JWT) + coarse-role checks on Zitadel project roles, in-process (no SpiceDB)",
"external-service": "Hono API calling a third-party vendor via platform-managed credentials (ExternalService + mock for `mesh dev --externals`)",
};
const REMOVED_TEMPLATES = {
"temporal-api-worker": "renamed to 'workflow'",
"api-web-db": "removed (SST-based) — use --primitives service,database",
};
const PRIMITIVES = {
service: "HTTP Service (API with Hono)",
database: "Database (PostgreSQL via Prisma)",
temporal: "Temporal (workflow orchestration)",
bucket: "S3 Bucket (file storage)",
};
const HUB_ACCOUNTS = {
mesh: "159923586610",
};
export const PLATFORM_MONOREPO_NAME = "mesh-platform";
export function isInsidePlatformMonorepo(dir) {
let cur = path.resolve(dir);
while (cur !== path.dirname(cur)) {
if (fs.existsSync(path.join(cur, "pnpm-workspace.yaml"))) {
try {
const pkg = JSON.parse(fs.readFileSync(path.join(cur, "package.json"), "utf-8"));
if (pkg?.name === PLATFORM_MONOREPO_NAME)
return true;
}
catch {
}
}
cur = path.dirname(cur);
}
return false;
}
function resolveDeployerRoleArn(tenant, platformName, env) {
const account = HUB_ACCOUNTS[platformName] ?? "ACCOUNT_ID";
return `arn:aws:iam::${account}:role/${tenant}-${env}-apps-deployer`;
}
const VALID_PRIMITIVES = Object.keys(PRIMITIVES);
export function shouldBootstrapAppsRepo(cwd) {
return (fs.existsSync(path.join(cwd, ".git")) &&
!isInsidePlatformMonorepo(cwd) &&
!fs.existsSync(path.join(cwd, "package.json")) &&
!fs.existsSync(path.join(cwd, "tenants")) &&
!fs.existsSync(path.join(cwd, "apps")));
}
export function bootstrapAppsRepo(cwd, tenant) {
const templateDir = path.join(packageRoot, "templates", "apps-repo");
const registryAccount = HUB_ACCOUNTS.mesh;
const context = {
repoName: path.basename(cwd),
tenant,
tenantTitle: tenant
.split("-")
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
.join(" "),
registryUrl: `https://mesh-platform-${registryAccount}.d.codeartifact.us-east-2.amazonaws.com/npm/mesh-packages/`,
intentRange: INTENT_RANGE,
};
const staging = fs.mkdtempSync(path.join(os.tmpdir(), "mesh-apps-repo-"));
try {
copyTemplate(templateDir, staging, context);
for (const [from, to] of [
["gitignore", ".gitignore"],
["npmrc", ".npmrc"],
]) {
const p = path.join(staging, from);
if (fs.existsSync(p))
fs.renameSync(p, path.join(staging, to));
}
const created = [];
for (const entry of fs.readdirSync(staging)) {
const dest = path.join(cwd, entry);
if (fs.existsSync(dest))
continue;
fs.cpSync(path.join(staging, entry), dest, { recursive: true });
created.push(entry);
}
fs.mkdirSync(path.join(cwd, "apps"), { recursive: true });
created.push("apps/");
return created;
}
finally {
fs.rmSync(staging, { recursive: true, force: true });
}
}
export function ensureWorkspaceGlobs(root, required) {
const file = path.join(root, "pnpm-workspace.yaml");
if (!fs.existsSync(file))
return [];
const text = fs.readFileSync(file, "utf-8");
let declared;
try {
declared = (parseYaml(text)?.packages ?? []);
}
catch {
return [];
}
const missing = required.filter((glob) => !declared.includes(glob));
if (missing.length === 0)
return [];
const lines = text.split("\n");
const start = lines.findIndex((line) => /^packages:\s*$/.test(line));
if (start === -1)
return [];
let end = start;
for (let i = start + 1; i < lines.length; i++) {
const line = lines[i] ?? "";
const isEntry = /^\s+-\s/.test(line);
if (isEntry || /^\s*#/.test(line) || line.trim() === "") {
if (isEntry)
end = i;
continue;
}
break;
}
const indent = (lines[end] ?? "").match(/^(\s*)-/)?.[1] ?? " ";
lines.splice(end + 1, 0, ...missing.map((glob) => `${indent}- ${glob}`));
fs.writeFileSync(file, lines.join("\n"));
return missing;
}
export function workspaceGlobForApp(root, appDir) {
const rel = path.relative(root, appDir).split(path.sep).join("/");
if (rel === "" || rel.split("/")[0] === "..")
return null;
return `${path.posix.dirname(rel)}/*/*`;
}
function maybeBootstrapAppsRepo(cwd, tenant, test) {
if (test || !shouldBootstrapAppsRepo(cwd))
return;
const created = bootstrapAppsRepo(cwd, tenant);
logInfo(`Standalone apps repo detected — bootstrapped workspace files: ${created.join(", ")}`);
}
Handlebars.registerHelper("titleCase", (str) => {
return str
.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
});
function isInteractive() {
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
}
async function promptForOptions(options) {
const { default: input } = await import("@inquirer/input");
const { default: checkbox } = await import("@inquirer/checkbox");
const tenant = options.tenant ??
(await input({
message: "Tenant name",
default: findMeshJson(process.cwd())?.data.tenant,
validate: (v) => (v.length > 0 ? true : "Tenant name is required"),
}));
const name = options.name ??
(await input({
message: "App name",
validate: (v) => (v.length > 0 ? true : "App name is required"),
}));
let primitives;
if (options.primitives) {
primitives = parsePrimitives(options.primitives);
}
else {
primitives = await checkbox({
message: "Select primitives",
choices: VALID_PRIMITIVES.map((p) => ({
name: PRIMITIVES[p],
value: p,
checked: p === "service",
})),
});
if (primitives.length === 0) {
primitives = ["service"];
}
}
if (primitives.includes("database") && !primitives.includes("service")) {
primitives.push("service");
logInfo("Added 'service' — database files live in the api/ directory.");
}
if (primitives.includes("bucket") && !primitives.includes("service")) {
primitives.push("service");
logInfo("Added 'service' — bucket requires an API service for S3 helpers.");
}
return { tenant, name, primitives };
}
export function copyTemplate(srcDir, destDir, context) {
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
for (const entry of entries) {
const srcPath = path.join(srcDir, entry.name);
let destName = entry.name;
if (destName.endsWith(".hbs")) {
destName = destName.slice(0, -4);
}
if (destName.includes("{{")) {
destName = Handlebars.compile(destName)(context);
}
const destPath = path.join(destDir, destName);
if (entry.isDirectory()) {
fs.mkdirSync(destPath, { recursive: true });
copyTemplate(srcPath, destPath, context);
}
else if (entry.name.endsWith(".hbs")) {
const templateContent = fs.readFileSync(srcPath, "utf-8");
const template = Handlebars.compile(templateContent);
const output = template(context);
fs.writeFileSync(destPath, output);
}
else {
fs.copyFileSync(srcPath, destPath);
}
}
}
function parsePrimitives(input) {
const selected = input.split(",").map((s) => s.trim().toLowerCase());
const invalid = selected.filter((s) => !VALID_PRIMITIVES.includes(s));
if (invalid.length > 0) {
logError(`Unknown primitives: ${invalid.join(", ")}`);
logInfo(`Available primitives: ${VALID_PRIMITIVES.join(", ")}`);
process.exit(1);
}
return selected;
}
export function describeNoAppsHome(cwd, tenant, baseDir, test) {
if (test || isInsidePlatformMonorepo(cwd)) {
return {
message: `Tenant directory not found: ${baseDir}/${tenant}/`,
hint: "Create the tenant directory first, or run from within it.",
};
}
return {
message: `This directory is not a Mesh apps repo — no apps/ here${fs.existsSync(path.join(cwd, ".git")) ? "" : " and it is not a git repo"}.`,
hint: `Run: mesh init (turns this folder into your ${tenant}-mesh-apps repo), or cd into that repo and re-run mesh create-app.`,
};
}
export function resolveAppDir(cwd, tenant, name, test) {
const baseDir = test ? "tests/tenants" : "tenants";
const possiblePaths = test
? [path.join(cwd, baseDir, tenant, "apps")]
: [
path.join(cwd, baseDir, tenant, "apps"),
path.join(cwd, "..", tenant, "apps"),
path.join(cwd, "apps"),
];
let appsDir = possiblePaths.find((p) => fs.existsSync(p));
if (!appsDir) {
appsDir = path.join(cwd, baseDir, tenant, "apps");
if (!fs.existsSync(path.join(cwd, baseDir, tenant))) {
const { message, hint } = describeNoAppsHome(cwd, tenant, baseDir, test);
logError(message);
logInfo(hint);
process.exit(1);
}
if (!fs.existsSync(appsDir)) {
logInfo(`Creating apps directory: ${appsDir}`);
fs.mkdirSync(appsDir, { recursive: true });
}
}
return path.join(appsDir, name);
}
export function generateComposableApp(appDir, context) {
const fragmentsDir = path.join(packageRoot, "fragments");
const baseDir = path.join(fragmentsDir, "base");
if (!fs.existsSync(baseDir)) {
logError(`Fragments directory not found: ${baseDir}`);
logInfo("Make sure the mesh-cli package is installed correctly.");
process.exit(1);
}
copyTemplate(baseDir, appDir, context);
if (context.service) {
const serviceDir = path.join(fragmentsDir, "service");
if (!fs.existsSync(serviceDir)) {
throw new Error(`Fragment directory not found: ${serviceDir}`);
}
copyTemplate(serviceDir, appDir, context);
}
if (context.database) {
const dbDir = path.join(fragmentsDir, "database");
if (!fs.existsSync(dbDir)) {
throw new Error(`Fragment directory not found: ${dbDir}`);
}
copyTemplate(dbDir, appDir, context);
}
if (context.temporal) {
const temporalDir = path.join(fragmentsDir, "temporal");
if (!fs.existsSync(temporalDir)) {
throw new Error(`Fragment directory not found: ${temporalDir}`);
}
copyTemplate(temporalDir, appDir, context);
}
if (context.bucket) {
const bucketDir = path.join(fragmentsDir, "bucket");
if (!fs.existsSync(bucketDir)) {
throw new Error(`Fragment directory not found: ${bucketDir}`);
}
copyTemplate(bucketDir, appDir, context);
}
const agentsDir = path.join(fragmentsDir, "agents");
if (fs.existsSync(agentsDir)) {
copyTemplate(agentsDir, appDir, context);
}
}
export function registerCreateAppCommand(program) {
program
.command("create-app")
.description("Scaffold a new tenant application")
.option("--tenant <tenant>", "Tenant name (e.g., acme)")
.option("--name <name>", "Application name (e.g., billing)")
.option("--template <template>", `Legacy template: ${Object.keys(TEMPLATES).join(", ")}`)
.option("--primitives <primitives>", `Comma-separated primitives: ${VALID_PRIMITIVES.join(", ")} (default: service)`)
.option("--test", "Create in tests/tenants/ directory", false)
.option("--skip-registry-check", "Scaffold without checking that this machine can install @mesh-tech packages (offline use; run `mesh registry login` before `pnpm install`)", false)
.action(async (options) => {
await ensureRegistryAccess(process.cwd(), options);
if (options.template) {
if (!options.tenant || !options.name) {
logError("--tenant and --name are required with --template");
process.exit(1);
}
await runLegacyTemplate(options.tenant, options.name, options.template, options.test);
return;
}
let tenant;
let name;
let primitives;
const needsPrompt = !options.tenant || !options.name || !options.primitives;
if (needsPrompt && isInteractive()) {
const prompted = await promptForOptions(options);
tenant = prompted.tenant;
name = prompted.name;
primitives = prompted.primitives;
}
else if (needsPrompt) {
options.tenant ??= findMeshJson(process.cwd())?.data.tenant;
if (!options.tenant) {
logError("--tenant is required (no TTY for interactive mode)");
process.exit(1);
}
if (!options.name) {
logError("--name is required (no TTY for interactive mode)");
process.exit(1);
}
tenant = options.tenant;
name = options.name;
primitives = options.primitives
? parsePrimitives(options.primitives)
: ["service"];
}
else {
tenant = options.tenant;
name = options.name;
primitives = parsePrimitives(options.primitives);
}
if (primitives.includes("database") && !primitives.includes("service")) {
primitives.push("service");
logInfo("Added 'service' — database files live in the api/ directory.");
}
if (primitives.includes("bucket") && !primitives.includes("service")) {
primitives.push("service");
logInfo("Added 'service' — bucket requires an API service for S3 helpers.");
}
await runComposable(tenant, name, primitives, options.test);
});
}
export const NO_REGISTRY_ACCESS_MESSAGE = "No registry access on this machine — @mesh-tech packages cannot be installed.\n" +
" Run: mesh init\n" +
" (sets up your tenant and signs you in to the package registry)\n" +
" Scaffolding offline anyway? add --skip-registry-check";
export async function ensureRegistryAccess(cwd, options, preflight = registryPreflight) {
if (isInsidePlatformMonorepo(cwd))
return;
if (options.skipRegistryCheck) {
logWarn("Skipping the registry check — `pnpm install` will need `mesh registry login` first.");
return;
}
const pf = await preflight();
if (pf.state === "unreachable") {
logWarn(`Could not reach the package registry to verify access (${pf.detail ?? "unknown error"}) — continuing.`);
return;
}
if (pf.state !== "valid") {
throw new MeshCliError(NO_REGISTRY_ACCESS_MESSAGE);
}
logSuccess(`Registry access OK (${pf.email ?? "token valid"})`);
}
async function runLegacyTemplate(tenant, name, template, test) {
if (!Object.keys(TEMPLATES).includes(template)) {
const removed = REMOVED_TEMPLATES[template];
logError(`Unknown template: ${template}${removed ? ` (${removed})` : ""}`);
logInfo(`Available templates:`);
for (const [tplName, desc] of Object.entries(TEMPLATES)) {
console.log(` ${tplName.padEnd(20)} - ${desc}`);
}
process.exit(1);
}
logInfo(`Creating app '${name}' for tenant '${tenant}'...`);
logInfo(`Template: ${template} (${TEMPLATES[template]})`);
if (test) {
logInfo("Creating in tests/ directory");
}
maybeBootstrapAppsRepo(process.cwd(), tenant, test);
const appDir = resolveAppDir(process.cwd(), tenant, name, test);
if (fs.existsSync(appDir)) {
logError(`App directory already exists: ${appDir}`);
process.exit(1);
}
const templateDir = path.join(packageRoot, "templates", template);
if (!fs.existsSync(templateDir)) {
logError(`Template not found: ${templateDir}`);
process.exit(1);
}
fs.mkdirSync(appDir, { recursive: true });
try {
copyTemplate(templateDir, appDir, { name, tenant });
logSuccess(`Created app at ${appDir}`);
}
catch (error) {
logError(`Failed to create app: ${error}`);
fs.rmSync(appDir, { recursive: true, force: true });
process.exit(1);
}
autoSyncSkills(appDir, name);
await checkScaffoldContract(appDir);
printLegacyNextSteps(appDir, template);
}
function autoSyncSkills(appDir, appName) {
const root = resolveTargetRoot(appDir);
try {
syncSkills(root);
}
catch (err) {
logWarn(`Agent-skill sync skipped: ${err instanceof Error ? err.message : err} — run: mesh skills sync`);
}
try {
const seeded = seedAppSkill(root, appName, path.relative(root, appDir));
if (seeded)
logSuccess(`seeded: ${seeded} (yours to grow — not managed by mesh skills sync)`);
}
catch (err) {
logWarn(`App skill stub skipped: ${err instanceof Error ? err.message : err}`);
}
}
async function checkScaffoldContract(appDir) {
const root = resolveTargetRoot(appDir);
const report = await runAppCheck(root, [path.relative(root, appDir) || "."]);
if (report.status === "ok") {
logSuccess("app contract: met (mesh app check)");
return;
}
console.log(renderReport(report, { verbose: false }));
if (report.status === "warn") {
logSuccess("app contract: met — the ⚠ lines above are advisories on the repo, not on this app (mesh app check)");
return;
}
if (report.status === "error") {
logError("the scaffold does not meet the Mesh app contract — this is a template defect; file a platform item and cite the ✗ lines above");
process.exitCode = 1;
}
}
async function runComposable(tenant, name, primitives, test) {
logInfo(`Creating app '${name}' for tenant '${tenant}'...`);
logInfo(`Primitives: ${primitives.join(", ")}`);
if (test) {
logInfo("Creating in tests/ directory");
}
maybeBootstrapAppsRepo(process.cwd(), tenant, test);
const appDir = resolveAppDir(process.cwd(), tenant, name, test);
if (fs.existsSync(appDir)) {
logError(`App directory already exists: ${appDir}`);
process.exit(1);
}
const region = "us-east-2";
const platformName = "mesh";
const platformEnv = "dev";
const workspaceDeps = isInsidePlatformMonorepo(appDir);
const repoRoot = resolveTargetRoot(process.cwd());
const mesh = workspaceDeps
? { range: "", resolved: true }
: resolvePublishedRange("@mesh-tech/app-kit", repoRoot);
const esmBundle = workspaceDeps
? { range: "", resolved: true }
: resolvePublishedRange("@mesh-tech/esm-bundle", repoRoot);
if (!workspaceDeps) {
logInfo(mesh.resolved
? `@mesh-tech/* pinned to ${mesh.range} (current published line)`
: `Registry lookup unavailable — pinning @mesh-tech/* to ${mesh.range}; run \`pnpm up @mesh-tech/*\` once you have registry auth`);
}
const context = {
name,
tenant,
service: primitives.includes("service"),
database: primitives.includes("database"),
temporal: primitives.includes("temporal"),
bucket: primitives.includes("bucket"),
region,
workspaceDeps,
meshRange: mesh.range,
esmBundleRange: esmBundle.range,
typesNodeRange: SCAFFOLD_TOOLCHAIN["@types/node"],
tsxRange: SCAFFOLD_TOOLCHAIN.tsx,
typescriptRange: SCAFFOLD_TOOLCHAIN.typescript,
deployerRoleArn: resolveDeployerRoleArn(tenant, platformName, platformEnv),
};
fs.mkdirSync(appDir, { recursive: true });
try {
generateComposableApp(appDir, context);
logSuccess(`Created app at ${appDir}`);
}
catch (error) {
logError(`Failed to create app: ${error}`);
fs.rmSync(appDir, { recursive: true, force: true });
process.exit(1);
}
const nested = fs
.readdirSync(appDir, { withFileTypes: true })
.some((entry) => entry.isDirectory() && fs.existsSync(path.join(appDir, entry.name, "package.json")));
const glob = workspaceGlobForApp(repoRoot, appDir);
if (nested && glob) {
const added = ensureWorkspaceGlobs(repoRoot, [glob]);
if (added.length > 0) {
logInfo(`pnpm-workspace.yaml: added ${added.join(", ")} so the app's api/worker packages install`);
}
}
autoSyncSkills(appDir, name);
await checkScaffoldContract(appDir);
printComposableNextSteps(appDir, context);
}
function printLegacyNextSteps(appDir, _template) {
console.log("");
logSuccess("App created successfully!");
console.log("");
console.log("Next steps:");
console.log(` cd ${appDir}`);
console.log(" pnpm install");
console.log(" pnpm mesh dev");
console.log("");
console.log("To deploy:");
console.log(" pnpm mesh deploy up");
console.log("");
}
function printComposableNextSteps(appDir, context) {
console.log("");
logSuccess("App created successfully!");
console.log("");
console.log("Generated:");
console.log(" index.ts Pulumi app definition");
console.log(" AGENTS.md AI agent instructions");
console.log(" CLAUDE.md Bridges AGENTS.md into Claude Code (@AGENTS.md)");
if (context.service) {
console.log(" api/ HTTP service (Hono)");
}
if (context.temporal) {
console.log(" worker/ Temporal worker");
}
if (context.database) {
console.log(" prisma/ Database schema");
}
if (context.bucket) {
console.log(" api/src/storage.ts S3 helpers");
}
console.log("");
for (const line of composableNextSteps(appDir, findMeshJson(process.cwd())?.data.platform ?? null)) {
console.log(line);
}
}
export function composableNextSteps(appDir, platform) {
const common = [
"Next steps:",
` cd ${appDir}`,
" pnpm install",
" mesh skills sync # re-run once deps are installed: picks up package skills + Intent",
" pnpm install # again only if skills sync reports it added @tanstack/intent",
];
const local = [
" mesh start # the local Mesh platform (once; Docker — from anywhere)",
" mesh dev # run the app against it",
];
const deploy = [
" mesh stack init # personal dev stack (deploy: false)",
" mesh deploy up --yes # deploy via the stack's deployer role",
];
if (platform === null || platform === "local") {
return [...common, ...local, "", "When you deploy to a Mesh platform:", ...deploy];
}
return [...common, ...deploy, " mesh dev # run locally against the platform"];
}