openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
188 lines (187 loc) • 8.27 kB
JavaScript
import { a as normalizeLowercaseStringOrEmpty } from "./string-coerce-mnp54Vah.js";
//#region src/infra/npm-registry-spec.ts
const EXACT_SEMVER_VERSION_RE = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/;
const OPENCLAW_STABLE_CORRECTION_VERSION_RE = /^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<patch>[1-9]\d*)-(?<correction>[1-9]\d*)$/;
const OPENCLAW_STABLE_VERSION_RE = /^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<patch>[1-9]\d*)$/;
const OPENCLAW_ALPHA_VERSION_RE = /^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<patch>[1-9]\d*)-alpha\.(?<alpha>[1-9]\d*)$/;
const OPENCLAW_BETA_VERSION_RE = /^(?<year>\d{4})\.(?<month>[1-9]\d?)\.(?<patch>[1-9]\d*)-beta\.(?<beta>[1-9]\d*)$/;
const DIST_TAG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
function parseRegistryNpmSpecInternal(rawSpec) {
const spec = rawSpec.trim();
if (!spec) return {
ok: false,
error: "missing npm spec"
};
if (/\s/.test(spec)) return {
ok: false,
error: "unsupported npm spec: whitespace is not allowed"
};
if (spec.includes("://")) return {
ok: false,
error: "unsupported npm spec: URLs are not allowed"
};
if (spec.includes("#")) return {
ok: false,
error: "unsupported npm spec: git refs are not allowed"
};
if (spec.includes(":")) return {
ok: false,
error: "unsupported npm spec: protocol specs are not allowed"
};
const at = spec.lastIndexOf("@");
const hasSelector = at > 0;
const name = hasSelector ? spec.slice(0, at) : spec;
const selector = hasSelector ? spec.slice(at + 1) : "";
if (!(name.startsWith("@") ? /^@[a-z0-9][a-z0-9-._~]*\/[a-z0-9][a-z0-9-._~]*$/.test(name) : /^[a-z0-9][a-z0-9-._~]*$/.test(name))) return {
ok: false,
error: "unsupported npm spec: expected <name> or <name>@<version> from the npm registry"
};
if (!hasSelector) return {
ok: true,
parsed: {
name,
raw: spec,
selectorKind: "none",
selectorIsPrerelease: false
}
};
if (!selector) return {
ok: false,
error: "unsupported npm spec: missing version/tag after @"
};
if (/[\\/]/.test(selector)) return {
ok: false,
error: "unsupported npm spec: invalid version/tag"
};
const exactVersionMatch = EXACT_SEMVER_VERSION_RE.exec(selector);
if (exactVersionMatch) return {
ok: true,
parsed: {
name,
raw: spec,
selector,
selectorKind: "exact-version",
selectorIsPrerelease: Boolean(exactVersionMatch[4]) && !isOpenClawStableCorrectionVersion(selector)
}
};
if (!DIST_TAG_RE.test(selector)) return {
ok: false,
error: "unsupported npm spec: use an exact version or dist-tag (ranges are not allowed)"
};
return {
ok: true,
parsed: {
name,
raw: spec,
selector,
selectorKind: "tag",
selectorIsPrerelease: false
}
};
}
/** Parses a registry-only npm package spec into package name and optional selector metadata. */
function parseRegistryNpmSpec(rawSpec) {
const parsed = parseRegistryNpmSpecInternal(rawSpec);
return parsed.ok ? parsed.parsed : null;
}
/** Returns whether a user-provided npm spec resolves to the official OpenClaw npm scope. */
function isOpenClawOrgNpmSpec(rawSpec) {
return (rawSpec ? parseRegistryNpmSpec(rawSpec) : null)?.name.startsWith("@openclaw/") === true;
}
/** Validates a registry-only npm spec and returns a user-facing error when rejected. */
function validateRegistryNpmSpec(rawSpec) {
const parsed = parseRegistryNpmSpecInternal(rawSpec);
return parsed.ok ? null : parsed.error;
}
/** Returns whether a value is an exact semver selector, with optional leading `v`. */
function isExactSemverVersion(value) {
return EXACT_SEMVER_VERSION_RE.test(value.trim());
}
/** Parses OpenClaw's monthly patch stable/alpha/beta/correction version format. */
function parseOpenClawReleaseVersion(value) {
const trimmed = value.trim();
const candidate = [
{
match: OPENCLAW_STABLE_VERSION_RE.exec(trimmed),
channel: "stable"
},
{
match: OPENCLAW_STABLE_CORRECTION_VERSION_RE.exec(trimmed),
channel: "stable"
},
{
match: OPENCLAW_ALPHA_VERSION_RE.exec(trimmed),
channel: "alpha"
},
{
match: OPENCLAW_BETA_VERSION_RE.exec(trimmed),
channel: "beta"
}
].find((entry) => entry.match?.groups);
if (!candidate?.match?.groups) return null;
const year = Number.parseInt(candidate.match.groups.year ?? "", 10);
const month = Number.parseInt(candidate.match.groups.month ?? "", 10);
const patch = Number.parseInt(candidate.match.groups.patch ?? "", 10);
if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(patch) || month < 1 || month > 12 || patch < 1) return null;
const correctionNumber = candidate.channel === "stable" && candidate.match.groups.correction ? Number.parseInt(candidate.match.groups.correction, 10) : void 0;
const alphaNumber = candidate.channel === "alpha" ? Number.parseInt(candidate.match.groups.alpha ?? "", 10) : void 0;
const betaNumber = candidate.channel === "beta" ? Number.parseInt(candidate.match.groups.beta ?? "", 10) : void 0;
return {
channel: candidate.channel,
year,
month,
patch,
correctionNumber,
alphaNumber,
betaNumber
};
}
/** Returns whether a version is an OpenClaw monthly patch stable correction release. */
function isOpenClawStableCorrectionVersion(value) {
const parsed = parseOpenClawReleaseVersion(value);
return parsed?.channel === "stable" && parsed.correctionNumber !== void 0;
}
/** Compares OpenClaw monthly patch release versions across alpha, beta, stable, and corrections. */
function compareOpenClawReleaseVersions(left, right) {
const parsedLeft = parseOpenClawReleaseVersion(left);
const parsedRight = parseOpenClawReleaseVersion(right);
if (!parsedLeft || !parsedRight) return null;
if (parsedLeft.year !== parsedRight.year) return parsedLeft.year < parsedRight.year ? -1 : 1;
if (parsedLeft.month !== parsedRight.month) return parsedLeft.month < parsedRight.month ? -1 : 1;
if (parsedLeft.patch !== parsedRight.patch) return parsedLeft.patch < parsedRight.patch ? -1 : 1;
if (parsedLeft.channel !== parsedRight.channel) {
const rank = {
alpha: 0,
beta: 1,
stable: 2
};
return rank[parsedLeft.channel] < rank[parsedRight.channel] ? -1 : 1;
}
if (parsedLeft.channel === "alpha") return Math.sign((parsedLeft.alphaNumber ?? 0) - (parsedRight.alphaNumber ?? 0));
if (parsedLeft.channel === "beta") return Math.sign((parsedLeft.betaNumber ?? 0) - (parsedRight.betaNumber ?? 0));
return Math.sign((parsedLeft.correctionNumber ?? 0) - (parsedRight.correctionNumber ?? 0));
}
/** Returns whether an exact semver value is a prerelease, excluding stable correction releases. */
function isPrereleaseSemverVersion(value) {
const trimmed = value.trim();
const match = EXACT_SEMVER_VERSION_RE.exec(trimmed);
return Boolean(match?.[4]) && !isOpenClawStableCorrectionVersion(trimmed);
}
/**
* Enforces explicit opt-in before an npm spec may resolve to a prerelease.
* Bare specs and `latest` stay on stable releases unless the resolved version
* is an OpenClaw stable correction.
*/
function isPrereleaseResolutionAllowed(params) {
if (!params.resolvedVersion || !isPrereleaseSemverVersion(params.resolvedVersion)) return true;
if (params.spec.selectorKind === "none") return false;
if (params.spec.selectorKind === "exact-version") return params.spec.selectorIsPrerelease;
return normalizeLowercaseStringOrEmpty(params.spec.selector) !== "latest";
}
/** Formats the install error shown when a registry spec resolves to a disallowed prerelease. */
function formatPrereleaseResolutionError(params) {
const selectorHint = params.spec.selectorKind === "none" || normalizeLowercaseStringOrEmpty(params.spec.selector) === "latest" ? `Use "${params.spec.name}@beta" (or another prerelease tag) or an exact prerelease version to opt in explicitly.` : `Use an explicit prerelease tag or exact prerelease version if you want prerelease installs.`;
return `Resolved ${params.spec.raw} to prerelease version ${params.resolvedVersion}, but prereleases are only installed when explicitly requested. ${selectorHint}`;
}
//#endregion
export { isPrereleaseResolutionAllowed as a, validateRegistryNpmSpec as c, isOpenClawOrgNpmSpec as i, formatPrereleaseResolutionError as n, isPrereleaseSemverVersion as o, isExactSemverVersion as r, parseRegistryNpmSpec as s, compareOpenClawReleaseVersions as t };