UNPKG

@mesh-tech/mesh-cli

Version:

CLI for Mesh platform development utilities

663 lines (662 loc) 29.3 kB
import { execFileSync } from "child_process"; import * as fs from "fs"; import * as path from "path"; import { logInfo, logSuccess, logWarn } from "../utils/log.js"; import { MeshCliError } from "../utils/errors.js"; import { awsConfigHasProfile, findUnscopedCodeArtifactRegistry, homeNpmrcPath, npmrcAuthKeyForEndpoint, probeRegistryToken, readAwsConfig, SSO_SESSION_SNIPPET, stripUnscopedCodeArtifactRegistry, upsertNpmrcLines, } from "../utils/auth-preflight.js"; import { fetchRegistryGrant, realGrantDeps, PUBLISHED_BROKER_TIMEOUT_MS, } from "../utils/registry-broker.js"; import { clearRegistrySession, describeSessionExpiry, ensureRegistrySession, identityChanged, logRegistryIdentity, readRegistrySession, refreshRegistrySession, resolveRegistryIdentity, } from "../utils/registry-identity.js"; import { readCredentials } from "./login.js"; import { registerRegistryPublish } from "./registry-publish.js"; const CA_DOMAIN = "mesh-platform"; const CA_REPOSITORY = "mesh-packages"; const CA_NAMESPACE = "@mesh-tech"; const CA_REGION = "us-east-2"; const DEFAULT_REGISTRY_ROLE = null; function getEndpoint(env) { try { const result = execFileSync("aws", [ "codeartifact", "get-repository-endpoint", "--domain", CA_DOMAIN, "--repository", CA_REPOSITORY, "--format", "npm", "--region", CA_REGION, "--output", "text", "--query", "repositoryEndpoint", ], { encoding: "utf-8", env: env ? { ...process.env, ...env } : undefined, stdio: ["pipe", "pipe", "pipe"], }); return result.trim() || null; } catch { return null; } } export function codeartifactLogin(env) { try { execFileSync("aws", [ "codeartifact", "login", "--tool", "npm", "--domain", CA_DOMAIN, "--repository", CA_REPOSITORY, "--namespace", CA_NAMESPACE, "--region", CA_REGION, ], { encoding: "utf-8", env: env ? { ...process.env, ...env } : undefined, stdio: ["pipe", "pipe", "pipe"], }); return true; } catch { return false; } } function parseTenantEnv(context) { const parts = context.split("."); if (parts.length !== 2 || !parts[0] || !parts[1]) return null; return { tenant: parts[0], env: parts[1] }; } async function getRegistrySsmExport(tenant, env) { try { const { SSMClient, GetParameterCommand } = await import("@aws-sdk/client-ssm"); const region = process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION ?? CA_REGION; const ssm = new SSMClient({ region }); const ssmPath = `/mesh-platform/${tenant}/${env}/registry`; const response = await ssm.send(new GetParameterCommand({ Name: ssmPath })); if (!response.Parameter?.Value) return null; return JSON.parse(response.Parameter.Value); } catch { return null; } } export function resolvePublisherRoleArn({ roleArnFlag, ssmArn, }) { const resolved = roleArnFlag || ssmArn; if (!resolved) { throw new Error("No publisher role: pass --role or ensure the registry stack exported publisherRoleArn."); } return resolved; } function assumeRole(roleArn) { try { const result = execFileSync("aws", [ "sts", "assume-role", "--role-arn", roleArn, "--role-session-name", "mesh-registry", "--output", "json", ], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }); const creds = JSON.parse(result).Credentials; return { AWS_ACCESS_KEY_ID: creds.AccessKeyId, AWS_SECRET_ACCESS_KEY: creds.SecretAccessKey, AWS_SESSION_TOKEN: creds.SessionToken, }; } catch { return null; } } function assumeRoleWithWebIdentity(roleArn, idToken, sessionName) { try { const result = execFileSync("aws", [ "sts", "assume-role-with-web-identity", "--role-arn", roleArn, "--web-identity-token", idToken, "--role-session-name", sessionName, "--output", "json", ], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }); const creds = JSON.parse(result).Credentials; return { AWS_ACCESS_KEY_ID: creds.AccessKeyId, AWS_SECRET_ACCESS_KEY: creds.SecretAccessKey, AWS_SESSION_TOKEN: creds.SessionToken, }; } catch { return null; } } export function resolveCredentials(context, roleArn) { if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SESSION_TOKEN) { logInfo("Using existing AWS credentials from environment"); return {}; } if (context) { const meshCreds = readCredentials(context); if (meshCreds && new Date(meshCreds.expiresAt) > new Date()) { const targetRole = roleArn || DEFAULT_REGISTRY_ROLE; if (targetRole) { const sessionName = (meshCreds.email ?? "mesh-registry") .replace(/[^a-zA-Z0-9=,.@-]/g, "_") .slice(0, 64); logInfo(`Assuming registry role via Zitadel (${meshCreds.email ?? context})`); const creds = assumeRoleWithWebIdentity(targetRole, meshCreds.idToken, sessionName); if (creds) { logSuccess("Assumed registry role via Zitadel"); return creds; } logWarn("Zitadel JWT role assumption failed — falling back to AWS profile"); } else { logInfo(`Zitadel auth available (${meshCreds.email}) but no registry role configured — using AWS profile`); } } else if (meshCreds) { logWarn(`Zitadel token expired for ${context}. Run: mesh login ${context}`); } else { logWarn(`No Zitadel credentials for ${context}. Run: mesh login ${context}`); } } if (roleArn) { logInfo(`Assuming ${roleArn.split("/").pop()} via AWS SSO`); const creds = assumeRole(roleArn); if (creds) { logSuccess("Assumed registry role via AWS SSO"); return creds; } logWarn("Role assumption failed — trying current credentials directly"); } logInfo("Using current AWS profile"); return {}; } function findProjectRoot() { let dir = process.cwd(); while (true) { if (fs.existsSync(path.join(dir, "pnpm-workspace.yaml")) || fs.existsSync(path.join(dir, "pnpm-lock.yaml")) || fs.existsSync(path.join(dir, "Pulumi.yaml")) || fs.existsSync(path.join(dir, ".npmrc"))) { return dir; } const parent = path.dirname(dir); if (parent === dir) return null; dir = parent; } } function ensureProjectNpmrc(endpoint) { const projectRoot = findProjectRoot() ?? process.cwd(); const npmrcPath = path.join(projectRoot, ".npmrc"); const scopeLine = `${CA_NAMESPACE}:registry=${endpoint}`; if (!fs.existsSync(npmrcPath)) { fs.writeFileSync(npmrcPath, scopeLine + "\n", "utf-8"); return { created: true, updated: false, path: npmrcPath }; } const content = fs.readFileSync(npmrcPath, "utf-8"); const scopeRegex = new RegExp(`^${CA_NAMESPACE.replace("/", "\\/")}:registry=.+$`, "m"); if (scopeRegex.test(content)) { if (content.includes(scopeLine)) { return { created: false, updated: false, path: npmrcPath }; } const updated = content.replace(scopeRegex, scopeLine); fs.writeFileSync(npmrcPath, updated, "utf-8"); return { created: false, updated: true, path: npmrcPath }; } const separator = content.endsWith("\n") ? "" : "\n"; fs.writeFileSync(npmrcPath, content + separator + scopeLine + "\n", "utf-8"); return { created: false, updated: true, path: npmrcPath }; } export function applyGrantToUserNpmrc(grant, npmrcPath = homeNpmrcPath()) { let existing = ""; try { existing = fs.readFileSync(npmrcPath, "utf-8"); } catch { } const content = upsertNpmrcLines(existing, [ { key: `${grant.scope}:registry`, value: grant.endpoint }, { key: `${npmrcAuthKeyForEndpoint(grant.endpoint)}:_authToken`, value: grant.authorizationToken }, ]); fs.mkdirSync(path.dirname(npmrcPath), { recursive: true }); fs.writeFileSync(npmrcPath, content, { mode: 0o600 }); try { fs.chmodSync(npmrcPath, 0o600); } catch { } return { path: npmrcPath }; } export function describeBrokerFailure(failure) { switch (failure.kind) { case "no-broker": return { message: "The registry publishes no broker — using the AWS paths.", fatal: false, }; case "no-session": return { message: "No registry session — using the AWS paths.", hint: "mesh registry login", fatal: false, }; case "unauthenticated": return { message: "The registry broker rejected your session (expired or wrong audience).", hint: "mesh registry logout && mesh registry login", fatal: false, }; case "not-authorized": return { message: `Your Mesh account${failure.email ? ` (${failure.email})` : ""} is not authorized to read ` + `${CA_NAMESPACE} packages.` + (failure.requiredRoles?.length ? `\n Needs one of these Zitadel roles: ${failure.requiredRoles.join(", ")}` : "") + (failure.requestUrl ? `\n Request access: ${failure.requestUrl}` : "\n Ask a Mesh platform admin to grant your account registry access.") + `\n ${NOT_AUTHORIZED_RERUN}`, fatal: true, }; case "unavailable": return { message: `Registry broker unavailable (${failure.detail}) — falling back to the AWS paths.`, fatal: false, }; } } export class RegistryNotAuthorizedError extends MeshCliError { constructor(message) { super(message); this.name = "RegistryNotAuthorizedError"; } } export function registrySignInAllowed(opts, hasTty = Boolean(process.stdin.isTTY && process.stdout.isTTY)) { return Boolean(opts.device) || hasTty; } export function shouldRemintAfter(failure) { return failure.kind === "not-authorized"; } export const NOT_AUTHORIZED_RERUN = "Once granted, run: mesh registry login (it re-mints your sign-in; still refused? mesh registry logout && mesh registry login)"; export const CONTEXT_IGNORED_NOTICE = "The registry is global — the context argument is no longer needed (ignored)."; async function tryBrokerLogin(opts) { if (opts.broker === false) return { outcome: { kind: "skipped-flag", flag: "--no-broker" } }; if (opts.ci) return { outcome: { kind: "skipped-flag", flag: "--ci" } }; let identity; try { identity = await resolveRegistryIdentity(); } catch (err) { const detail = err.message.split("\n")[0] ?? String(err); logWarn(`Registry broker unavailable (${detail}) — falling back to the AWS paths.`); return { outcome: { kind: "unavailable", detail } }; } logRegistryIdentity(identity); const session = await ensureRegistrySession({ device: opts.device, interactive: registrySignInAllowed(opts), }); if (!session.ok) { if (session.reason === "login-failed") { throw new MeshCliError(`Registry sign-in failed: ${session.detail}`, { remediation: { command: opts.device ? "mesh registry login --device" : "mesh registry login" }, }); } logWarn(`No registry session (${session.detail}) — falling back to the AWS paths.`); return { outcome: { kind: "no-session", detail: session.detail } }; } logInfo(`Registry session: ${session.email}`); let result = await requestGrant(session.context, identity); if (!result.ok && (result.failure.kind === "unauthenticated" || result.failure.kind === "unavailable")) { let fresh = null; try { fresh = await resolveRegistryIdentity({ refresh: true }); } catch { } if (fresh && identityChanged(identity, fresh)) { logInfo(`Registry identity changed since it was cached — retrying with ${fresh.registryBroker}`); identity = fresh; result = await requestGrant(session.context, identity); } } if (!result.ok && shouldRemintAfter(result.failure)) { const { rolesChanged } = await refreshRegistrySession(session.context); if (rolesChanged) { logInfo("Your sign-in now carries a role it did not have — retrying…"); result = await requestGrant(session.context, identity); } } if (result.ok) return { grant: result.grant }; const { message, hint, fatal } = describeBrokerFailure(result.failure); if (fatal) { throw new RegistryNotAuthorizedError(message); } logWarn(message); if (hint) logInfo(` Try: ${hint}`); const detail = result.failure.kind === "unavailable" ? result.failure.detail : result.failure.kind; return { outcome: { kind: "unavailable", detail } }; } async function requestGrant(sessionContext, identity) { logInfo(`Requesting a registry token from ${identity.registryBroker} (no AWS credentials needed)…`); return fetchRegistryGrant(sessionContext, identity.registryBroker, { ...realGrantDeps(), timeoutMs: PUBLISHED_BROKER_TIMEOUT_MS, }); } export function shouldRepairUserNpmrc(opts) { return !opts.ci; } export function repairUnscopedRegistry(npmrcPath = homeNpmrcPath()) { let original; try { original = fs.readFileSync(npmrcPath, "utf-8"); } catch { return { removed: null }; } const { content, removed } = stripUnscopedCodeArtifactRegistry(original); if (!removed) return { removed: null }; const backupPath = `${npmrcPath}.bak-${new Date().toISOString().replace(/[:.]/g, "-")}`; try { fs.writeFileSync(backupPath, original, "utf-8"); fs.writeFileSync(npmrcPath, content, "utf-8"); } catch (err) { return { removed: null, error: err instanceof Error ? err.message : String(err) }; } return { removed, backupPath }; } export function renderRegistrySessionLine(session, now = Date.now()) { if (!session) return "No registry session — run: mesh registry login"; return `Registry session: ${session.email} (expires in ${describeSessionExpiry(session.expiresAt, now)})`; } export async function checkExistingToken() { const npmrcPath = homeNpmrcPath(); if (!fs.existsSync(npmrcPath)) return { valid: false, state: "missing", reason: "no ~/.npmrc" }; const hijackLine = findUnscopedCodeArtifactRegistry(fs.readFileSync(npmrcPath, "utf-8")) ?? undefined; const probe = await probeRegistryToken({ npmrcPath }); if (probe.state === "missing") { return { valid: false, state: probe.state, reason: "no CodeArtifact token in ~/.npmrc", hijackLine }; } if (probe.state === "expired") { return { valid: false, state: probe.state, reason: probe.detail ?? "the CodeArtifact token is expired", hijackLine, }; } if (probe.state === "unreachable") { return { valid: false, state: probe.state, reason: `could not reach the registry to verify the token (${probe.detail ?? "unknown error"})`, hijackLine, }; } if (hijackLine) { return { valid: false, state: probe.state, reason: "unscoped CodeArtifact registry", hijackLine }; } return { valid: true, state: probe.state, endpoint: getEndpoint() ?? undefined }; } export async function runRegistryStatus(opts) { const { valid, state, endpoint, reason, hijackLine } = await (opts?.check ?? checkExistingToken)(); if (hijackLine) { logWarn("~/.npmrc sets CodeArtifact as your DEFAULT registry — every public npm install goes " + "through it and will 401 when the 12h token expires:"); logInfo(` ${hijackLine.trim()}`); logInfo("Fix: mesh registry login # removes it (backup taken) and keeps @mesh-tech scoped"); } const unverifiable = state === "unreachable" && !hijackLine; if (!valid && !unverifiable) { logWarn(`Registry auth is not usable${reason ? ` — ${reason}` : ""}. Run: mesh registry login`); return 1; } if (unverifiable) { logWarn(`Could not verify the CodeArtifact token in ~/.npmrc — ${reason}.`); logInfo("Nothing to fix from here; retry when the registry is reachable."); } else { logSuccess("CodeArtifact token in ~/.npmrc is valid"); if (endpoint) { logInfo(`Endpoint: ${endpoint}`); } } logInfo(renderRegistrySessionLine(opts?.session ?? readRegistrySession())); const projectRoot = findProjectRoot(); if (projectRoot) { const npmrcPath = path.join(projectRoot, ".npmrc"); if (fs.existsSync(npmrcPath)) { const content = fs.readFileSync(npmrcPath, "utf-8"); if (content.includes(`${CA_NAMESPACE}:registry=`)) { logSuccess(`Project .npmrc configured (${npmrcPath})`); } else { logWarn(`Project .npmrc exists but missing ${CA_NAMESPACE} scope (${npmrcPath})`); logInfo("Run: mesh registry login"); } } else { logWarn(`No .npmrc in project root (${projectRoot})`); logInfo("Run: mesh registry login"); } } return 0; } function ensureSsoProfile(profile) { const awsConfig = readAwsConfig(); if (awsConfig && !awsConfigHasProfile(awsConfig, profile)) { throw new MeshCliError(`AWS profile '${profile}' is not defined in ~/.aws/config — \`aws sso login --profile ${profile}\` cannot work until it exists.\n\n` + "If you only need to INSTALL @mesh-tech packages, drop --profile: `mesh registry login` uses the\n" + "Zitadel-gated broker and needs no AWS account at all.\n\n" + `If you DEPLOY and genuinely need this profile — one-time setup: \`aws configure sso --profile ${profile}\`,\n` + `or add the session + profile by hand:\n` + `${SSO_SESSION_SNIPPET}\n[profile ${profile}]\nsso_session = mesh\nsso_account_id = <account id>\nsso_role_name = <role>\nregion = us-east-2`, { remediation: { command: `aws configure sso --profile ${profile}` } }); } const probe = () => execFileSync("aws", ["sts", "get-caller-identity", "--profile", profile], { stdio: ["ignore", "pipe", "pipe"], timeout: 20_000, }); try { probe(); logInfo(`AWS SSO session for profile '${profile}' is live`); return; } catch { logInfo(`AWS SSO session for profile '${profile}' is stale — opening browser login…`); } execFileSync("aws", ["sso", "login", "--profile", profile], { stdio: "inherit" }); try { probe(); } catch (err) { throw new MeshCliError(`AWS SSO login for profile '${profile}' completed but the session still fails an identity check.`, { remediation: { command: `aws sts get-caller-identity --profile ${profile} # see the underlying error` }, cause: err, }); } logSuccess(`AWS SSO session refreshed (profile '${profile}')`); } export async function runRegistryLogin(context, opts) { let roleArn = opts.role; if (opts.publish) { let ssmArn; if (!opts.role) { if (!context) { throw new MeshCliError('`--publish` requires a platform context, e.g. "mesh registry login mesh.dev --publish".'); } const parsed = parseTenantEnv(context); if (!parsed) { throw new MeshCliError(`Could not parse tenant/env from context "${context}" (expected "<tenant>.<env>").`); } const registryExport = await getRegistrySsmExport(parsed.tenant, parsed.env); ssmArn = registryExport?.publisherRoleArn; } try { roleArn = resolvePublisherRoleArn({ roleArnFlag: opts.role, ssmArn }); } catch (err) { throw new MeshCliError(err.message); } logInfo(`Publisher role resolved: ${roleArn}`); } let outcome; if (opts.publish) { outcome = { kind: "skipped-flag", flag: "--publish" }; } else if (opts.profile) { outcome = { kind: "skipped-profile", profile: opts.profile }; } else { if (context && opts.broker !== false && !opts.ci) logInfo(CONTEXT_IGNORED_NOTICE); const attempt = await tryBrokerLogin(opts); if ("grant" in attempt) { const { path: npmrcPath } = applyGrantToUserNpmrc(attempt.grant); logSuccess(`Registry token issued by the Mesh broker → ${npmrcPath}` + (attempt.grant.expiresAt ? ` (expires ${attempt.grant.expiresAt})` : "")); if (shouldRepairUserNpmrc(opts)) { const repair = repairUnscopedRegistry(); if (repair.removed) { logWarn("Removed an unscoped CodeArtifact `registry=` from ~/.npmrc — it routed ALL npm traffic " + "(public packages included) through CodeArtifact."); logInfo(` removed: ${repair.removed.trim()}`); if (repair.backupPath) logInfo(` backup: ${repair.backupPath}`); } } if (!opts.ci && !opts.skipNpmrc) { const result = ensureProjectNpmrc(attempt.grant.endpoint); if (result.created) { logSuccess(`Created ${result.path} with ${CA_NAMESPACE} registry scope`); } else if (result.updated) { logSuccess(`Updated ${CA_NAMESPACE} registry in ${result.path}`); } else { logInfo(`${result.path} already configured`); } } logSuccess(`Ready — run pnpm install to fetch ${CA_NAMESPACE} packages`); return; } outcome = attempt.outcome; } let env; if (opts.profile) { ensureSsoProfile(opts.profile); env = { ...process.env, AWS_PROFILE: opts.profile }; for (const key of ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"]) { delete env[key]; } } else { const awsCreds = resolveCredentials(context, roleArn); env = Object.keys(awsCreds).length > 0 ? { ...process.env, ...awsCreds } : undefined; } logInfo("Authenticating with CodeArtifact..."); if (!codeartifactLogin(env)) { throw new MeshCliError(describeAwsChainFailure(outcome)); } logSuccess("CodeArtifact token refreshed (valid for 12 hours)"); if (shouldRepairUserNpmrc(opts)) { const repair = repairUnscopedRegistry(); if (repair.removed) { logWarn("Removed an unscoped CodeArtifact `registry=` from ~/.npmrc — it routed ALL npm traffic " + "(public packages included) through CodeArtifact, so every install broke when the 12h token expired. " + `${CA_NAMESPACE} stays scoped; public packages now go to npmjs.`); logInfo(` removed: ${repair.removed.trim()}`); if (repair.backupPath) logInfo(` backup: ${repair.backupPath}`); } else if (repair.error) { logWarn(`Could not repair ~/.npmrc (${repair.error}) — remove any unscoped \`registry=\` line by hand.`); } } const endpoint = getEndpoint(env); if (!endpoint) { logWarn("Could not retrieve registry endpoint — .npmrc scope not configured"); logInfo("You may need to manually add to .npmrc:"); logInfo(` ${CA_NAMESPACE}:registry=https://mesh-platform-<ACCOUNT>.d.codeartifact.${CA_REGION}.amazonaws.com/npm/${CA_REPOSITORY}/`); return; } if (!opts.ci && !opts.skipNpmrc) { const result = ensureProjectNpmrc(endpoint); if (result.created) { logSuccess(`Created ${result.path} with ${CA_NAMESPACE} registry scope`); } else if (result.updated) { logSuccess(`Updated ${CA_NAMESPACE} registry in ${result.path}`); } else { logInfo(`${result.path} already configured`); } } logSuccess(`Ready — run pnpm install to fetch ${CA_NAMESPACE} packages`); } export function describeAwsChainFailure(outcome) { switch (outcome.kind) { case "skipped-profile": return (`AWS profile '${outcome.profile}' cannot read the Mesh registry (aws codeartifact login failed).\n\n` + "--profile is for people who DEPLOY with an AWS account that is allowed to read the registry\n" + "(Trabian's mesh-dev profile, or a tenant deployer role). Any other profile fails here.\n\n" + "To install packages you do not need AWS at all — drop the flag:\n" + " mesh registry login"); case "unavailable": return (`Registry broker unavailable (${outcome.detail}) and this machine has no AWS path either.\n\n` + "Retry in a minute: mesh registry login\n" + "If you deploy and have an AWS account: mesh registry login --profile mesh-dev"); case "no-session": return (`No registry session on this machine (${outcome.detail}) and the AWS chain failed too.\n\n` + "Sign in once — on a headless box: mesh registry login --device\n" + "In CI with AWS creds: mesh registry login --ci"); case "skipped-flag": return (`CodeArtifact login failed with the AWS credential chain (${outcome.flag} skips the registry broker).\n\n` + "This path needs AWS credentials that can read the registry — ambient keys, a live SSO\n" + "session, or --profile <deployer-profile>.\n\n" + "To install packages you do not need AWS at all — drop the flag:\n" + " mesh registry login"); } } export function registerRegistryCommands(program) { const registry = program .command("registry") .description("Manage npm registry auth for @mesh-tech packages"); registry .command("login") .description("Sign in to the Mesh package registry (browser, or device code with --device) and configure ~/.npmrc + the project .npmrc. Needs no platform context and no AWS account.") .argument("[context]", 'Platform context — only meaningful with --publish (publisher role from that context\'s SSM export) or on the explicit AWS chain; ignored on the default read path, which is global') .option("--device", "Sign in with the device-code flow (headless box, SSH, no browser)") .option("--role <arn>", "IAM role ARN for registry access") .option("--ci", "CI mode — AWS chain only (ambient credentials), refresh token only, skip project .npmrc setup") .option("--skip-npmrc", "Skip project .npmrc setup") .option("--profile <name>", "AWS SSO profile of an account allowed to READ the registry (deployers only — e.g. mesh-dev); skips the broker, auto-runs `aws sso login` when the session is stale") .option("--no-broker", "Skip the Zitadel-gated registry broker and go straight to the AWS credential chain") .option("--publish", "Assume the registry's publisher role for CodeArtifact publish access " + "(opt-in privileged path — resolves publisherRoleArn from the registry's SSM export unless --role is given)") .action(async (context, opts) => { await runRegistryLogin(context, opts); }); registry .command("logout") .description("Forget the package-registry session (platform logins under `mesh login` are untouched)") .action(() => { clearRegistrySession(); logSuccess("Registry session cleared. ~/.npmrc is untouched — its token expires on its own within 12 hours."); }); registry .command("status") .description("Check current registry authentication status — the ~/.npmrc token and the registry session") .action(async () => { const code = await runRegistryStatus(); if (code !== 0) process.exit(code); }); registerRegistryPublish(registry); }