@mesh-tech/mesh-cli
Version:
CLI for Mesh platform development utilities
283 lines (282 loc) • 12.8 kB
JavaScript
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;
}