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