UNPKG

@mesh-tech/mesh-cli

Version:

CLI for Mesh platform development utilities

327 lines (326 loc) 15.4 kB
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" }, }); } }); }