@openclaw/plugin-inspector 0.3.18 → 0.3.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +14 -6
- package/examples/github-actions-code-scanning.yml +3 -3
- package/examples/github-actions-plugin-inspector.yml +3 -3
- package/package.json +7 -2
- package/src/advanced.js +7 -0
- package/src/api.js +4 -0
- package/src/batch.js +8 -0
- package/src/cli.js +33 -10
- package/src/compatibility-report.js +6 -0
- package/src/config.js +2 -1
- package/src/fixture-summary.js +162 -20
- package/src/index.js +12 -3
- package/src/init.js +3 -3
- package/src/inspector.js +170 -11
- package/src/issues.js +4 -5
- package/src/openclaw-target.js +202 -6
- package/src/openclaw-version.js +246 -0
- package/src/report.js +1 -0
package/src/openclaw-target.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
5
|
export const defaultOpenClawCheckoutPaths = ["./openclaw", "../openclaw"];
|
|
@@ -26,11 +26,16 @@ export async function readOpenClawTargetSurface(options = {}) {
|
|
|
26
26
|
});
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
if (match.kind === "package") {
|
|
30
|
+
return readPackedOpenClawTargetSurface({ rootDir, requestedPaths, ...match });
|
|
31
|
+
}
|
|
32
|
+
|
|
29
33
|
const { requestedPath, resolvedPath, registryPath } = match;
|
|
30
34
|
const hookTypesPath = path.join(resolvedPath, "src/plugins/hook-types.ts");
|
|
31
35
|
const apiBuilderPath = path.join(resolvedPath, "src/plugins/api-builder.ts");
|
|
32
36
|
const capturedRegistrationPath = path.join(resolvedPath, "src/plugins/captured-registration.ts");
|
|
33
|
-
const
|
|
37
|
+
const currentManifestTypesPath = path.join(resolvedPath, "src/plugins/manifest-types.ts");
|
|
38
|
+
const legacyManifestTypesPath = path.join(resolvedPath, "src/plugins/manifest.ts");
|
|
34
39
|
const pluginSdkEntrypointsPath = path.join(resolvedPath, "src/plugin-sdk/entrypoints.ts");
|
|
35
40
|
const packagePath = path.join(resolvedPath, "package.json");
|
|
36
41
|
|
|
@@ -40,9 +45,21 @@ export async function readOpenClawTargetSurface(options = {}) {
|
|
|
40
45
|
const hookNames = hookTypesSource ? parseConstStringArray(hookTypesSource, "PLUGIN_HOOK_NAMES") : [];
|
|
41
46
|
const apiBuilderSource = existsSync(apiBuilderPath) ? await readFile(apiBuilderPath, "utf8") : "";
|
|
42
47
|
const apiRegistrars = apiBuilderSource ? parseApiRegistrars(apiBuilderSource) : [];
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
const currentManifestTypesSource = existsSync(currentManifestTypesPath)
|
|
49
|
+
? await readFile(currentManifestTypesPath, "utf8")
|
|
50
|
+
: "";
|
|
51
|
+
const legacyManifestTypesSource = existsSync(legacyManifestTypesPath)
|
|
52
|
+
? await readFile(legacyManifestTypesPath, "utf8")
|
|
53
|
+
: "";
|
|
54
|
+
const currentManifestFields = parseTypeFields(currentManifestTypesSource, "PluginManifest");
|
|
55
|
+
const legacyManifestFields = parseTypeFields(legacyManifestTypesSource, "PluginManifest");
|
|
56
|
+
const currentManifestContractFields = parseTypeFields(currentManifestTypesSource, "PluginManifestContracts");
|
|
57
|
+
const legacyManifestContractFields = parseTypeFields(legacyManifestTypesSource, "PluginManifestContracts");
|
|
58
|
+
const useCurrentManifestTypes = currentManifestFields.length > 0;
|
|
59
|
+
const manifestTypesPath = useCurrentManifestTypes ? currentManifestTypesPath : legacyManifestTypesPath;
|
|
60
|
+
const manifestFields = useCurrentManifestTypes ? currentManifestFields : legacyManifestFields;
|
|
61
|
+
const manifestContractFields =
|
|
62
|
+
currentManifestContractFields.length > 0 ? currentManifestContractFields : legacyManifestContractFields;
|
|
46
63
|
const capturedRegistrars = existsSync(capturedRegistrationPath)
|
|
47
64
|
? parseCapturedRegistrars(await readFile(capturedRegistrationPath, "utf8"))
|
|
48
65
|
: [];
|
|
@@ -238,12 +255,191 @@ function findTargetCheckout(rootDir, requestedPaths) {
|
|
|
238
255
|
const resolvedPath = path.resolve(rootDir, requestedPath);
|
|
239
256
|
const registryPath = path.join(resolvedPath, "src/plugins/compat/registry.ts");
|
|
240
257
|
if (existsSync(registryPath)) {
|
|
241
|
-
return { requestedPath, resolvedPath, registryPath };
|
|
258
|
+
return { kind: "checkout", requestedPath, resolvedPath, registryPath };
|
|
259
|
+
}
|
|
260
|
+
if (existsSync(path.join(resolvedPath, "package.json")) && existsSync(path.join(resolvedPath, "dist"))) {
|
|
261
|
+
return { kind: "package", requestedPath, resolvedPath };
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function readPackedOpenClawTargetSurface({ rootDir, requestedPaths, requestedPath, resolvedPath }) {
|
|
268
|
+
const packagePath = path.join(resolvedPath, "package.json");
|
|
269
|
+
const packageJson = JSON.parse(await readFile(packagePath, "utf8"));
|
|
270
|
+
if (packageJson.name !== "openclaw") {
|
|
271
|
+
return emptyTargetSurface({ configuredPath: requestedPath, searchedPaths: requestedPaths, status: "missing" });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const distPath = path.join(resolvedPath, "dist");
|
|
275
|
+
const declarationFiles = await listDeclarationFiles(distPath);
|
|
276
|
+
const declarations = [];
|
|
277
|
+
for (const filePath of declarationFiles) {
|
|
278
|
+
declarations.push({ filePath, source: await readFile(filePath, "utf8") });
|
|
279
|
+
}
|
|
280
|
+
const apiDeclaration = declarations
|
|
281
|
+
.map((declaration) => ({
|
|
282
|
+
...declaration,
|
|
283
|
+
values: parseObjectTypeFields(declaration.source, "OpenClawPluginApi", (value) => value.startsWith("register")),
|
|
284
|
+
}))
|
|
285
|
+
.sort((left, right) => right.values.length - left.values.length)[0];
|
|
286
|
+
const hookDeclaration = declarations.find((declaration) => declaration.source.includes("type PluginHookName ="));
|
|
287
|
+
const manifestDeclaration = declarations.find((declaration) => declaration.source.includes("type PluginManifestRecord ="));
|
|
288
|
+
const manifestContractDeclaration = declarations.find((declaration) => declaration.source.includes("type PluginManifestContracts ="));
|
|
289
|
+
const apiRegistrars = apiDeclaration?.values ?? [];
|
|
290
|
+
const hookNames = hookDeclaration ? parseStringUnion(hookDeclaration.source, "PluginHookName") : [];
|
|
291
|
+
const manifestFields = manifestDeclaration
|
|
292
|
+
? parseObjectTypeFields(manifestDeclaration.source, "PluginManifestRecord")
|
|
293
|
+
: [];
|
|
294
|
+
const manifestContractFields = manifestContractDeclaration
|
|
295
|
+
? parseObjectTypeFields(manifestContractDeclaration.source, "PluginManifestContracts")
|
|
296
|
+
: [];
|
|
297
|
+
const sdkExports = parsePluginSdkExports(packageJson);
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
configuredPath: requestedPath,
|
|
301
|
+
searchedPaths: requestedPaths,
|
|
302
|
+
status: "ok",
|
|
303
|
+
version: packageJson.version ?? null,
|
|
304
|
+
compatRegistryPath: null,
|
|
305
|
+
compatRecordCount: 0,
|
|
306
|
+
compatRecords: [],
|
|
307
|
+
compatRecordStatuses: {},
|
|
308
|
+
hookTypesPath: hookDeclaration ? relativePath(rootDir, hookDeclaration.filePath) : null,
|
|
309
|
+
hookNameCount: hookNames.length,
|
|
310
|
+
hookNames,
|
|
311
|
+
apiBuilderPath: apiDeclaration ? relativePath(rootDir, apiDeclaration.filePath) : null,
|
|
312
|
+
apiRegistrarCount: apiRegistrars.length,
|
|
313
|
+
apiRegistrars,
|
|
314
|
+
capturedRegistrationPath: apiDeclaration ? relativePath(rootDir, apiDeclaration.filePath) : null,
|
|
315
|
+
capturedRegistrarCount: apiRegistrars.length,
|
|
316
|
+
capturedRegistrars: apiRegistrars,
|
|
317
|
+
packagePath: relativePath(rootDir, packagePath),
|
|
318
|
+
sdkExportCount: sdkExports.length,
|
|
319
|
+
sdkExports,
|
|
320
|
+
pluginSdkEntrypointsPath: null,
|
|
321
|
+
reservedSdkExportCount: 0,
|
|
322
|
+
reservedSdkExports: [],
|
|
323
|
+
supportedFacadeSdkExports: [],
|
|
324
|
+
publicPluginOwnedSdkExports: [],
|
|
325
|
+
manifestTypesPath: manifestDeclaration ? relativePath(rootDir, manifestDeclaration.filePath) : null,
|
|
326
|
+
manifestFieldCount: manifestFields.length,
|
|
327
|
+
manifestFields,
|
|
328
|
+
manifestContractFieldCount: manifestContractFields.length,
|
|
329
|
+
manifestContractFields,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function listDeclarationFiles(rootDir) {
|
|
334
|
+
const files = [];
|
|
335
|
+
const entries = await readdir(rootDir, { withFileTypes: true });
|
|
336
|
+
for (const entry of entries) {
|
|
337
|
+
const entryPath = path.join(rootDir, entry.name);
|
|
338
|
+
if (entry.isDirectory()) {
|
|
339
|
+
files.push(...(await listDeclarationFiles(entryPath)));
|
|
340
|
+
} else if (entry.isFile() && entry.name.endsWith(".d.ts")) {
|
|
341
|
+
files.push(entryPath);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return files.sort();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function parseObjectTypeFields(source, typeName, filter = () => true) {
|
|
348
|
+
const body = readObjectTypeBody(source, typeName);
|
|
349
|
+
if (!body) return [];
|
|
350
|
+
return unique(parseTopLevelTypeProperties(body).filter(filter)).sort();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function parseTopLevelTypeProperties(body) {
|
|
354
|
+
const properties = [];
|
|
355
|
+
let braceDepth = 0;
|
|
356
|
+
let bracketDepth = 0;
|
|
357
|
+
let parenDepth = 0;
|
|
358
|
+
let propertyStart = true;
|
|
359
|
+
|
|
360
|
+
for (let index = 0; index < body.length; index += 1) {
|
|
361
|
+
const char = body[index];
|
|
362
|
+
const next = body[index + 1];
|
|
363
|
+
if (char === "/" && next === "*") {
|
|
364
|
+
index = body.indexOf("*/", index + 2);
|
|
365
|
+
if (index === -1) break;
|
|
366
|
+
index += 1;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
if (char === "/" && next === "/") {
|
|
370
|
+
const newline = body.indexOf("\n", index + 2);
|
|
371
|
+
if (newline === -1) break;
|
|
372
|
+
index = newline;
|
|
373
|
+
continue;
|
|
242
374
|
}
|
|
375
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
376
|
+
index = skipQuotedTypeText(body, index);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (char === "{") braceDepth += 1;
|
|
380
|
+
else if (char === "}") braceDepth -= 1;
|
|
381
|
+
else if (char === "[") bracketDepth += 1;
|
|
382
|
+
else if (char === "]") bracketDepth -= 1;
|
|
383
|
+
else if (char === "(") parenDepth += 1;
|
|
384
|
+
else if (char === ")") parenDepth -= 1;
|
|
385
|
+
|
|
386
|
+
if (braceDepth !== 0 || bracketDepth !== 0 || parenDepth !== 0) continue;
|
|
387
|
+
if (char === ";" || char === ",") {
|
|
388
|
+
propertyStart = true;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
if (/\s/.test(char)) continue;
|
|
392
|
+
if (!propertyStart || !/[A-Za-z_$]/.test(char)) {
|
|
393
|
+
propertyStart = false;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const match = body.slice(index).match(/^([A-Za-z_$][A-Za-z0-9_$]*)/);
|
|
398
|
+
if (!match) {
|
|
399
|
+
propertyStart = false;
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
const name = match[1];
|
|
403
|
+
index += name.length - 1;
|
|
404
|
+
if (name === "readonly") continue;
|
|
405
|
+
let cursor = index + 1;
|
|
406
|
+
while (/\s/.test(body[cursor] ?? "")) cursor += 1;
|
|
407
|
+
if (body[cursor] === "?") cursor += 1;
|
|
408
|
+
while (/\s/.test(body[cursor] ?? "")) cursor += 1;
|
|
409
|
+
if (body[cursor] === ":") properties.push(name);
|
|
410
|
+
propertyStart = false;
|
|
411
|
+
}
|
|
412
|
+
return properties;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function skipQuotedTypeText(source, quoteIndex) {
|
|
416
|
+
const quote = source[quoteIndex];
|
|
417
|
+
for (let index = quoteIndex + 1; index < source.length; index += 1) {
|
|
418
|
+
if (source[index] === "\\") index += 1;
|
|
419
|
+
else if (source[index] === quote) return index;
|
|
420
|
+
}
|
|
421
|
+
return source.length - 1;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function readObjectTypeBody(source, typeName) {
|
|
425
|
+
const marker = new RegExp(`(?:export\\s+)?type\\s+${typeName}(?:\\$\\d+)?\\s*=\\s*\\{`, "g");
|
|
426
|
+
const match = marker.exec(source);
|
|
427
|
+
if (!match) return null;
|
|
428
|
+
const start = match.index + match[0].length;
|
|
429
|
+
let depth = 1;
|
|
430
|
+
for (let index = start; index < source.length; index += 1) {
|
|
431
|
+
if (source[index] === "{") depth += 1;
|
|
432
|
+
if (source[index] === "}") depth -= 1;
|
|
433
|
+
if (depth === 0) return source.slice(start, index);
|
|
243
434
|
}
|
|
244
435
|
return null;
|
|
245
436
|
}
|
|
246
437
|
|
|
438
|
+
function parseStringUnion(source, typeName) {
|
|
439
|
+
const match = source.match(new RegExp(`type\\s+${typeName}\\s*=\\s*([^;]+);`));
|
|
440
|
+
return match ? unique([...match[1].matchAll(/["']([^"']+)["']/g)].map((item) => item[1])).sort() : [];
|
|
441
|
+
}
|
|
442
|
+
|
|
247
443
|
function emptyTargetSurface({ configuredPath, searchedPaths = undefined, status }) {
|
|
248
444
|
return {
|
|
249
445
|
configuredPath,
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import semver from "semver";
|
|
7
|
+
import { x as extractTar } from "tar";
|
|
8
|
+
import { readOpenClawTargetSurface } from "./openclaw-target.js";
|
|
9
|
+
|
|
10
|
+
const defaultRegistryUrl = "https://registry.npmjs.org";
|
|
11
|
+
const supportedTags = new Set(["latest", "beta"]);
|
|
12
|
+
const downloadUrls = new WeakMap();
|
|
13
|
+
|
|
14
|
+
export async function resolveOpenClawTargetVersion(requestedVersion, options = {}) {
|
|
15
|
+
const requested = requestedVersion ?? "latest";
|
|
16
|
+
if (typeof requested !== "string" || requested.trim().length === 0) {
|
|
17
|
+
throw new Error("OpenClaw target version must be latest, beta, or an exact version");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const registryUrl = normalizeRegistryUrl(
|
|
21
|
+
options.registryUrl ?? process.env.PLUGIN_INSPECTOR_NPM_REGISTRY ?? defaultRegistryUrl,
|
|
22
|
+
);
|
|
23
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
24
|
+
let version = requested;
|
|
25
|
+
let distTag = null;
|
|
26
|
+
|
|
27
|
+
if (supportedTags.has(requested)) {
|
|
28
|
+
const metadata = await fetchJson(`${registryUrl}/openclaw`, fetchImpl);
|
|
29
|
+
version = metadata["dist-tags"]?.[requested];
|
|
30
|
+
if (typeof version !== "string" || version.length === 0) {
|
|
31
|
+
throw new Error(`OpenClaw npm dist-tag ${requested} did not resolve to an exact version`);
|
|
32
|
+
}
|
|
33
|
+
if (!isExactOpenClawVersion(version)) {
|
|
34
|
+
throw new Error(`OpenClaw npm dist-tag ${requested} did not resolve to a valid exact version`);
|
|
35
|
+
}
|
|
36
|
+
distTag = requested;
|
|
37
|
+
} else if (!isExactOpenClawVersion(requested)) {
|
|
38
|
+
throw new Error("--openclaw-version must be latest, beta, or an exact OpenClaw version");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const versionMetadata = await fetchJson(`${registryUrl}/openclaw/${encodeURIComponent(version)}`, fetchImpl);
|
|
42
|
+
if (versionMetadata.version !== version || typeof versionMetadata.dist?.tarball !== "string") {
|
|
43
|
+
throw new Error(`OpenClaw npm metadata for ${version} is incomplete`);
|
|
44
|
+
}
|
|
45
|
+
if (!hasVerifiableIntegrity(versionMetadata.dist)) {
|
|
46
|
+
throw new Error(`OpenClaw npm metadata for ${version} has no verifiable integrity metadata`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const resolvedTarget = {
|
|
50
|
+
requestedVersion: requested,
|
|
51
|
+
version,
|
|
52
|
+
eligibilityVersion: openClawEligibilityVersion(version),
|
|
53
|
+
source: {
|
|
54
|
+
type: "npm",
|
|
55
|
+
package: "openclaw",
|
|
56
|
+
registry: sanitizeUrlForReport(registryUrl),
|
|
57
|
+
distTag,
|
|
58
|
+
tarball: sanitizeUrlForReport(versionMetadata.dist.tarball),
|
|
59
|
+
integrity: versionMetadata.dist.integrity ?? null,
|
|
60
|
+
shasum: versionMetadata.dist.shasum ?? null,
|
|
61
|
+
repository: sanitizeRepositoryForReport(versionMetadata.repository),
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
downloadUrls.set(resolvedTarget, versionMetadata.dist.tarball);
|
|
65
|
+
return resolvedTarget;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function prepareOpenClawTarget(resolvedTarget, options = {}) {
|
|
69
|
+
if (!resolvedTarget?.version || !resolvedTarget?.source?.integrity && !resolvedTarget?.source?.shasum) {
|
|
70
|
+
throw new Error("prepareOpenClawTarget requires a resolved npm target");
|
|
71
|
+
}
|
|
72
|
+
if (!downloadUrls.has(resolvedTarget)) {
|
|
73
|
+
throw new Error("prepareOpenClawTarget requires the target object directly returned by resolveOpenClawTargetVersion");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const cacheDir = path.resolve(
|
|
77
|
+
options.cacheDir ??
|
|
78
|
+
process.env.PLUGIN_INSPECTOR_CACHE_DIR ??
|
|
79
|
+
path.join(process.env.XDG_CACHE_HOME ?? path.join(os.homedir(), ".cache"), "plugin-inspector"),
|
|
80
|
+
);
|
|
81
|
+
const cacheKey = cacheKeyFor(resolvedTarget);
|
|
82
|
+
const targetDir = path.join(cacheDir, "openclaw", cacheKey);
|
|
83
|
+
const packageDir = path.join(targetDir, "package");
|
|
84
|
+
let cacheHit = await isPreparedPackage(packageDir, resolvedTarget.version);
|
|
85
|
+
|
|
86
|
+
if (!cacheHit) {
|
|
87
|
+
await preparePackageArchive(resolvedTarget, { ...options, cacheDir, targetDir });
|
|
88
|
+
cacheHit = false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const surface = await readOpenClawTargetSurface({ rootDir: packageDir, configuredPath: "." });
|
|
92
|
+
if (surface.status !== "ok") {
|
|
93
|
+
throw new Error(`prepared OpenClaw ${resolvedTarget.version} package has no readable public plugin surface`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
...surface,
|
|
98
|
+
configuredPath: `npm:openclaw@${resolvedTarget.version}`,
|
|
99
|
+
searchedPaths: [`npm:openclaw@${resolvedTarget.version}`],
|
|
100
|
+
requestedVersion: resolvedTarget.requestedVersion,
|
|
101
|
+
version: resolvedTarget.version,
|
|
102
|
+
eligibilityVersion: resolvedTarget.eligibilityVersion,
|
|
103
|
+
source: resolvedTarget.source,
|
|
104
|
+
cache: { hit: cacheHit, key: cacheKey },
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function openClawEligibilityVersion(version) {
|
|
109
|
+
const parsed = semver.parse(version);
|
|
110
|
+
return parsed ? `${parsed.major}.${parsed.minor}.${parsed.patch}` : version;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function satisfiesOpenClawVersionRange(version, range) {
|
|
114
|
+
return Boolean(semver.valid(version) && semver.validRange(range) && semver.satisfies(version, range));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function satisfiesOpenClawCompatibilityRange({ targetVersion, eligibilityVersion, range }) {
|
|
118
|
+
try {
|
|
119
|
+
const target = semver.parse(targetVersion);
|
|
120
|
+
if (!target) return false;
|
|
121
|
+
return new semver.Range(range).set.some((comparators) => {
|
|
122
|
+
const branch = comparators.map((comparator) => comparator.value).filter(Boolean).join(" ") || "*";
|
|
123
|
+
if (semver.satisfies(targetVersion, branch)) return true;
|
|
124
|
+
const constrainsTargetPrerelease = comparators.some(
|
|
125
|
+
(comparator) =>
|
|
126
|
+
(comparator.semver?.prerelease?.length ?? 0) > 0 &&
|
|
127
|
+
comparator.semver.major === target.major &&
|
|
128
|
+
comparator.semver.minor === target.minor &&
|
|
129
|
+
comparator.semver.patch === target.patch,
|
|
130
|
+
);
|
|
131
|
+
return !constrainsTargetPrerelease && semver.satisfies(eligibilityVersion, branch);
|
|
132
|
+
});
|
|
133
|
+
} catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function preparePackageArchive(resolvedTarget, options) {
|
|
139
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
140
|
+
const response = await fetchImpl(downloadUrlFor(resolvedTarget));
|
|
141
|
+
if (!response.ok) {
|
|
142
|
+
throw new Error(`failed to download OpenClaw ${resolvedTarget.version}: HTTP ${response.status}`);
|
|
143
|
+
}
|
|
144
|
+
const archive = Buffer.from(await response.arrayBuffer());
|
|
145
|
+
verifyArchive(archive, resolvedTarget.source);
|
|
146
|
+
|
|
147
|
+
await mkdir(path.dirname(options.targetDir), { recursive: true });
|
|
148
|
+
const temporaryDir = await mkdtemp(path.join(path.dirname(options.targetDir), `.${path.basename(options.targetDir)}-`));
|
|
149
|
+
try {
|
|
150
|
+
const archivePath = path.join(temporaryDir, "openclaw.tgz");
|
|
151
|
+
await writeFile(archivePath, archive);
|
|
152
|
+
await extractTar({ cwd: temporaryDir, file: archivePath, strict: true });
|
|
153
|
+
const packageDir = path.join(temporaryDir, "package");
|
|
154
|
+
if (!(await isPreparedPackage(packageDir, resolvedTarget.version))) {
|
|
155
|
+
throw new Error(`downloaded OpenClaw ${resolvedTarget.version} archive has unexpected package metadata`);
|
|
156
|
+
}
|
|
157
|
+
await rm(archivePath, { force: true });
|
|
158
|
+
try {
|
|
159
|
+
await rename(temporaryDir, options.targetDir);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (error?.code !== "EEXIST" && error?.code !== "ENOTEMPTY") throw error;
|
|
162
|
+
}
|
|
163
|
+
} finally {
|
|
164
|
+
await rm(temporaryDir, { recursive: true, force: true });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function isPreparedPackage(packageDir, version) {
|
|
169
|
+
if (!existsSync(path.join(packageDir, "package.json"))) return false;
|
|
170
|
+
try {
|
|
171
|
+
const packageJson = JSON.parse(await readFile(path.join(packageDir, "package.json"), "utf8"));
|
|
172
|
+
return packageJson.name === "openclaw" && packageJson.version === version;
|
|
173
|
+
} catch {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function verifyArchive(archive, source) {
|
|
179
|
+
if (typeof source.integrity === "string" && source.integrity.startsWith("sha512-")) {
|
|
180
|
+
const actual = createHash("sha512").update(archive).digest("base64");
|
|
181
|
+
if (actual !== source.integrity.slice("sha512-".length)) throw new Error("OpenClaw npm archive failed integrity verification");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (typeof source.shasum === "string" && /^[a-f0-9]{40}$/i.test(source.shasum)) {
|
|
185
|
+
const actual = createHash("sha1").update(archive).digest("hex");
|
|
186
|
+
if (actual !== source.shasum.toLowerCase()) throw new Error("OpenClaw npm archive failed shasum verification");
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
throw new Error("OpenClaw npm archive has no supported integrity metadata");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function fetchJson(url, fetchImpl) {
|
|
193
|
+
const response = await fetchImpl(url, { headers: { accept: "application/json" } });
|
|
194
|
+
if (!response.ok) throw new Error(`failed to resolve OpenClaw npm metadata: HTTP ${response.status}`);
|
|
195
|
+
return response.json();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function cacheKeyFor(target) {
|
|
199
|
+
const identity = target.source.integrity ?? target.source.shasum ?? target.source.tarball;
|
|
200
|
+
const digest = createHash("sha256").update(String(identity)).digest("hex").slice(0, 12);
|
|
201
|
+
return `${target.version}-${digest}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function downloadUrlFor(target) {
|
|
205
|
+
return downloadUrls.get(target) ?? null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function hasVerifiableIntegrity(dist) {
|
|
209
|
+
return (
|
|
210
|
+
(typeof dist.integrity === "string" && /^sha512-[A-Za-z0-9+/]+=*$/.test(dist.integrity)) ||
|
|
211
|
+
(typeof dist.shasum === "string" && /^[a-f0-9]{40}$/i.test(dist.shasum))
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function isExactOpenClawVersion(value) {
|
|
216
|
+
return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(value) && semver.valid(value) === value;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function normalizeRegistryUrl(value) {
|
|
220
|
+
const url = String(value);
|
|
221
|
+
let end = url.length;
|
|
222
|
+
while (end > 0 && url.charCodeAt(end - 1) === 47) end -= 1;
|
|
223
|
+
return url.slice(0, end);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function sanitizeUrlForReport(value) {
|
|
227
|
+
try {
|
|
228
|
+
const url = new URL(value);
|
|
229
|
+
url.username = "";
|
|
230
|
+
url.password = "";
|
|
231
|
+
url.search = "";
|
|
232
|
+
url.hash = "";
|
|
233
|
+
return url.toString().replace(/\/$/, "");
|
|
234
|
+
} catch {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function sanitizeRepositoryForReport(repository) {
|
|
240
|
+
if (typeof repository === "string") return sanitizeUrlForReport(repository);
|
|
241
|
+
if (!repository || typeof repository !== "object") return null;
|
|
242
|
+
return {
|
|
243
|
+
...repository,
|
|
244
|
+
url: typeof repository.url === "string" ? sanitizeUrlForReport(repository.url) : null,
|
|
245
|
+
};
|
|
246
|
+
}
|
package/src/report.js
CHANGED
|
@@ -110,6 +110,7 @@ export async function buildCompatibilityReport(options = {}) {
|
|
|
110
110
|
fixtureReport,
|
|
111
111
|
targetOpenClaw,
|
|
112
112
|
});
|
|
113
|
+
breakages.push(...fixtureClassification.breakages);
|
|
113
114
|
warnings.push(...fixtureClassification.warnings);
|
|
114
115
|
suggestions.push(...fixtureClassification.suggestions);
|
|
115
116
|
logs.push(...fixtureClassification.logs);
|