@mesh-tech/mesh-cli
Version:
CLI for Mesh platform development utilities
244 lines (243 loc) • 10.5 kB
JavaScript
import { execFileSync } from "node:child_process";
import { appendFileSync, existsSync, writeFileSync } from "node:fs";
import path from "node:path";
import { logError, logInfo, logSuccess } from "../utils/log.js";
import { resolveCliRuntime } from "../utils/build-info.js";
import { discoverDocRoots } from "./discover.js";
import { runAssemble, reportAssembly, DOCS_APP_DIR } from "./portal.js";
import { serveDocsSite } from "./serve.js";
import { DOCS_PACKAGE, fetchDocsArtifact, listDocsVersions, readDocsRegistryAuth, } from "./registry-docs.js";
export const DOCS_TMUX_SESSION = "mesh-docs";
function registryAuthOrThrow() {
const auth = readDocsRegistryAuth();
if (!auth) {
throw new Error("no CodeArtifact token in ~/.npmrc — run `mesh registry login` first " +
"(the docs artifact is role-gated the same way the packages are).");
}
return auth;
}
export async function runDocsList() {
const auth = registryAuthOrThrow();
const { versions, latest } = await listDocsVersions(auth);
if (versions.length === 0) {
logInfo(`No ${DOCS_PACKAGE} versions published yet.`);
return;
}
logInfo(`Published docs versions (docs version == @mesh-tech/* baseline):`);
for (const version of [...versions].reverse()) {
const marker = version === latest ? " ← latest" : "";
process.stdout.write(` ${version}${marker}\n`);
}
}
export function tmuxInstallHint(platform = process.platform) {
if (platform === "darwin")
return "brew install tmux";
if (platform === "linux")
return "sudo apt-get install tmux # or your distro's package manager";
return "install tmux via your package manager";
}
export function tmuxAvailable() {
try {
execFileSync("tmux", ["-V"], { stdio: ["pipe", "pipe", "pipe"] });
return true;
}
catch {
return false;
}
}
export function shouldDetach(args) {
return !args.foreground && !args.dev && args.isTTY;
}
export function docsSessionExists() {
try {
execFileSync("tmux", ["has-session", "-t", DOCS_TMUX_SESSION], { stdio: ["pipe", "pipe", "pipe"] });
return true;
}
catch {
return false;
}
}
export function tmuxServeArgs(args) {
const serveArgs = ["docs", "serve-static", "--root", args.serveRoot, "--port", String(args.port)];
if (args.mode === "dist") {
return { command: ["mesh", ...serveArgs], cwd: args.repoRoot };
}
return { command: ["pnpm", "exec", "mesh", ...serveArgs], cwd: args.repoRoot };
}
export async function startDetached(args) {
if (!tmuxAvailable()) {
throw new Error(`tmux is not installed — the detached docs server runs in a tmux session named "${DOCS_TMUX_SESSION}".\n` +
`Install it with: ${tmuxInstallHint()}\n` +
`Or run in the foreground instead: mesh docs start --foreground`);
}
if (docsSessionExists()) {
logInfo(`Replacing the docs server already running in tmux session "${DOCS_TMUX_SESSION}".`);
try {
execFileSync("tmux", ["kill-session", "-t", DOCS_TMUX_SESSION], { stdio: ["pipe", "pipe", "pipe"] });
}
catch {
}
}
const { command, cwd } = tmuxServeArgs(args);
execFileSync("tmux", ["new-session", "-d", "-s", DOCS_TMUX_SESSION, "-c", cwd, "--", ...command], { stdio: ["pipe", "pipe", "pipe"] });
const deadline = Date.now() + 60_000;
const url = `http://127.0.0.1:${args.port}`;
let lastError = "";
while (Date.now() < deadline) {
try {
const response = await fetch(`${url}/healthz`);
if (response.ok) {
printReady(url, args.label);
return;
}
lastError = `HTTP ${response.status}`;
}
catch (error) {
lastError = error instanceof Error ? error.message : String(error);
}
await new Promise((resolve) => setTimeout(resolve, 500));
}
throw new Error(`the docs server did not answer at ${url} within 60s (${lastError || "no response"}).\n` +
`Read its log with: tmux attach -t ${DOCS_TMUX_SESSION} (detach: Ctrl-B then D)`);
}
function printReady(url, label) {
logSuccess(`Mesh docs (${label}) are live:`);
process.stdout.write(`\n ${url}\n\n`);
logInfo(`Running detached in the tmux session "${DOCS_TMUX_SESSION}".`);
logInfo(` attach: tmux attach -t ${DOCS_TMUX_SESSION}`);
logInfo(` stop: mesh docs stop`);
}
export function runDocsStop() {
try {
execFileSync("tmux", ["kill-session", "-t", DOCS_TMUX_SESSION], { stdio: ["pipe", "pipe", "pipe"] });
logSuccess(`Stopped the docs server (tmux session "${DOCS_TMUX_SESSION}").`);
}
catch {
logInfo(`No docs server is running (no tmux session named "${DOCS_TMUX_SESSION}").`);
}
}
export async function runDocsServeStatic(args) {
const requestedPort = Number.parseInt(args.port, 10);
if (!Number.isInteger(requestedPort) || requestedPort < 0 || requestedPort > 65535) {
throw new Error(`Invalid --port: ${args.port}`);
}
if (!args.root || !existsSync(path.join(args.root, "index.html"))) {
throw new Error(`--root ${args.root || "(empty)"} does not hold a built site (no index.html). ` +
`This command exists so the detached tmux session can re-enter; you probably want \`mesh docs start\`.`);
}
const server = await serveDocsSite({ root: args.root, port: requestedPort });
logSuccess(`Mesh docs serving at ${server.url} (from ${args.root})`);
await new Promise((resolve) => {
const stop = () => void server.close().finally(() => resolve());
process.once("SIGINT", stop);
process.once("SIGTERM", stop);
});
}
export async function runDocsStart(args) {
const requestedPort = Number.parseInt(args.port, 10);
if (!Number.isInteger(requestedPort) || requestedPort < 0 || requestedPort > 65535) {
throw new Error(`Invalid --port: ${args.port}`);
}
const detached = shouldDetach({
foreground: args.foreground,
dev: args.dev,
isTTY: process.stdout.isTTY === true,
});
if (detached && requestedPort === 0) {
throw new Error("--port 0 (pick a free port) needs a foreground server, because the detached session " +
"cannot report the port it chose back to this process. Use `mesh docs start --foreground --port 0`, " +
"or pass an explicit port to run detached.");
}
const localRoots = discoverDocRoots(args.repoRoot);
const repoHasDocs = localRoots.roots.length > 0;
const registryMode = args.version !== undefined || !repoHasDocs;
let serveRoot;
let label;
if (registryMode) {
const auth = registryAuthOrThrow();
const version = args.version === undefined || args.version === "latest"
? (await listDocsVersions(auth)).latest
: args.version;
if (!version) {
throw new Error(`the registry has no ${DOCS_PACKAGE} versions tagged latest`);
}
logInfo(`Fetching ${DOCS_PACKAGE}@${version} from the registry…`);
const artifactDir = await fetchDocsArtifact(auth, version);
serveRoot = path.join(artifactDir, "dist");
label = `@mesh-tech/* ${version} (published artifact)`;
}
else {
if (localRoots.errors.length > 0) {
for (const error of localRoots.errors)
logError(error);
throw new Error(`docs discovery failed with ${localRoots.errors.length} problem(s)`);
}
const appDir = path.join(args.repoRoot, DOCS_APP_DIR);
if (args.dev) {
const result = runAssemble({
repoRoot: args.repoRoot,
outDir: path.join(appDir, "content"),
configOut: path.join(appDir, "zudoku.config.ts"),
});
reportAssembly(result);
const { spawn } = await import("node:child_process");
const child = spawn("pnpm", ["exec", "zudoku", "dev", "--port", String(requestedPort)], {
cwd: appDir,
stdio: "inherit",
env: process.env,
});
await new Promise((resolve) => {
child.on("close", () => resolve());
process.once("SIGINT", () => child.kill("SIGINT"));
process.once("SIGTERM", () => child.kill("SIGTERM"));
});
return;
}
const buildLog = path.join(args.repoRoot, DOCS_APP_DIR, "build.log");
logInfo(`Preparing the docs from this checkout (assemble + build — log: ${path.relative(args.repoRoot, buildLog)})…`);
const result = runAssemble({
repoRoot: args.repoRoot,
outDir: path.join(appDir, "content"),
configOut: path.join(appDir, "zudoku.config.ts"),
});
reportAssembly(result);
const { spawn } = await import("node:child_process");
writeFileSync(buildLog, "");
const code = await new Promise((resolve) => {
const child = spawn("pnpm", ["exec", "zudoku", "build"], {
cwd: appDir,
stdio: ["ignore", "pipe", "pipe"],
env: process.env,
});
child.stdout?.on("data", (chunk) => appendFileSync(buildLog, chunk));
child.stderr?.on("data", (chunk) => appendFileSync(buildLog, chunk));
child.on("error", () => resolve(1));
child.on("close", (exitCode) => resolve(exitCode ?? 1));
});
if (code !== 0)
throw new Error(`zudoku build exited with code ${code} — see ${buildLog}`);
serveRoot = path.join(appDir, "dist");
label = "this checkout";
}
if (detached) {
const runtime = resolveCliRuntime({ vcs: false });
await startDetached({
mode: runtime.mode,
repoRoot: args.repoRoot,
serveRoot,
port: requestedPort,
label,
});
return;
}
const server = await serveDocsSite({ root: serveRoot, port: requestedPort });
logSuccess(`Mesh docs (${label}) serving at ${server.url}`);
logInfo("Press Ctrl-C to stop");
await new Promise((resolve) => {
const stop = () => {
void server.close().finally(() => resolve());
};
process.once("SIGINT", stop);
process.once("SIGTERM", stop);
});
}