openclaw/scripts/plugin-npm-prepared-release.mjs

694 lines
26 KiB
JavaScript

#!/usr/bin/env node
import { createHash } from "node:crypto";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import {
downloadActionsArtifactArchive,
inspectActionsArtifactZipWithPolicy,
readBoundedRegularFile,
validateActionsArtifactBinding,
validateActionsArtifactProducerJob,
} from "./lib/actions-artifact-archive.mjs";
import {
classifyNpmDistTagVersion,
fetchNpmRegistryPackumentWithRetry,
fetchNpmRegistryTarballWithRetry,
npmRegistryReadbackDeadline,
NpmRegistryUnavailableError,
resolveNpmPublishPlan,
} from "./lib/npm-publish-plan.mjs";
import { collectExtensionPackageJsonCandidates } from "./lib/plugin-publication-candidates.ts";
import { isPluginPublicationEnabled } from "./lib/plugin-publication-target.mjs";
import { parseReleaseVersion } from "./lib/release-version.mjs";
import {
inspectPackageTarballBytes,
verifyPluginPublicationArtifact,
} from "./plugin-publication-artifact.mjs";
export const PREPARED_NPM_MANIFEST = "plugin-npm-prepared.json";
const SCHEMA = "openclaw.plugin-npm-prepared/v1";
const WORKFLOW_PATH = ".github/workflows/plugin-npm-release.yml";
const MAX_MANIFEST_BYTES = 1024 * 1024;
const MAX_PACKAGE_BYTES = 256 * 1024 * 1024;
const SHA = /^[a-f0-9]{40}$/u;
const DIGEST = /^sha256:[a-f0-9]{64}$/u;
const PACKAGE = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/u;
const ROUTES = new Set(["npm-oidc", "npm-token-bootstrap", "npm-readback"]);
function requireValue(condition, message) {
if (!condition) {
throw new Error(message);
}
}
function sha256(bytes) {
return createHash("sha256").update(bytes).digest("hex");
}
function readJson(file, maxBytes = MAX_MANIFEST_BYTES) {
return JSON.parse(readBoundedRegularFile(file, { label: file, maxBytes }).toString("utf8"));
}
function normalizeProducer(value) {
requireValue(
value?.workflowPath === WORKFLOW_PATH &&
value.workflowEvent === "workflow_dispatch" &&
/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u.test(value.repository ?? "") &&
SHA.test(value.workflowSha ?? "") &&
Number.isSafeInteger(value.runId) &&
value.runId > 0 &&
Number.isSafeInteger(value.runAttempt) &&
value.runAttempt > 0 &&
typeof value.workflowHeadBranch === "string" &&
/^(?:main|release\/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*|extended-stable\/[0-9]{4}\.[1-9][0-9]*\.33|release-publish\/[a-f0-9]{12}-[1-9][0-9]*)$/u.test(
value.workflowHeadBranch,
),
"Prepared npm producer must bind an exact trusted workflow run and attempt.",
);
return {
repository: value.repository,
runId: value.runId,
runAttempt: value.runAttempt,
workflowPath: value.workflowPath,
workflowEvent: value.workflowEvent,
workflowHeadBranch: value.workflowHeadBranch,
workflowSha: value.workflowSha,
};
}
function normalizePackage(value, npmDistTag) {
requireValue(
/^[a-z0-9][a-z0-9._-]*$/u.test(value?.extensionId ?? "") &&
value.packageDir === `extensions/${value.extensionId}` &&
PACKAGE.test(value.packageName ?? "") &&
typeof value.installNpmSpec === "string" &&
value.installNpmSpec.length > 0 &&
!/[\s\p{Cc}]/u.test(value.installNpmSpec),
"Prepared npm package identity is invalid.",
);
const plan = resolveNpmPublishPlan(
value.version,
undefined,
npmDistTag === "default" ? undefined : npmDistTag,
);
requireValue(
value.channel === plan.channel && value.publishTag === plan.publishTag,
"Prepared npm package channel differs from its version.",
);
return {
extensionId: value.extensionId,
packageDir: value.packageDir,
packageName: value.packageName,
version: value.version,
channel: value.channel,
publishTag: value.publishTag,
installNpmSpec: value.installNpmSpec,
};
}
function artifactTuple(metadata, producer) {
requireValue(
Number.isSafeInteger(metadata.id) &&
metadata.id > 0 &&
typeof metadata.name === "string" &&
/^[A-Za-z0-9][A-Za-z0-9_.-]*$/u.test(metadata.name) &&
DIGEST.test(metadata.digest ?? "") &&
Number.isSafeInteger(metadata.size_in_bytes) &&
metadata.size_in_bytes > 0 &&
metadata.size_in_bytes <= MAX_PACKAGE_BYTES,
"Prepared npm artifact identity is invalid.",
);
return {
...producer,
artifactId: metadata.id,
artifactName: metadata.name,
artifactDigest: metadata.digest,
artifactSizeBytes: metadata.size_in_bytes,
};
}
function packageArtifactName(entry, route, producer) {
return `plugin-npm-package-${entry.extensionId}-${entry.version}-${route}-${producer.runId}-${producer.runAttempt}`;
}
export function preparedNpmArtifactName(sourceSha, producer) {
requireValue(SHA.test(sourceSha), "Prepared npm source must be a full lowercase SHA.");
const identity = normalizeProducer(producer);
return `plugin-npm-prepared-${sourceSha}-${identity.runId}-${identity.runAttempt}`;
}
function sourcePackageRoster(sourceRoot, npmDistTag, selectedNames) {
return collectExtensionPackageJsonCandidates(sourceRoot)
.filter(({ packageJson }) => isPluginPublicationEnabled(packageJson, "npm"))
.filter(
({ packageJson }) =>
selectedNames.length === 0 || selectedNames.includes(packageJson.name?.trim()),
)
.map(({ extensionId, packageDir, packageJson }) => {
const plan = resolveNpmPublishPlan(
packageJson.version,
undefined,
npmDistTag === "default" ? undefined : npmDistTag,
);
return normalizePackage(
{
extensionId,
packageDir,
packageName: packageJson.name?.trim(),
version: packageJson.version?.trim(),
channel: plan.channel,
publishTag: plan.publishTag,
installNpmSpec: packageJson.openclaw?.install?.npmSpec?.trim(),
},
npmDistTag,
);
})
.toSorted((a, b) =>
a.packageName < b.packageName ? -1 : a.packageName > b.packageName ? 1 : 0,
);
}
export function validatePreparedNpmRelease(manifest, expected) {
const producer = normalizeProducer(manifest?.producer);
requireValue(
manifest.schema === SCHEMA &&
manifest.runtimeQualification === "packed-runtime-v1" &&
SHA.test(manifest.sourceSha ?? "") &&
manifest.sourceSha === expected.sourceSha &&
manifest.npmDistTag === expected.npmDistTag &&
["default", "extended-stable"].includes(manifest.npmDistTag) &&
manifest.selectionMode === expected.selectionMode &&
["selected", "all-publishable"].includes(manifest.selectionMode) &&
manifest.publisherPolicySha256 === expected.publisherPolicySha256 &&
/^[a-f0-9]{64}$/u.test(manifest.publisherPolicySha256 ?? "") &&
producer.repository === expected.repository &&
producer.workflowSha === expected.workflowSha,
"Prepared npm release differs from the approved source, tooling, selection, or channel.",
);
requireValue(
Array.isArray(manifest.packages) &&
manifest.packages.length > 0 &&
manifest.packages.length <= 256,
"Prepared npm release must contain the complete non-empty package roster.",
);
const names = new Set();
const artifacts = new Set();
const packages = manifest.packages.map((value) => {
const entry = normalizePackage(value, manifest.npmDistTag);
const tuple = value.artifact;
requireValue(
ROUTES.has(value.route),
"Prepared npm route requires separately authorized repair tooling.",
);
if (value.route === "npm-token-bootstrap") {
const parsed = parseReleaseVersion(entry.version);
requireValue(
entry.channel === "beta" ||
(parsed?.channel === "stable" &&
parsed.patch < 33 &&
manifest.npmDistTag === "default" &&
entry.publishTag === "latest"),
"Prepared npm token bootstrap requires beta or regular stable/latest qualification.",
);
}
requireValue(
JSON.stringify(normalizeProducer(tuple)) === JSON.stringify(producer),
"Prepared npm package producer differs from the sealed release.",
);
const normalizedTuple = artifactTuple(
{
id: tuple.artifactId,
name: tuple.artifactName,
digest: tuple.artifactDigest,
size_in_bytes: tuple.artifactSizeBytes,
},
producer,
);
requireValue(
normalizedTuple.artifactName === packageArtifactName(entry, value.route, producer),
"Prepared npm package artifact does not bind its exact producer attempt.",
);
requireValue(
!names.has(entry.packageName) && !artifacts.has(tuple.artifactId),
"Prepared npm release contains duplicate packages or artifacts.",
);
names.add(entry.packageName);
artifacts.add(tuple.artifactId);
return { ...entry, route: value.route, artifact: normalizedTuple };
});
const selected = (expected.plugins ?? "")
.split(",")
.map((name) => name.trim())
.filter(Boolean)
.toSorted();
if (manifest.selectionMode === "all-publishable") {
requireValue(
selected.length === 0,
"All-publishable prepared npm release must not narrow the roster.",
);
} else {
requireValue(
JSON.stringify(selected) ===
JSON.stringify([...names].toSorted((a, b) => (a < b ? -1 : a > b ? 1 : 0))),
"Prepared npm package roster differs from the requested selection.",
);
}
if (expected.sourceRoot !== undefined) {
const sourcePackages = sourcePackageRoster(expected.sourceRoot, manifest.npmDistTag, selected);
requireValue(
JSON.stringify(sourcePackages) ===
JSON.stringify(packages.map((entry) => normalizePackage(entry, manifest.npmDistTag))),
"Prepared npm roster differs from the complete selected frozen-source roster.",
);
}
return { ...manifest, producer, packages };
}
export function createPreparedNpmRelease(params) {
const producer = normalizeProducer(params.producer);
const packages = params.matrix
.map((value) => {
const entry = normalizePackage(value, params.npmDistTag);
const matches = params.artifacts.filter((artifact) =>
[...ROUTES].some((route) => artifact.name === packageArtifactName(entry, route, producer)),
);
requireValue(
matches.length === 1,
`Expected one qualified npm artifact for ${entry.packageName}.`,
);
const [metadata] = matches;
const route = [...ROUTES].find(
(candidate) => metadata.name === packageArtifactName(entry, candidate, producer),
);
const artifact = artifactTuple(metadata, producer);
const expected = {
...artifact,
runStatePolicy: "same-run-producer-success",
consumerRunAttempt: producer.runAttempt,
producerJobName: `Preflight plugin npm package (${entry.packageName})`,
};
validateActionsArtifactBinding({
artifactMetadata: metadata,
expected,
workflowRun: params.workflowRun,
});
validateActionsArtifactProducerJob({ expected, workflowJobs: params.workflowJobs });
return { ...entry, route, artifact };
})
.toSorted((a, b) =>
a.packageName < b.packageName ? -1 : a.packageName > b.packageName ? 1 : 0,
);
return validatePreparedNpmRelease(
{
schema: SCHEMA,
runtimeQualification: "packed-runtime-v1",
sourceSha: params.sourceSha,
npmDistTag: params.npmDistTag,
selectionMode: params.selectionMode,
publisherPolicySha256: params.publisherPolicySha256,
producer,
packages,
},
{ ...params, repository: producer.repository, workflowSha: producer.workflowSha },
);
}
export async function downloadPreparedNpmRelease(params) {
requireValue(
typeof params.sourceRoot === "string",
"Prepared npm admission requires the frozen source root.",
);
const producer = normalizeProducer(params.artifact);
requireValue(
producer.repository === params.repository &&
producer.workflowSha === params.workflowSha &&
params.artifact.artifactName === preparedNpmArtifactName(params.sourceSha, producer),
"Prepared npm manifest artifact differs from the approved producer.",
);
const downloaded = await downloadActionsArtifactArchive({
token: params.token,
fetchImpl: params.fetchImpl,
expected: { ...params.artifact, runStatePolicy: "completed-success" },
archivePath: params.archivePath,
maxArchiveBytes: MAX_MANIFEST_BYTES,
});
const files = inspectActionsArtifactZipWithPolicy(downloaded.archiveBytes, {
minEntries: 1,
maxEntries: 1,
maxArchiveBytes: MAX_MANIFEST_BYTES,
maxExpandedBytes: MAX_MANIFEST_BYTES,
maxEntryBytes: () => MAX_MANIFEST_BYTES,
allowPath: (name) => name === PREPARED_NPM_MANIFEST,
});
const manifest = validatePreparedNpmRelease(
JSON.parse(files.get(PREPARED_NPM_MANIFEST).toString("utf8")),
params,
);
requireValue(
JSON.stringify(manifest.producer) === JSON.stringify(producer),
"Prepared npm manifest producer differs from its artifact.",
);
return manifest;
}
export async function consumePreparedNpmPackage(params) {
const entry = params.package;
const producer = normalizeProducer(entry.artifact);
normalizePackage(entry, params.npmDistTag);
requireValue(
producer.repository === params.repository &&
producer.workflowSha === params.workflowSha &&
ROUTES.has(entry.route) &&
entry.artifact.artifactName === packageArtifactName(entry, entry.route, producer),
"Prepared npm package differs from the approved publication identity.",
);
const cacheDir = resolve(params.cacheDir);
mkdirSync(cacheDir, { recursive: true });
const archivePath = join(cacheDir, `${entry.artifact.artifactId}.zip`);
const downloaded = await downloadActionsArtifactArchive({
token: params.token,
fetchImpl: params.fetchImpl,
expected: { ...entry.artifact, runStatePolicy: "completed-success" },
archivePath,
});
const metadataPath = join(cacheDir, `${entry.artifact.artifactId}.metadata.json`);
const runPath = join(cacheDir, `${entry.artifact.artifactId}.run.json`);
writeFileSync(metadataPath, JSON.stringify(downloaded.artifactMetadata));
writeFileSync(runPath, JSON.stringify(downloaded.workflowRun));
return verifyPluginPublicationArtifact({
...entry.artifact,
artifactMetadataPath: metadataPath,
artifactZipPath: archivePath,
workflowRunMetadataPath: runPath,
runStatePolicy: "completed-success",
packageDir: entry.packageDir,
packageName: entry.packageName,
version: entry.version,
publishTag: entry.publishTag,
route: entry.route,
targetSha: params.sourceSha,
publicationReason: `Stable npm registry preflight selected ${entry.route}.`,
publisherPolicy: {
policyId: "plugin-npm-release-workflow",
schema: "openclaw.plugin-npm-publisher-policy/v1",
sha256: params.publisherPolicySha256,
},
sourcePackageJsonSha256: sha256(
readBoundedRegularFile(params.sourcePackageJson, {
label: "source package.json",
maxBytes: MAX_MANIFEST_BYTES,
}),
),
outputDir: params.outputDir,
verificationOutput: params.verificationOutput,
});
}
export async function verifyPreparedNpmRegistry(params) {
const tarball = readBoundedRegularFile(params.tarballPath, {
label: "qualified plugin tarball",
maxBytes: MAX_PACKAGE_BYTES,
});
const { distTags } = await verifyNpmRegistryPublication(params, tarball);
if (distTags) {
requireNpmDistTagSelection(params, distTags);
}
return { alreadyPublished: distTags !== null };
}
// A version this run never published may already be superseded: a later
// release owns the primary selector, so no tag is verified or repaired for it.
export async function verifyPublishedNpmRegistry(params) {
const { distTags } = await verifyNpmRegistryPublication({
...params,
route: "npm-readback",
allowMissing: false,
});
const [primaryTag] = params.publishTags;
const supersededBy = distTags[primaryTag];
if (classifyNpmDistTagVersion(supersededBy, params.version) === "ahead") {
return { alreadyPublished: true, supersededBy };
}
requireNpmDistTagSelection(params, distTags);
return { alreadyPublished: true, supersededBy: null };
}
function requireNpmDistTagSelection(params, distTags) {
for (const publishTag of params.publishTags) {
const selectorState = classifyNpmDistTagVersion(distTags[publishTag], params.version);
if (selectorState !== "match") {
const message = `${params.packageName}: ${publishTag} differs from the prepared version; use authorized tag repair.`;
if (selectorState === "missing" || selectorState === "lagging") {
throw new NpmRegistryUnavailableError(message);
}
throw new Error(message);
}
}
}
async function verifyNpmRegistryPublication(params, tarball) {
requireValue(
PACKAGE.test(params.packageName) && ROUTES.has(params.route),
"Invalid prepared npm publication request.",
);
const deadlineMs = params.allowMissing === true ? undefined : npmRegistryReadbackDeadline();
const pendingMessage = `${params.packageName}@${params.version}: exact version is not visible yet; verification pending. Retry readback, not publication.`;
let registry;
let version;
for (;;) {
if (deadlineMs !== undefined && Date.now() >= deadlineMs) {
throw new NpmRegistryUnavailableError(pendingMessage);
}
registry = await fetchNpmRegistryPackumentWithRetry({
packageName: params.packageName,
packageUrl: `https://registry.npmjs.org/${encodeURIComponent(params.packageName)}`,
fetchImpl: params.fetchImpl,
deadlineMs,
});
requireValue(
registry.status === 404 || registry.ok,
`npm registry returned HTTP ${registry.status}.`,
);
if (registry.ok) {
requireValue(
registry.packument?.name === params.packageName &&
registry.packument.versions &&
typeof registry.packument.versions === "object" &&
!Array.isArray(registry.packument.versions) &&
registry.packument["dist-tags"] &&
typeof registry.packument["dist-tags"] === "object" &&
!Array.isArray(registry.packument["dist-tags"]),
"npm registry response is not an authoritative package inventory.",
);
}
version = registry.packument?.versions?.[params.version];
if (version) {
break;
}
if (params.allowMissing === true) {
requireValue(params.route !== "npm-readback", pendingMessage);
requireValue(
registry.status !== 404 || params.route === "npm-token-bootstrap",
"Prepared OIDC package no longer exists; obtain an explicitly approved bootstrap.",
);
return { distTags: null };
}
const remainingMs = deadlineMs - Date.now();
if (remainingMs <= 0) {
throw new NpmRegistryUnavailableError(pendingMessage);
}
console.error(pendingMessage);
await new Promise((resolveDelay) => {
setTimeout(resolveDelay, Math.min(10_000, remainingMs));
});
}
requireValue(
version.name === params.packageName && version.version === params.version,
"npm registry version identity differs from the requested package.",
);
if (tarball) {
requireValue(
version.dist?.integrity ===
`sha512-${createHash("sha512").update(tarball).digest("base64")}` &&
version.dist?.shasum === createHash("sha1").update(tarball).digest("hex"),
`${params.packageName}@${params.version}: registry bytes conflict with the qualified artifact.`,
);
}
const url = new URL(version.dist.tarball);
requireValue(
url.origin === "https://registry.npmjs.org" && !url.username && !url.password,
"Prepared npm readback requires the canonical registry tarball URL.",
);
const published = await fetchNpmRegistryTarballWithRetry({
packageName: params.packageName,
packageUrl: url.href,
maxBytes: tarball?.length ?? MAX_PACKAGE_BYTES,
deadlineMs,
fetchImpl: params.fetchImpl,
});
if (tarball) {
requireValue(
published.length === tarball.length && sha256(published) === sha256(tarball),
"Published npm tarball bytes differ from the qualified artifact.",
);
} else {
requireValue(
version.dist?.integrity ===
`sha512-${createHash("sha512").update(published).digest("base64")}` &&
version.dist?.shasum === createHash("sha1").update(published).digest("hex"),
"Published npm tarball bytes differ from registry integrity.",
);
const { packageManifest } = inspectPackageTarballBytes(published);
requireValue(
packageManifest.name === params.packageName && packageManifest.version === params.version,
"Published npm archive package identity differs from the requested version.",
);
}
return { distTags: registry.packument["dist-tags"] };
}
function outputValues(file, values) {
writeFileSync(
file,
`${Object.entries(values)
.map(([key, value]) => `${key}=${value}`)
.join("\n")}\n`,
{ flag: "a" },
);
}
async function main(argv) {
const [command, ...args] = argv;
const options = {};
for (let index = 0; index < args.length; index += 2) {
const key = args[index];
requireValue(
key?.startsWith("--") && args[index + 1] !== undefined && !Object.hasOwn(options, key),
"Invalid prepared npm arguments.",
);
options[key] = args[index + 1];
}
const common = {
repository: process.env.GITHUB_REPOSITORY,
workflowSha: process.env.GITHUB_SHA,
sourceSha: options["--source-sha"],
npmDistTag: options["--npm-dist-tag"] ?? "default",
selectionMode: options["--selection-mode"],
plugins: options["--plugins"] ?? "",
publisherPolicySha256: options["--policy-file"]
? sha256(readFileSync(options["--policy-file"]))
: undefined,
token: process.env.GH_TOKEN,
};
if (command === "seal") {
const producer = {
repository: common.repository,
runId: Number(process.env.GITHUB_RUN_ID),
runAttempt: Number(process.env.GITHUB_RUN_ATTEMPT),
workflowPath: WORKFLOW_PATH,
workflowEvent: process.env.GITHUB_EVENT_NAME,
workflowHeadBranch: process.env.GITHUB_REF_NAME,
workflowSha: common.workflowSha,
};
const manifest = createPreparedNpmRelease({
...common,
producer,
matrix: JSON.parse(process.env.PLUGIN_MATRIX),
artifacts: readJson(options["--artifacts-file"], 4 * MAX_MANIFEST_BYTES).artifacts,
workflowRun: readJson(options["--run-file"]),
workflowJobs: readJson(options["--jobs-file"], 4 * MAX_MANIFEST_BYTES),
});
mkdirSync(options["--output-dir"], { recursive: true });
writeFileSync(
join(options["--output-dir"], PREPARED_NPM_MANIFEST),
`${JSON.stringify(manifest)}\n`,
);
outputValues(options["--github-output"], {
artifact_name: preparedNpmArtifactName(common.sourceSha, producer),
});
} else if (command === "load") {
const manifest = await downloadPreparedNpmRelease({
...common,
sourceRoot: options["--source-root"] ?? process.cwd(),
artifact: JSON.parse(options["--artifact"]),
archivePath: options["--archive-path"],
});
writeFileSync(options["--output"], JSON.stringify(manifest));
outputValues(options["--github-output"], {
has_candidates: true,
has_selection: true,
candidate_count: manifest.packages.length,
selection_count: manifest.packages.length,
matrix: JSON.stringify(manifest.packages),
all_matrix: JSON.stringify(manifest.packages),
});
} else if (command === "consume") {
const result = await consumePreparedNpmPackage({
...common,
package: JSON.parse(options["--package"]),
sourcePackageJson: options["--source-package-json"],
cacheDir: options["--cache-dir"],
outputDir: options["--output-dir"],
verificationOutput: options["--verification-output"],
});
outputValues(options["--github-output"], {
package_name: result.manifest.package.name,
package_version: result.manifest.package.version,
publish_route: result.manifest.publication.route,
publish_tag: result.manifest.publication.tag,
tarball_path: result.tarballPath,
tarball_sha256: result.tarballSha256,
});
} else if (command === "registry") {
let result;
try {
result = await verifyPreparedNpmRegistry({
packageName: options["--package-name"],
version: options["--version"],
publishTags: [options["--publish-tag"]],
route: options["--route"],
tarballPath: options["--tarball"],
allowMissing: options["--allow-missing"] === "true",
});
} catch (error) {
// Only an accepted publish with a parent verifier may defer unavailable
// reads. Conflicting bytes/identity and unsafe selectors remain hard failures.
if (
options["--defer-visibility"] !== "true" ||
options["--allow-missing"] !== "false" ||
options["--route"] === "npm-readback" ||
!(error instanceof NpmRegistryUnavailableError)
) {
throw error;
}
const message = `${options["--package-name"]}@${options["--version"]}: published, visibility pending; the parent's final registry verification must pass before release completion. Do not republish.`;
console.warn(`::warning::${message}`);
if (process.env.GITHUB_STEP_SUMMARY) {
writeFileSync(process.env.GITHUB_STEP_SUMMARY, `- ${message}\n`, { flag: "a" });
}
return;
}
if (options["--github-output"]) {
outputValues(options["--github-output"], { already_published: result.alreadyPublished });
}
console.log(
`${options["--package-name"]}@${options["--version"]}: ${result.alreadyPublished ? "verified exact published bytes and selector" : "qualified artifact ready for publication"}`,
);
} else {
throw new Error(
"Usage: plugin-npm-prepared-release.mjs <seal|load|consume|registry> [options]",
);
}
}
if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
try {
await main(process.argv.slice(2));
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
console.error("[plugin-npm-prepared-release] FAILED (exit 1)");
process.exitCode = 1;
}
}