@mesh-tech/mesh-cli
Version:
CLI for Mesh platform development utilities
404 lines (403 loc) • 18.3 kB
JavaScript
import * as fs from "fs";
import * as path from "path";
import { execFileSync } from "child_process";
import chalk from "chalk";
import { logInfo, logWarn } from "../../utils/log.js";
import { MeshCliError, emitJsonPayload } from "../../utils/errors.js";
import { probeRegistryToken } from "../../utils/auth-preflight.js";
import { RegistryNotAuthorizedError } from "../registry.js";
import { readRegistrySession } from "../../utils/registry-identity.js";
import { findMeshJson, isValidTenantName, writeMeshJson } from "../../utils/mesh-json.js";
import { discoverConfigForContext, isRemoteEnvironment, readCredentials, renderNoConfigHelp, runLoginFlow, tokenStillValid, } from "../login.js";
import { bootstrapAppsRepo, isInsidePlatformMonorepo, shouldBootstrapAppsRepo } from "../create-app.js";
import { resolveTargetRoot, syncSkills } from "../skills.js";
import { detectLocalTenant } from "../local/dev-local.js";
export const NO_SESSION_NO_TTY_MESSAGE = "No registry session and no terminal to sign in with.\n" +
" On a headless box: mesh registry login --device (one-time, prints a code)\n" +
" In CI with AWS creds: mesh registry login --ci";
export const TENANT_REQUIRED_MESSAGE = "--tenant is required (no TTY for interactive mode)";
export const INTERRUPTED_EXIT_CODE = 130;
const STEP_LABELS = {
registry: "Registry access",
platform: "Platform sign-in",
repo: "Repo",
skills: "Agent skills",
};
export function wizardExitCode(steps) {
return steps.some((s) => s.status === "fail") ? 1 : 0;
}
export function wizardNextCommands(mode, steps = []) {
const repo = steps.find((s) => s.name === STEP_LABELS.repo);
const first = repo && repo.status !== "pass" && repo.fix ? repo.fix : "mesh create-app";
return mode === "local" ? [first] : [first, "mesh deploy up # from the app directory, once it exists"];
}
export function stepBlocks(result) {
return result.status === "fail" && result.blocking !== false;
}
export function describeMode(mode) {
return mode === "local" ? "local only" : `deployed platform (${mode.platform})`;
}
export function renderWizardSummary(outcome, opts = {}) {
const paint = opts.color === false ? { green: (s) => s, red: (s) => s, yellow: (s) => s, dim: (s) => s, cyan: (s) => s } : chalk;
const icon = (status) => status === "pass" ? paint.green("✔") : status === "fail" ? paint.red("✘") : status === "warn" ? paint.yellow("▲") : paint.dim("–");
const lines = [`Setup summary — tenant ${outcome.tenant} (${describeMode(outcome.mode)})`];
for (const step of outcome.steps) {
lines.push(` ${icon(step.status)} ${step.name.padEnd(18)} ${step.detail}`);
if (step.fix && step.status !== "pass")
lines.push(` ${paint.dim("→ run:")} ${paint.cyan(step.fix)}`);
}
lines.push("", "Next:");
for (const cmd of outcome.next)
lines.push(` ${cmd}`);
return lines.join("\n");
}
export function resolveAnswers(opts, recorded, interactive) {
const open = [];
let tenant = opts.tenant;
if (tenant !== undefined && !isValidTenantName(tenant)) {
throw new MeshCliError(`Invalid tenant name '${tenant}' (lowercase alphanumeric + dashes, starting with a letter).`);
}
if (!tenant && (opts.yes || !interactive) && recorded)
tenant = recorded.tenant;
if (!tenant) {
if (!interactive)
throw new MeshCliError(TENANT_REQUIRED_MESSAGE);
open.push("tenant");
}
let mode;
if (opts.local && opts.platform) {
throw new MeshCliError("--local and --platform are mutually exclusive.");
}
if (opts.platform)
mode = { platform: opts.platform };
else if (opts.local)
mode = "local";
else if (opts.yes || !interactive) {
mode = recorded && recorded.platform !== "local" ? { platform: recorded.platform } : "local";
}
else {
open.push("mode");
}
return { tenant, mode, open };
}
export const registryStep = {
name: "registry",
async run(ctx) {
const name = STEP_LABELS.registry;
const probe = await (ctx.seams?.probe ?? probeRegistryToken)();
if (probe.state === "fresh") {
const session = readRegistrySession();
return { name, status: "pass", detail: `Registry access OK (${session ? `signed in as ${session.email}` : "token valid"})` };
}
if (probe.state === "unreachable") {
return {
name,
status: "warn",
detail: `could not reach the registry to verify access (${probe.detail ?? "unknown error"})`,
fix: "mesh registry login",
};
}
if (!ctx.interactive && !ctx.device && !ctx.profile) {
logWarn(NO_SESSION_NO_TTY_MESSAGE);
return { name, status: "fail", detail: NO_SESSION_NO_TTY_MESSAGE.split("\n")[0], fix: "mesh registry login --device", blocking: true };
}
try {
const login = ctx.seams?.runRegistryLogin ??
(async (opts) => {
const { runRegistryLogin } = await import("../registry.js");
await runRegistryLogin(undefined, opts);
});
await login({ device: ctx.device, profile: ctx.profile });
}
catch (err) {
const message = err instanceof Error ? err.message : String(err);
if (err instanceof RegistryNotAuthorizedError) {
return {
name,
status: "fail",
detail: `not authorized — ${message.split("\n")[0]}`,
fix: "ask a Mesh platform admin for the registry:read role, then: mesh registry login",
blocking: false,
};
}
return {
name,
status: "fail",
detail: message.split("\n")[0],
fix: ctx.device || ctx.remote ? "mesh registry login --device" : "mesh registry login",
blocking: true,
};
}
const session = readRegistrySession();
return { name, status: "pass", detail: `Registry access OK (${session ? `signed in as ${session.email}` : "token written"})` };
},
};
const platformStep = {
name: "platform",
async run(ctx) {
const name = STEP_LABELS.platform;
if (!ctx.context)
return { name, status: "skip", detail: "local only — no platform to sign in to" };
const context = ctx.context;
const cached = readCredentials(context);
if (cached && tokenStillValid(cached.expiresAt)) {
return { name, status: "pass", detail: `signed in to ${context}${cached.email ? ` as ${cached.email}` : ""}` };
}
const config = await discoverConfigForContext(context);
if (!config) {
logWarn(renderNoConfigHelp(context));
return { name, status: "fail", detail: `no configuration found for ${context} (see above)`, fix: `mesh login ${context}` };
}
if (!ctx.interactive && !ctx.device) {
return { name, status: "fail", detail: `no session for ${context} and no terminal to sign in with`, fix: `mesh login ${context} --device` };
}
try {
await runLoginFlow(context, config, { device: ctx.device });
}
catch (err) {
return { name, status: "fail", detail: `sign-in failed: ${err.message}`, fix: `mesh login ${context}${ctx.device ? " --device" : ""}` };
}
const creds = readCredentials(context);
return {
name,
status: "pass",
detail: `signed in to ${context}${creds?.email ? ` as ${creds.email}` : ""} — tenant registration: mesh init app-tenant --context ${context} --hub-url <hub api url>`,
};
},
};
export function classifyRepo(cwd) {
if (isInsidePlatformMonorepo(cwd))
return { kind: "platform-monorepo", root: resolveTargetRoot(cwd) };
if (shouldBootstrapAppsRepo(cwd))
return { kind: "empty-apps-repo", root: cwd };
const root = resolveTargetRoot(cwd);
const inRepo = fs.existsSync(path.join(root, ".git"));
if (!inRepo) {
const hasContent = fs.readdirSync(cwd).some((entry) => !entry.startsWith("."));
return { kind: hasContent ? "no-repo" : "empty-dir", root: cwd };
}
const hasWorkspace = fs.existsSync(path.join(root, "pnpm-workspace.yaml")) || fs.existsSync(path.join(root, "pnpm-lock.yaml"));
const hasApps = fs.existsSync(path.join(root, "apps")) || fs.existsSync(path.join(root, "tenants"));
if (hasWorkspace || hasApps)
return { kind: "apps-repo", root };
return { kind: "other-repo", root };
}
function gitInit(dir) {
execFileSync("git", ["init", "-q"], { cwd: dir, stdio: ["ignore", "ignore", "pipe"] });
}
export const INIT_REPO_FIX = "git init && mesh init";
export const repoStep = {
name: "repo",
async run(ctx) {
const name = STEP_LABELS.repo;
if (ctx.skipRepo)
return { name, status: "skip", detail: "--skip-repo" };
const { kind, root } = classifyRepo(ctx.cwd);
switch (kind) {
case "platform-monorepo":
return { name, status: "skip", detail: "inside mesh-platform — apps here link workspace packages; nothing to set up" };
case "empty-dir": {
const go = ctx.yes || !ctx.interactive
? true
: await ctx.prompts.confirm({
message: `This folder is not a git repo — initialize ${root} as your ${ctx.tenant}-mesh-apps repo here?`,
default: true,
});
if (!go) {
return { name, status: "skip", detail: "folder left as is", fix: `cd <your ${ctx.tenant}-mesh-apps clone> && mesh init` };
}
try {
(ctx.seams?.gitInit ?? gitInit)(root);
}
catch (err) {
const why = err instanceof Error && "stderr" in err && err.stderr ? String(err.stderr).trim() : "";
return {
name,
status: "fail",
detail: `git init failed — is git installed and ${root} writable?${why ? ` (${why})` : ""}`,
fix: INIT_REPO_FIX,
};
}
const created = bootstrapAppsRepo(root, ctx.tenant);
return { name, status: "pass", detail: `initialized a git repo and bootstrapped the apps repo: ${created.join(", ")}` };
}
case "no-repo":
return {
name,
status: "skip",
detail: `not in a git repo, and this folder already holds files — clone (or git init) your ${ctx.tenant}-mesh-apps repo and re-run mesh init there`,
fix: INIT_REPO_FIX,
};
case "other-repo":
return {
name,
status: "warn",
detail: "this repo is not a Mesh apps repo (no pnpm workspace, no apps/)",
fix: "mesh create-app # scaffolds an app and the workspace files around it",
};
case "empty-apps-repo": {
const go = ctx.yes || !ctx.interactive
? true
: await ctx.prompts.confirm({ message: `Empty repo — bootstrap the ${ctx.tenant}-mesh-apps layout here?`, default: true });
if (!go)
return { name, status: "skip", detail: "empty repo left as is", fix: "mesh create-app" };
const created = bootstrapAppsRepo(root, ctx.tenant);
return { name, status: "pass", detail: `bootstrapped apps repo: ${created.join(", ")}` };
}
case "apps-repo": {
const missing = [];
if (!fs.existsSync(path.join(root, "package.json")))
missing.push("package.json");
if (!fs.existsSync(path.join(root, "pnpm-workspace.yaml")) && !fs.existsSync(path.join(root, "pnpm-lock.yaml"))) {
missing.push("pnpm-workspace.yaml");
}
if (missing.length) {
return { name, status: "warn", detail: `apps repo missing ${missing.join(", ")}`, fix: "mesh create-app # writes the workspace files" };
}
return { name, status: "pass", detail: `apps repo at ${root}` };
}
}
},
};
const skillsStep = {
name: "skills",
async run(ctx) {
const name = STEP_LABELS.skills;
if (ctx.skipRepo)
return { name, status: "skip", detail: "--skip-repo" };
const { kind, root } = classifyRepo(ctx.cwd);
if (kind !== "apps-repo" && kind !== "empty-apps-repo") {
return { name, status: "skip", detail: "no apps repo to install skills into" };
}
try {
const complete = syncSkills(root);
return complete
? { name, status: "pass", detail: "base skills + Intent discovery in sync" }
: { name, status: "pass", detail: "base skills synced — after pnpm install, run: mesh skills sync (platform skills)" };
}
catch (err) {
return { name, status: "fail", detail: `skills sync failed: ${err.message}`, fix: "mesh skills sync", blocking: false };
}
},
};
export const WIZARD_STEPS = [registryStep, platformStep, repoStep, skillsStep];
export async function runWizardSteps(ctx, steps) {
const results = [];
for (const step of steps) {
const result = await step.run(ctx);
results.push(result);
if (stepBlocks(result))
break;
}
return results;
}
async function inquirerPrompts() {
const [{ default: input }, { default: select }, { default: confirm }] = await Promise.all([
import("@inquirer/input"),
import("@inquirer/select"),
import("@inquirer/confirm"),
]);
return {
input: (args) => input(args),
select: (args) => select(args),
confirm: (args) => confirm(args),
};
}
function isInterrupt(err) {
return err instanceof Error && err.name === "ExitPromptError";
}
export async function runInitWizard(opts, deps = {}) {
const cwd = deps.cwd ?? process.cwd();
const interactive = deps.interactive ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
const remote = deps.remote ?? isRemoteEnvironment();
const recorded = findMeshJson(cwd)?.data ?? null;
const origLog = console.log;
if (opts.json)
console.log = (...a) => console.error(...a);
try {
const answers = resolveAnswers(opts, recorded, interactive);
const prompts = deps.prompts ?? (answers.open.length ? await inquirerPrompts() : {});
let tenant = answers.tenant;
if (!tenant) {
const detected = recorded?.tenant ?? (() => {
try {
const t = detectLocalTenant(cwd);
return t === "local" ? undefined : t;
}
catch {
return undefined;
}
})();
tenant = await prompts.input({
message: "Tenant name",
default: detected,
validate: (v) => (isValidTenantName(v) ? true : "lowercase letters, digits and dashes, starting with a letter"),
});
}
let mode = answers.mode;
if (!mode) {
const choice = await prompts.select({
message: "Where does this tenant run?",
choices: [
{ name: "Local only (mesh start / mesh dev — no deployed platform yet)", value: "local" },
{ name: `A deployed Mesh platform (I have a ${tenant}.<env> context)`, value: "deployed" },
],
default: recorded && recorded.platform !== "local" ? "deployed" : "local",
});
if (choice === "local")
mode = "local";
else {
const env = await prompts.input({
message: "Environment",
default: recorded && recorded.platform !== "local" ? recorded.platform : "dev",
validate: (v) => (/^[a-z][a-z0-9-]*$/.test(v) ? true : "lowercase letters, digits and dashes"),
});
mode = { platform: env };
}
}
const ctx = {
cwd,
tenant,
mode,
context: mode === "local" ? null : `${tenant}.${mode.platform}`,
device: opts.device,
profile: opts.profile,
interactive,
remote,
yes: Boolean(opts.yes),
skipRepo: Boolean(opts.skipRepo),
prompts,
seams: deps.seams,
};
logInfo(`Setting up tenant '${tenant}' (${describeMode(mode)})…`);
const steps = await runWizardSteps(ctx, deps.steps ?? WIZARD_STEPS);
const { kind, root } = classifyRepo(cwd);
if (!opts.skipRepo && (kind === "apps-repo" || kind === "empty-apps-repo")) {
const written = writeMeshJson(root, { tenant, platform: mode === "local" ? "local" : mode.platform });
logInfo(`Recorded tenant + mode in ${written}`);
}
const outcome = { tenant, mode, steps, next: wizardNextCommands(mode, steps) };
const code = wizardExitCode(steps);
if (opts.json) {
console.log = origLog;
emitJsonPayload({ ok: code === 0, ...outcome });
}
else {
console.log("");
console.log(renderWizardSummary(outcome));
console.log("");
}
return code;
}
catch (err) {
if (isInterrupt(err)) {
console.error("");
logWarn("Interrupted — nothing was written.");
return INTERRUPTED_EXIT_CODE;
}
throw err;
}
finally {
console.log = origLog;
}
}