






















@@ -0,0 +1,208 @@
1+#!/usr/bin/env node
2+3+import { execFileSync } from "node:child_process";
4+import fs from "node:fs";
5+import os from "node:os";
6+import path from "node:path";
7+import { pathToFileURL } from "node:url";
8+import * as tar from "tar";
9+10+function normalizeStringList(value) {
11+if (!Array.isArray(value)) {
12+return [];
13+}
14+return value.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter(Boolean);
15+}
16+17+function normalizePackagePath(value) {
18+return value
19+.replace(/\\/g, "/")
20+.replace(/^package\//u, "")
21+.replace(/^\.\//u, "");
22+}
23+24+function isTypeScriptPackageEntry(entryPath) {
25+return [".ts", ".mts", ".cts"].includes(path.extname(entryPath).toLowerCase());
26+}
27+28+function listBuiltRuntimeEntryCandidates(entryPath) {
29+if (!isTypeScriptPackageEntry(entryPath)) {
30+return [];
31+}
32+const normalized = entryPath.replace(/\\/g, "/");
33+const withoutExtension = normalized.replace(/\.[^.]+$/u, "");
34+const normalizedRelative = normalized.replace(/^\.\//u, "");
35+const distWithoutExtension = normalizedRelative.startsWith("src/")
36+ ? `./dist/${normalizedRelative.slice("src/".length).replace(/\.[^.]+$/u, "")}`
37+ : `./dist/${withoutExtension.replace(/^\.\//u, "")}`;
38+const withJavaScriptExtensions = (basePath) => [
39+`${basePath}.js`,
40+`${basePath}.mjs`,
41+`${basePath}.cjs`,
42+];
43+return [
44+ ...new Set([
45+ ...withJavaScriptExtensions(distWithoutExtension),
46+ ...withJavaScriptExtensions(withoutExtension),
47+]),
48+].filter((candidate) => candidate !== normalized);
49+}
50+51+function formatPackageLabel(packageJson, fallbackSpec) {
52+const packageName = typeof packageJson.name === "string" ? packageJson.name.trim() : "";
53+const packageVersion = typeof packageJson.version === "string" ? packageJson.version.trim() : "";
54+if (packageName && packageVersion) {
55+return `${packageName}@${packageVersion}`;
56+}
57+return packageName || fallbackSpec || "<package>";
58+}
59+60+export function collectPluginNpmPublishedRuntimeErrors(params) {
61+const packageJson = params.packageJson ?? {};
62+const packageFiles = new Set([...params.files].map(normalizePackagePath));
63+const packageLabel = formatPackageLabel(packageJson, params.spec);
64+const extensions = normalizeStringList(packageJson.openclaw?.extensions);
65+const runtimeExtensions = normalizeStringList(packageJson.openclaw?.runtimeExtensions);
66+const errors = [];
67+68+if (extensions.length === 0) {
69+return errors;
70+}
71+72+if (runtimeExtensions.length > 0 && runtimeExtensions.length !== extensions.length) {
73+errors.push(
74+`${packageLabel} package.json openclaw.runtimeExtensions length (${runtimeExtensions.length}) must match openclaw.extensions length (${extensions.length})`,
75+);
76+return errors;
77+}
78+79+for (const [index, entry] of extensions.entries()) {
80+const runtimeEntry = runtimeExtensions[index];
81+if (runtimeEntry) {
82+if (!packageFiles.has(normalizePackagePath(runtimeEntry))) {
83+errors.push(`${packageLabel} runtime extension entry not found: ${runtimeEntry}`);
84+}
85+continue;
86+}
87+88+if (!isTypeScriptPackageEntry(entry)) {
89+continue;
90+}
91+92+const candidates = listBuiltRuntimeEntryCandidates(entry);
93+if (candidates.some((candidate) => packageFiles.has(normalizePackagePath(candidate)))) {
94+continue;
95+}
96+97+errors.push(
98+`${packageLabel} requires compiled runtime output for TypeScript entry ${entry}: expected ${candidates.join(", ")}`,
99+);
100+}
101+102+return errors;
103+}
104+105+function npmPack(spec, destinationDir) {
106+const output = execFileSync(
107+"npm",
108+["pack", spec, "--json", "--ignore-scripts", "--pack-destination", destinationDir],
109+{
110+encoding: "utf8",
111+stdio: ["ignore", "pipe", "pipe"],
112+},
113+);
114+const rows = JSON.parse(output);
115+const filename = rows?.[0]?.filename;
116+if (typeof filename !== "string" || !filename) {
117+throw new Error(`npm pack ${spec} did not report a tarball filename`);
118+}
119+return path.isAbsolute(filename) ? filename : path.join(destinationDir, filename);
120+}
121+122+function sleep(ms) {
123+return new Promise((resolve) => setTimeout(resolve, ms));
124+}
125+126+async function packPublishedPackage(spec, destinationDir) {
127+const attempts = Number.parseInt(process.env.OPENCLAW_PLUGIN_NPM_VERIFY_ATTEMPTS ?? "6", 10);
128+const delayMs = Number.parseInt(process.env.OPENCLAW_PLUGIN_NPM_VERIFY_DELAY_MS ?? "5000", 10);
129+let lastError;
130+for (let attempt = 1; attempt <= attempts; attempt += 1) {
131+try {
132+return npmPack(spec, destinationDir);
133+} catch (error) {
134+lastError = error;
135+if (attempt < attempts) {
136+await sleep(delayMs);
137+}
138+}
139+}
140+throw lastError;
141+}
142+143+function listFiles(rootDir, prefix = "") {
144+const files = [];
145+for (const entry of fs.readdirSync(path.join(rootDir, prefix), { withFileTypes: true })) {
146+const relativePath = path.join(prefix, entry.name).replace(/\\/g, "/");
147+if (entry.isDirectory()) {
148+files.push(...listFiles(rootDir, relativePath));
149+} else if (entry.isFile()) {
150+files.push(relativePath);
151+}
152+}
153+return files;
154+}
155+156+async function readPackedPackage(tarballPath, extractDir) {
157+await tar.x({ file: tarballPath, cwd: extractDir });
158+const packageDir = path.join(extractDir, "package");
159+const packageJson = JSON.parse(fs.readFileSync(path.join(packageDir, "package.json"), "utf8"));
160+return {
161+ packageJson,
162+files: listFiles(packageDir),
163+};
164+}
165+166+export async function verifyPublishedPluginRuntime(spec) {
167+const workingDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-plugin-npm-runtime."));
168+try {
169+const tarballPath = await packPublishedPackage(spec, workingDir);
170+const extractDir = path.join(workingDir, "extract");
171+fs.mkdirSync(extractDir, { recursive: true });
172+const packedPackage = await readPackedPackage(tarballPath, extractDir);
173+const errors = collectPluginNpmPublishedRuntimeErrors({
174+ ...packedPackage,
175+ spec,
176+});
177+if (errors.length > 0) {
178+throw new Error(errors.join("\n"));
179+}
180+return {
181+packageName: packedPackage.packageJson.name,
182+version: packedPackage.packageJson.version,
183+fileCount: packedPackage.files.length,
184+};
185+} finally {
186+fs.rmSync(workingDir, { force: true, recursive: true });
187+}
188+}
189+190+async function main(argv) {
191+const spec = argv[0]?.trim();
192+if (!spec) {
193+throw new Error("Usage: node scripts/verify-plugin-npm-published-runtime.mjs <package-spec>");
194+}
195+const result = await verifyPublishedPluginRuntime(spec);
196+console.log(
197+`plugin-npm-published-runtime-check: ${result.packageName}@${result.version} OK (${result.fileCount} files)`,
198+);
199+}
200+201+if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
202+main(process.argv.slice(2)).catch((error) => {
203+console.error(
204+`plugin-npm-published-runtime-check: ${error instanceof Error ? error.message : String(error)}`,
205+);
206+process.exitCode = 1;
207+});
208+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。