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