@openclaw/plugin-inspector 0.3.19 → 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 +13 -0
- package/README.md +11 -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/inspector.js +170 -11
- package/src/issues.js +4 -1
- package/src/openclaw-target.js +185 -2
- package/src/openclaw-version.js +246 -0
- package/src/report.js +1 -0
|
@@ -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);
|