UNPKG

@mesh-tech/mesh-cli

Version:

CLI for Mesh platform development utilities

283 lines (282 loc) 12.8 kB
import { execFile, execFileSync } from "child_process"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; import { promisify } from "util"; import { logInfo, logSuccess, logWarn, startHeartbeat } from "../../utils/log.js"; import { MeshCliError } from "../../utils/errors.js"; import { isNpmAuthError, REGISTRY_LOGIN_FIX } from "../../utils/auth-preflight.js"; import { findPackageRoot } from "./stack.js"; import { meshCacheDir } from "../../utils/cache-home.js"; const execFileAsync = promisify(execFile); const HUB_PACKAGE = "@mesh-tech/hub"; export const HUB_IMAGES = ["mesh-local-hub-api", "mesh-local-hub-ui"]; const LOCAL_IMAGE_REV = "r2"; function cacheDir() { return meshCacheDir("hub-local"); } function npmrcPath() { const p = path.join(os.homedir(), ".npmrc"); if (!fs.existsSync(p) || !fs.readFileSync(p, "utf-8").includes("codeartifact")) { throw new MeshCliError("The local Hub builds from the published @mesh-tech/hub tarball, which needs registry auth.", { remediation: { command: REGISTRY_LOGIN_FIX } }); } return p; } function imageExists(tag) { try { execFileSync("docker", ["image", "inspect", tag], { stdio: ["ignore", "pipe", "pipe"] }); return true; } catch { return false; } } const HUB_AUTH_IMAGE = "mesh-local-hub-auth:v7.7.1-r1"; export function ensureHubAuthImage() { if (imageExists(HUB_AUTH_IMAGE)) return; logInfo(`Building ${HUB_AUTH_IMAGE} (Hub auth proxy)…`); const hubStackDir = path.join(findPackageRoot(), "stack", "hub"); execFileSync("docker", ["build", "-f", path.join(hubStackDir, "Dockerfile.auth"), "-t", HUB_AUTH_IMAGE, hubStackDir], { stdio: ["ignore", "inherit", "inherit"], env: { ...process.env, DOCKER_BUILDKIT: "1" } }); logSuccess(`Built ${HUB_AUTH_IMAGE}`); } export function hasRegistryAuth() { const p = path.join(os.homedir(), ".npmrc"); return fs.existsSync(p) && fs.readFileSync(p, "utf-8").includes("codeartifact"); } export function localHubVersion() { const versions = HUB_IMAGES.map((name) => { try { const out = execFileSync("docker", ["images", name, "--format", "{{.Tag}}"], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], }); return out.split("\n").map((t) => t.trim()).filter((t) => t && t !== "<none>"); } catch { return []; } }); const shared = versions[0].filter((v) => v.endsWith(`-${LOCAL_IMAGE_REV}`) && versions.every((list) => list.includes(v))); if (shared.length === 0) return undefined; return shared.sort((a, b) => a.localeCompare(b, undefined, { numeric: true })).at(-1); } async function fetchTarball() { const dir = cacheDir(); logInfo(`Resolving ${HUB_PACKAGE} from the registry…`); const heartbeat = startHeartbeat(`fetching the ${HUB_PACKAGE} tarball from CodeArtifact`); try { const { stdout } = await execFileAsync("npm", ["pack", `${HUB_PACKAGE}@latest`, "--pack-destination", dir, "--json"], { encoding: "utf-8" }); const info = JSON.parse(stdout)[0]; return { tarball: path.join(dir, info.filename), version: info.version }; } catch (err) { const authFailure = isNpmAuthError(err); const cached = fs .readdirSync(dir) .filter((f) => f.startsWith("mesh-tech-hub-") && f.endsWith(".tgz")) .sort((a, b) => a.localeCompare(b, undefined, { numeric: true })) .at(-1); if (!cached) { if (authFailure) { throw new MeshCliError(`The registry rejected the ${HUB_PACKAGE} tarball fetch (E401 Unauthorized) — your CodeArtifact token is expired or invalid. (npm's "try npm login" advice does not apply to this registry.)`, { remediation: { command: REGISTRY_LOGIN_FIX }, cause: err }); } throw err; } const version = cached.replace("mesh-tech-hub-", "").replace(/\.tgz$/, ""); if (authFailure) { logWarn(`Registry rejected the fetch (E401 — CodeArtifact token expired). Using the cached hub tarball v${version}; refresh with: ${REGISTRY_LOGIN_FIX}`); } else { logInfo(`Registry unavailable — using the cached hub tarball v${version}`); } return { tarball: path.join(dir, cached), version }; } finally { heartbeat.stop(); } } export function planWithHubRefresh(probeState, builtVersion) { if (probeState !== "expired" && probeState !== "missing") return { action: "refresh" }; if (builtVersion) { return { action: "use-local", version: builtVersion, warning: `CodeArtifact token is ${probeState} — using the already-built local Hub images (v${builtVersion}). ` + `To pick up a newer published Hub: ${REGISTRY_LOGIN_FIX}, then re-run mesh start --with-hub.`, }; } return { action: "fail", message: probeState === "expired" ? "Your CodeArtifact token is expired or rejected — the Hub build would fail minutes in with E401." : "No CodeArtifact registry auth found in ~/.npmrc — the Hub builds from the published @mesh-tech/hub tarball.", remediation: REGISTRY_LOGIN_FIX, }; } export async function ensureHubImages() { const npmrc = npmrcPath(); const { tarball, version: packageVersion } = await fetchTarball(); const version = `${packageVersion}-${LOCAL_IMAGE_REV}`; const tags = HUB_IMAGES.map((name) => `${name}:${version}`); if (tags.every(imageExists)) { logInfo(`Local Hub images ready (v${version})`); return version; } const context = path.join(cacheDir(), `context-${version}`); fs.rmSync(context, { recursive: true, force: true }); fs.mkdirSync(context, { recursive: true }); execFileSync("tar", ["-xzf", tarball, "-C", context, "--strip-components", "1"], { stdio: ["ignore", "pipe", "pipe"], }); const hubStackDir = path.join(findPackageRoot(), "stack", "hub"); for (const [name, dockerfile] of [ ["mesh-local-hub-api", "Dockerfile.api"], ["mesh-local-hub-ui", "Dockerfile.ui"], ]) { const tag = `${name}:${version}`; if (imageExists(tag)) continue; logInfo(`Building ${tag} from the published tarball…`); execFileSync("docker", [ "build", "-f", path.join(hubStackDir, dockerfile), "-t", tag, "--secret", `id=npmrc,src=${npmrc}`, context, ], { stdio: ["ignore", "inherit", "inherit"], env: { ...process.env, DOCKER_BUILDKIT: "1" } }); logSuccess(`Built ${tag}`); } return version; } export const HUB_COMPILED_AUTHZ_REL = "api/dist/authz/compiled.json"; export function tarballNameForImageVersion(version) { if (version.endsWith("-src")) return null; return `mesh-tech-hub-${version.replace(/-r\d+$/, "")}.tgz`; } export function readHubCompiledAuthz(version) { const context = path.join(cacheDir(), `context-${version}`); const file = path.join(context, HUB_COMPILED_AUTHZ_REL); if (!fs.existsSync(file)) { const tarball = tarballNameForImageVersion(version); if (!tarball || !fs.existsSync(path.join(cacheDir(), tarball))) return null; fs.mkdirSync(context, { recursive: true }); try { execFileSync("tar", ["-xzf", path.join(cacheDir(), tarball), "-C", context, "--strip-components", "1", `package/${HUB_COMPILED_AUTHZ_REL}`], { stdio: ["ignore", "pipe", "pipe"] }); } catch { return null; } if (!fs.existsSync(file)) return null; } const parsed = JSON.parse(fs.readFileSync(file, "utf-8")); if (typeof parsed.zed !== "string" || !parsed.metadata || typeof parsed.metadata !== "object") return null; return { zed: parsed.zed, metadata: parsed.metadata }; } export function readWorkspaceCatalog(repoRoot) { const text = fs.readFileSync(path.join(repoRoot, "pnpm-workspace.yaml"), "utf-8"); const marker = "\ncatalog:\n"; const at = text.indexOf(marker); if (at === -1) return {}; const catalog = {}; for (const line of text.slice(at + marker.length).split("\n")) { if (line.trim() !== "" && !/^\s/.test(line)) break; const m = /^\s+"?([^":\s]+)"?:\s*"?([^"\s#]+)"?/.exec(line); if (m) catalog[m[1]] = m[2]; } return catalog; } export function normalizeHubManifests(contextDir, catalog) { const DEP_FIELDS = ["dependencies", "optionalDependencies", "peerDependencies"]; const touched = []; for (const entry of fs.readdirSync(contextDir, { withFileTypes: true })) { if (!entry.isDirectory()) continue; const manifest = path.join(contextDir, entry.name, "package.json"); if (!fs.existsSync(manifest)) continue; const pkg = JSON.parse(fs.readFileSync(manifest, "utf-8")); let changed = false; if (pkg.devDependencies) { delete pkg.devDependencies; changed = true; } for (const field of DEP_FIELDS) { const deps = pkg[field]; if (!deps) continue; for (const [name, spec] of Object.entries(deps)) { if (typeof spec !== "string") continue; if (spec.startsWith("workspace:")) { delete deps[name]; changed = true; } else if (spec.startsWith("catalog:")) { const key = spec.slice("catalog:".length) || name; const range = catalog[name] ?? catalog[key]; if (!range) { throw new MeshCliError(`${entry.name}/package.json depends on "${name}": "${spec}", which the workspace catalog does not define.`, { remediation: { command: "Add the dependency to the `catalog:` block in pnpm-workspace.yaml" } }); } deps[name] = range; changed = true; } } } if (changed) { fs.writeFileSync(manifest, `${JSON.stringify(pkg, null, 2)}\n`); touched.push(`${entry.name}/package.json`); } } return touched; } export async function buildHubImagesFromSource(repoRoot) { const hubDir = path.join(repoRoot, "apps", "hub"); if (!fs.existsSync(path.join(hubDir, "package.json"))) { throw new MeshCliError(`--hub-from-source needs a mesh-platform checkout; no apps/hub under ${repoRoot}.`, { remediation: { command: "Run mesh start from a mesh-platform checkout, or drop --hub-from-source" } }); } const npmrc = npmrcPath(); const version = `${JSON.parse(fs.readFileSync(path.join(hubDir, "package.json"), "utf-8")).version}-src`; const build = startHeartbeat("building apps/hub (api + ui) from source"); try { for (const pkg of ["@mesh-tech/hub-api", "@mesh-tech/hub-ui"]) { await execFileAsync("pnpm", ["--filter", pkg, "build"], { cwd: repoRoot, maxBuffer: 64 * 1024 * 1024 }); } } finally { build.stop(); } const context = path.join(cacheDir(), `context-${version}`); fs.rmSync(context, { recursive: true, force: true }); fs.mkdirSync(context, { recursive: true }); await execFileAsync("pnpm", ["pack", "--pack-destination", context], { cwd: hubDir, maxBuffer: 64 * 1024 * 1024 }); const tarball = fs.readdirSync(context).find((f) => f.endsWith(".tgz")); if (!tarball) throw new MeshCliError("pnpm pack produced no tarball for apps/hub"); execFileSync("tar", ["-xzf", path.join(context, tarball), "-C", context, "--strip-components", "1"], { stdio: ["ignore", "pipe", "pipe"], }); const rewritten = normalizeHubManifests(context, readWorkspaceCatalog(repoRoot)); if (rewritten.length > 0) logInfo(`Normalized ${rewritten.length} manifest(s) for npm: ${rewritten.join(", ")}`); const hubStackDir = path.join(findPackageRoot(), "stack", "hub"); for (const [name, dockerfile] of [ ["mesh-local-hub-api", "Dockerfile.api"], ["mesh-local-hub-ui", "Dockerfile.ui"], ]) { const tag = `${name}:${version}`; logInfo(`Building ${tag} from source…`); execFileSync("docker", ["build", "-f", path.join(hubStackDir, dockerfile), "-t", tag, "--secret", `id=npmrc,src=${npmrc}`, context], { stdio: ["ignore", "inherit", "inherit"], env: { ...process.env, DOCKER_BUILDKIT: "1" } }); logSuccess(`Built ${tag}`); } return version; }