@mesh-tech/mesh-cli
Version:
CLI for Mesh platform development utilities
327 lines (326 loc) • 15.4 kB
JavaScript
import chalk from "chalk";
import * as fs from "fs";
import * as path from "path";
import { logInfo } from "../utils/log.js";
import { MeshCliError, emitJsonPayload } from "../utils/errors.js";
import { findUnscopedCodeArtifactRegistry, homeNpmrcPath, probeRegistryToken, } from "../utils/auth-preflight.js";
import { readCredentials } from "./login.js";
import { resolveTargetRoot, syncSkills } from "./skills.js";
import { detectLocalTenant } from "./local/dev-local.js";
import { LOGIN_CONTEXT } from "./local/seed-zitadel.js";
const LOCAL_HUB_API = "http://localhost:4568";
function isLocalContext(context) {
return context === LOGIN_CONTEXT;
}
async function probeHubTenants(hubUrl, accessToken) {
try {
const response = await fetch(`${hubUrl}/tenants`, {
headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : {},
signal: AbortSignal.timeout(5000),
});
if (response.status === 401)
return { ok: false, reason: "unauthenticated against the Hub API" };
if (!response.ok)
return { ok: false, reason: `Hub API returned ${response.status}` };
return { ok: true, tenants: (await response.json()) };
}
catch {
return { ok: false, reason: `Hub API unreachable at ${hubUrl}` };
}
}
function readHomeNpmrc() {
try {
return fs.readFileSync(homeNpmrcPath(), "utf-8");
}
catch {
return "";
}
}
export function registryFixCommand(profile) {
return profile ? `mesh registry login --profile ${profile}` : "mesh registry login";
}
export async function registryChecks(args) {
const { fix, profile } = args;
const probe = args.probe ?? probeRegistryToken;
const login = args.login ??
(async () => {
const { runRegistryLogin } = await import("./registry.js");
await runRegistryLogin(undefined, profile ? { profile } : {});
});
const REGISTRY_FIX = registryFixCommand(profile);
let tokenState = await probe({ npmrcPath: homeNpmrcPath() });
let hijack = findUnscopedCodeArtifactRegistry(readHomeNpmrc());
let repaired = false;
let loginError = null;
if (fix && (tokenState.state === "expired" || tokenState.state === "missing" || hijack)) {
try {
await login();
repaired = true;
tokenState = await probe({ npmrcPath: homeNpmrcPath() });
hijack = findUnscopedCodeArtifactRegistry(readHomeNpmrc());
}
catch (err) {
loginError = err instanceof Error ? err.message : String(err);
}
}
const suffix = repaired ? " (--fix)" : "";
const checks = [];
if (loginError) {
checks.push({
name: "Registry access",
status: "fail",
detail: `token refresh failed: ${loginError}`,
fix: REGISTRY_FIX,
});
}
else if (tokenState.state === "fresh") {
checks.push({
name: "Registry access",
status: "pass",
detail: `CodeArtifact token in ~/.npmrc is accepted by the registry${suffix}`,
});
}
else if (tokenState.state === "unreachable") {
checks.push({
name: "Registry access",
status: "warn",
detail: `could not reach the registry to verify the token (${tokenState.detail ?? "unknown error"})`,
fix: REGISTRY_FIX,
});
}
else {
checks.push({
name: "Registry access",
status: "fail",
detail: tokenState.state === "expired"
? `the CodeArtifact token in ~/.npmrc is expired (${tokenState.detail ?? "rejected by the registry"})`
: "no CodeArtifact token in ~/.npmrc",
fix: REGISTRY_FIX,
});
}
checks.push(hijack
? {
name: "Public npm not hijacked",
status: "fail",
detail: `~/.npmrc makes CodeArtifact the DEFAULT registry (${hijack.trim()}) — every public ` +
"package resolves through it and 401s when the 12h token expires",
fix: REGISTRY_FIX,
}
: {
name: "Public npm not hijacked",
status: "pass",
detail: `no unscoped CodeArtifact registry= in ~/.npmrc${repaired ? " (--fix removed it)" : ""}`,
});
return checks;
}
async function runChecks(args) {
const { root, tenant, context, hubUrl, fix, profile } = args;
const checks = [];
const local = isLocalContext(context);
const creds = readCredentials(context);
const expired = creds ? new Date(creds.expiresAt).getTime() < Date.now() : true;
checks.push(creds && !expired
? { name: "CLI auth", status: "pass", detail: `logged in (${context}${creds.email ? `, ${creds.email}` : ""})` }
: {
name: "CLI auth",
status: "fail",
detail: creds ? `credentials for '${context}' are expired` : `no credentials for context '${context}'`,
fix: `mesh login ${context}`,
});
const accessToken = creds && !expired ? creds.accessToken : null;
if (local) {
checks.push({ name: "Registry access", status: "pass", detail: "local context — no registry required" });
checks.push({ name: "Public npm not hijacked", status: "pass", detail: "local context — no registry required" });
}
else {
checks.push(...(await registryChecks({ fix, profile })));
}
const hub = await probeHubTenants(hubUrl, accessToken);
if (!hub.ok) {
const fixCmd = local ? "mesh start" : `mesh login ${context}`;
checks.push({ name: "Platform reachable", status: "fail", detail: hub.reason, fix: fixCmd });
checks.push({
name: `App tenant '${tenant}'`,
status: "warn",
detail: "not verifiable while the platform registry is unreachable",
fix: fixCmd,
});
}
else {
checks.push({
name: "Platform reachable",
status: "pass",
detail: `Hub API at ${hubUrl} (${hub.tenants.length} tenant(s) registered)`,
});
const registered = hub.tenants.some((t) => t.name === tenant);
checks.push(registered
? { name: `App tenant '${tenant}'`, status: "pass", detail: "registered in the platform registry" }
: {
name: `App tenant '${tenant}'`,
status: local ? "warn" : "fail",
detail: local
? "not registered yet — auto-created on the first `mesh dev` run of an app in this repo"
: "not registered — register it on the platform stack and deploy",
fix: local ? "mesh dev" : `mesh tenant add ${tenant} # in the platform repo, then: mesh deploy up`,
});
}
if (local) {
checks.push({ name: "Deployer role", status: "pass", detail: "local context — no deployer role required" });
}
else {
const stackFiles = fs.existsSync(root)
? fs.readdirSync(root).filter((f) => /^Pulumi\..*\.yaml$/.test(f))
: [];
const hasRole = stackFiles.some((f) => /mesh:deployerRole/.test(fs.readFileSync(path.join(root, f), "utf-8")));
checks.push(hasRole
? { name: "Deployer role", status: "pass", detail: "mesh:deployerRole configured in stack config" }
: {
name: "Deployer role",
status: "warn",
detail: "no mesh:deployerRole in Pulumi.*.yaml — `mesh deploy` will not be able to assume a role",
fix: "mesh create-app # scaffolds stack config with the deployer role",
});
}
const shapeProblems = [];
if (!fs.existsSync(path.join(root, "package.json")))
shapeProblems.push("package.json");
const hasWorkspace = fs.existsSync(path.join(root, "pnpm-workspace.yaml")) ||
fs.existsSync(path.join(root, "pnpm-lock.yaml"));
if (!hasWorkspace)
shapeProblems.push("pnpm-workspace.yaml");
checks.push(shapeProblems.length === 0
? { name: "Repo shape", status: "pass", detail: "pnpm workspace layout present" }
: {
name: "Repo shape",
status: "warn",
detail: `missing: ${shapeProblems.join(", ")}`,
fix: "mesh create-app # scaffold an app (workspace files included)",
});
const skillsOk = fix ? syncSkills(root) : syncSkills(root, { check: true });
checks.push(skillsOk
? { name: "Agent skills", status: "pass", detail: "base skills + Intent discovery in sync" }
: {
name: "Agent skills",
status: fix ? "pass" : "fail",
detail: fix ? "synced (--fix)" : "missing or stale",
fix: "mesh skills sync",
});
return checks;
}
export function registerInitCommands(program) {
const init = program
.command("init")
.enablePositionalOptions()
.description("Set up this machine and repo for a tenant — a guided wizard with no subcommand (tenant, local or deployed, package-registry sign-in, repo, skills); `init platform` scaffolds a platform repo, `init app-tenant` is the apps-repo doctor")
.option("--tenant <name>", "app tenant this repo belongs to (prompted when omitted on a TTY)")
.option("--local", "local only — mesh start / mesh dev, no deployed platform (the default)")
.option("--platform <env>", "a deployed platform's environment; signs in to <tenant>.<env>")
.option("--device", "device-code sign-in for the registry and platform steps (headless / SSH)")
.option("--profile <aws-profile>", "deployers only: AWS profile that can read the registry (skips the broker)")
.option("--skip-repo", "do not bootstrap, check or record anything in the current repo", false)
.option("--yes", "accept every default and never prompt", false)
.option("--json", "one JSON document on stdout ({tenant, mode, steps, next}); human output on stderr", false)
.action(async (opts) => {
const { runInitWizard } = await import("./init/wizard.js");
const code = await runInitWizard(opts);
if (code !== 0)
process.exitCode = code;
});
init
.command("platform <tenant>")
.description("Scaffold a tenant PLATFORM repo (core + platform Pulumi layers on @mesh-tech/infra-components) that deploys with `mesh deploy` unmodified — the proven mesh-sandbox shape, annotated.")
.option("--env <env>", "environment the first stack targets", "dev")
.option("--region <region>", "AWS region", "us-east-2")
.option("--domain <domain>", "base public domain (ingress zone becomes {env}.{domain})", "example.com")
.option("--profile <profile>", "AWS profile the stack config references", "default")
.option("--state-bucket <bucket>", "Pulumi state S3 bucket (created by an operator)", undefined)
.option("--dir <path>", "target directory (default: ./{tenant}-mesh-platform)")
.action(async (tenant, opts) => {
if (!/^[a-z][a-z0-9-]*$/.test(tenant)) {
throw new MeshCliError(`Invalid tenant name '${tenant}' (lowercase alphanumeric + dashes).`);
}
const targetDir = path.resolve(opts.dir ?? `${tenant}-mesh-platform`);
if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
throw new MeshCliError(`Target directory is not empty: ${targetDir}`, {
remediation: { command: `mesh init platform ${tenant} --dir <empty-dir>` },
});
}
const { copyTemplate } = await import("./create-app.js");
const { findPackageRoot } = await import("./local/stack.js");
const templateDir = path.join(findPackageRoot(), "templates", "platform-repo");
const context = {
tenant,
env: opts.env,
region: opts.region,
domain: opts.domain,
profile: opts.profile,
stateBucket: opts.stateBucket ?? `${tenant}-mesh-platform-pulumi-state`,
};
fs.mkdirSync(targetDir, { recursive: true });
copyTemplate(templateDir, targetDir, context);
syncSkills(targetDir);
logInfo(`Platform repo scaffolded at ${targetDir}`);
console.log("");
console.log("Next steps:");
console.log(` cd ${path.relative(process.cwd(), targetDir) || "."}`);
console.log(` mesh login ${tenant}.${opts.env} # or an operator context with access`);
console.log(" pnpm install");
console.log(" cd core && mesh deploy up");
console.log(" cd ../platform && mesh deploy up");
console.log("");
logInfo(`Review the annotated stack configs (Pulumi.${tenant}-${opts.env}.yaml) before the first deploy.`);
});
init
.command("app-tenant")
.description("Check (and with --fix, repair) everything an app-tenant repo needs: auth, registry, tenant registration, repo shape, agent skills. Re-run any time — it is the doctor.")
.option("--tenant <name>", "app tenant (default: mesh:tenant from Pulumi config, else 'local')")
.option("--context <ctx>", "login context to check against (default: 'local' when the local platform is targeted)", LOGIN_CONTEXT)
.option("--hub-url <url>", "platform Hub API base URL", LOCAL_HUB_API)
.option("--fix", "apply developer-scope fixes (registry token, skills sync)", false)
.option("--profile <profile>", "AWS SSO profile --fix logs into CodeArtifact with (e.g. mesh-dev)")
.option("--json", "machine-readable output", false)
.action(async (opts) => {
const root = resolveTargetRoot();
const tenant = opts.tenant ?? detectLocalTenant(root);
if (!opts.json) {
logInfo(`Checking app-tenant repo ${root} (tenant '${tenant}', context '${opts.context}')…`);
}
const origLog = console.log;
if (opts.json)
console.log = (...a) => console.error(...a);
let checks;
try {
checks = await runChecks({
root,
tenant,
context: opts.context,
hubUrl: opts.hubUrl.replace(/\/+$/, ""),
fix: opts.fix,
profile: opts.profile,
});
}
finally {
console.log = origLog;
}
const ok = !checks.some((c) => c.status === "fail");
if (opts.json) {
emitJsonPayload({ ok, root, tenant, context: opts.context, checks });
}
else {
console.log("");
for (const check of checks) {
const icon = check.status === "pass" ? chalk.green("✔") : check.status === "warn" ? chalk.yellow("▲") : chalk.red("✘");
console.log(` ${icon} ${check.name.padEnd(24)} ${check.detail}`);
if (check.fix && check.status !== "pass") {
console.log(` ${chalk.dim("→ run:")} ${chalk.cyan(check.fix)}`);
}
}
console.log("");
}
if (!ok) {
throw new MeshCliError("app-tenant checks failed — apply the fixes above and re-run.", {
remediation: { command: "mesh init app-tenant --fix" },
});
}
});
}