UNPKG

@mesh-tech/mesh-cli

Version:

CLI for Mesh platform development utilities

177 lines (176 loc) 6.51 kB
import * as fs from "fs"; import * as os from "os"; import * as path from "path"; export const REGISTRY_LOGIN_FIX = "mesh registry login"; export const REGISTRY_LOGIN_FIX_AWS = "mesh registry login --profile mesh-dev"; export function registryLoginFix() { return REGISTRY_LOGIN_FIX; } export const SSO_LOGIN_FIX = "aws sso login --sso-session=mesh # or: pnpm sso"; export const SSO_SESSION_SNIPPET = `[sso-session mesh] sso_start_url = https://trabian.awsapps.com/start sso_region = us-east-1 sso_registration_scopes = sso:account:access`; export function homeNpmrcPath() { return path.join(os.homedir(), ".npmrc"); } export function parseNpmrcRegistryAuth(content) { for (const line of content.split("\n")) { const match = line.trim().match(/^\/\/([^:]*\.codeartifact\.[^:]*):_authToken=(.+)$/); if (match && match[1] && match[2]) { const endpoint = `https://${match[1]}${match[1].endsWith("/") ? "" : "/"}`; return { endpoint, token: match[2].trim() }; } } return null; } export function findUnscopedCodeArtifactRegistry(content) { for (const line of content.split("\n")) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith(";") || trimmed.startsWith("#")) continue; const match = trimmed.match(/^registry\s*=\s*(\S+)$/); if (!match || !match[1]) continue; if (isCodeArtifactRegistryUrl(match[1])) return line; } return null; } function isCodeArtifactRegistryUrl(value) { try { return new URL(value).hostname.includes(".codeartifact."); } catch { return value.includes(".codeartifact."); } } export function stripUnscopedCodeArtifactRegistry(content) { const offender = findUnscopedCodeArtifactRegistry(content); if (offender === null) return { content, removed: null }; const kept = content .split("\n") .filter((line) => findUnscopedCodeArtifactRegistry(line) === null); return { content: kept.join("\n"), removed: offender }; } export function npmrcAuthKeyForEndpoint(endpoint) { const withoutScheme = endpoint.replace(/^https?:\/\//, ""); return `//${withoutScheme.endsWith("/") ? withoutScheme : `${withoutScheme}/`}`; } export function upsertNpmrcLines(content, entries) { let lines = content.length ? content.split("\n") : []; const trailingNewline = content.endsWith("\n"); if (trailingNewline) lines = lines.slice(0, -1); for (const { key, value } of entries) { const line = `${key}=${value}`; const idx = lines.findIndex((l) => { const trimmed = l.trim(); if (!trimmed || trimmed.startsWith(";") || trimmed.startsWith("#")) return false; const eq = trimmed.indexOf("="); return eq > 0 && trimmed.slice(0, eq).trim() === key; }); if (idx >= 0) lines[idx] = line; else lines.push(line); } return lines.join("\n") + "\n"; } export function classifyRegistryStatus(status) { return status === 401 || status === 403 ? "expired" : "fresh"; } export async function probeRegistryToken(opts) { const npmrcPath = opts?.npmrcPath ?? homeNpmrcPath(); const timeoutMs = opts?.timeoutMs ?? 8_000; const fetchFn = opts?.fetchFn ?? fetch; let auth = null; try { auth = parseNpmrcRegistryAuth(fs.readFileSync(npmrcPath, "utf-8")); } catch { } if (!auth) return { state: "missing" }; try { const res = await fetchFn(`${auth.endpoint}${encodeURIComponent("@mesh-tech/hub")}`, { method: "GET", headers: { authorization: `Bearer ${auth.token}` }, signal: AbortSignal.timeout(timeoutMs), }); const state = classifyRegistryStatus(res.status); return state === "expired" ? { state, detail: `registry answered HTTP ${res.status} for an authenticated request` } : { state }; } catch (err) { return { state: "unreachable", detail: err instanceof Error ? err.message : String(err), }; } } export function classifyRegistryPreflight(probe, session) { const email = session?.email; const fix = "mesh init"; if (probe.state === "fresh") return { state: "valid", email, fix }; return { state: probe.state, email, fix, detail: probe.detail }; } export async function registryPreflight(opts) { const probe = await probeRegistryToken(opts); let session = null; try { const { readRegistrySession } = await import("./registry-identity.js"); session = readRegistrySession(); } catch { } return classifyRegistryPreflight(probe, session); } export function isNpmAuthErrorText(text) { return /\bE?40[13]\b|unauthenticated|unable to authenticate|authentication (required|failed)/i.test(text); } export function isNpmAuthError(err) { const e = err; const text = [e?.stderr, e?.message] .map((v) => (v === null || v === undefined ? "" : String(v))) .join("\n"); return isNpmAuthErrorText(text); } export function isExpiredAwsTokenMessage(message) { return /expired|invalid.*(security )?token|token.*(is )?invalid|no credential|could not load credentials|could not be found|sso session/i.test(message); } export function parseSsoSessionNames(content) { const names = []; for (const line of content.split("\n")) { const match = line.trim().match(/^\[sso-session\s+([^\]]+)\]$/); if (match && match[1]) names.push(match[1].trim()); } return names; } export function awsConfigHasProfile(content, profile) { const pattern = new RegExp(`^\\[(?:profile\\s+)?${profile.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\]$`); return content.split("\n").some((line) => pattern.test(line.trim())); } export function readAwsConfig() { const file = process.env.AWS_CONFIG_FILE ?? path.join(os.homedir(), ".aws", "config"); try { return fs.readFileSync(file, "utf-8"); } catch { return ""; } } export function appUsesMeshPackages(appRoot) { try { const pkg = JSON.parse(fs.readFileSync(path.join(appRoot, "package.json"), "utf-8")); return [pkg.dependencies, pkg.devDependencies, pkg.optionalDependencies].some((deps) => deps && Object.keys(deps).some((name) => name.startsWith("@mesh-tech/"))); } catch { return false; } }