@openclaw/plugin-inspector 0.0.0 → 0.1.1
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 +21 -0
- package/LICENSE +21 -0
- package/README.md +151 -2
- package/examples/github-actions-plugin-inspector.yml +24 -0
- package/examples/plugin-inspector.config.json +15 -0
- package/package.json +56 -3
- package/src/advanced.js +186 -0
- package/src/api.js +85 -0
- package/src/artifacts.js +113 -0
- package/src/capture-api.js +115 -0
- package/src/ci-policy.js +259 -0
- package/src/ci-summary.js +223 -0
- package/src/cli.js +113 -0
- package/src/cold-import-readiness.js +271 -0
- package/src/compatibility-report.js +356 -0
- package/src/config.js +182 -0
- package/src/contract-capture.js +288 -0
- package/src/contract-coverage.js +167 -0
- package/src/contract-probes.js +156 -0
- package/src/execution-results.js +297 -0
- package/src/fixture-summary.js +780 -0
- package/src/import-loop-profile.js +169 -0
- package/src/index.js +10 -0
- package/src/inspector.js +405 -0
- package/src/issues.js +366 -0
- package/src/json-file.js +10 -0
- package/src/mock-sdk-capture-runner.js +69 -0
- package/src/openclaw-target.js +179 -0
- package/src/path-utils.js +28 -0
- package/src/platform-probes.js +238 -0
- package/src/process-profile.js +117 -0
- package/src/profile-diff.js +222 -0
- package/src/ref-diff.js +335 -0
- package/src/report.js +435 -0
- package/src/runtime-capture-report.js +134 -0
- package/src/runtime-profile.js +289 -0
- package/src/sdk-mock.js +61 -0
- package/src/stats.js +13 -0
- package/src/synthetic-probes.js +544 -0
- package/src/workspace-plan.js +496 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
renderTextSummary,
|
|
4
|
+
runPluginCheck,
|
|
5
|
+
} from "./index.js";
|
|
6
|
+
import {
|
|
7
|
+
captureEntrypoint,
|
|
8
|
+
inspectFixtureSet,
|
|
9
|
+
loadInspectorConfig,
|
|
10
|
+
writeArtifacts,
|
|
11
|
+
writeReport,
|
|
12
|
+
} from "./advanced.js";
|
|
13
|
+
|
|
14
|
+
const args = process.argv.slice(2);
|
|
15
|
+
const command = args[0]?.startsWith("-") ? "check" : (args[0] ?? "check");
|
|
16
|
+
const commandArgs = args[0]?.startsWith("-") ? args : args.slice(1);
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
20
|
+
printHelp();
|
|
21
|
+
} else if (command === "check") {
|
|
22
|
+
await runCheck(commandArgs);
|
|
23
|
+
} else if (command === "inspect" || command === "report" || command === "ci") {
|
|
24
|
+
await runReport(command, commandArgs);
|
|
25
|
+
} else if (command === "capture") {
|
|
26
|
+
await runCapture(commandArgs);
|
|
27
|
+
} else {
|
|
28
|
+
throw new Error(`unknown command: ${command}`);
|
|
29
|
+
}
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error(error.message);
|
|
32
|
+
process.exitCode = 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function runCheck(commandArgs) {
|
|
36
|
+
const configPath = readFlag(commandArgs, "--config");
|
|
37
|
+
const outDir = readFlag(commandArgs, "--out") ?? "reports";
|
|
38
|
+
const openclawPath = commandArgs.includes("--no-openclaw") ? false : readFlag(commandArgs, "--openclaw");
|
|
39
|
+
const json = commandArgs.includes("--json");
|
|
40
|
+
const capture = commandArgs.includes("--capture");
|
|
41
|
+
const { report } = await runPluginCheck({ configPath, outDir, openclawPath, capture });
|
|
42
|
+
|
|
43
|
+
if (json) {
|
|
44
|
+
console.log(JSON.stringify(report, null, 2));
|
|
45
|
+
} else {
|
|
46
|
+
console.log(renderTextSummary(report));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (report.status !== "pass") {
|
|
50
|
+
throw new Error(`plugin-inspector found ${report.summary.breakageCount} breakages`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function runReport(command, commandArgs) {
|
|
55
|
+
const configPath = readFlag(commandArgs, "--config");
|
|
56
|
+
const outDir = readFlag(commandArgs, "--out") ?? "reports";
|
|
57
|
+
const check = commandArgs.includes("--check") || command === "ci";
|
|
58
|
+
const json = commandArgs.includes("--json");
|
|
59
|
+
const config = await loadInspectorConfig(configPath);
|
|
60
|
+
const report = await inspectFixtureSet(config);
|
|
61
|
+
await writeReport(report, { outDir });
|
|
62
|
+
|
|
63
|
+
if (json) {
|
|
64
|
+
console.log(JSON.stringify(report, null, 2));
|
|
65
|
+
} else {
|
|
66
|
+
console.log(renderTextSummary(report));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (check && report.status !== "pass") {
|
|
70
|
+
throw new Error(`plugin-inspector found ${report.summary.breakageCount} breakages`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function runCapture(commandArgs) {
|
|
75
|
+
const entrypoint = commandArgs.find((arg) => !arg.startsWith("-"));
|
|
76
|
+
const outputPath = readFlag(commandArgs, "--output");
|
|
77
|
+
const pluginRoot = readFlag(commandArgs, "--plugin-root");
|
|
78
|
+
const mockSdk = commandArgs.includes("--mock-sdk");
|
|
79
|
+
if (!entrypoint) {
|
|
80
|
+
throw new Error("capture requires an entrypoint path");
|
|
81
|
+
}
|
|
82
|
+
if (process.env.PLUGIN_INSPECTOR_EXECUTE_ISOLATED !== "1") {
|
|
83
|
+
throw new Error("capture imports plugin code; rerun with PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 in an isolated workspace");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const result = await captureEntrypoint(entrypoint, { mockSdk, pluginRoot });
|
|
87
|
+
const json = `${JSON.stringify(result, null, 2)}\n`;
|
|
88
|
+
if (outputPath) {
|
|
89
|
+
await writeArtifacts([{ path: outputPath, content: json }]);
|
|
90
|
+
} else {
|
|
91
|
+
process.stdout.write(json);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function readFlag(commandArgs, name) {
|
|
96
|
+
const index = commandArgs.indexOf(name);
|
|
97
|
+
if (index === -1) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return commandArgs[index + 1] ?? null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function printHelp() {
|
|
104
|
+
console.log(`plugin-inspector
|
|
105
|
+
|
|
106
|
+
Usage:
|
|
107
|
+
plugin-inspector check [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--capture] [--json]
|
|
108
|
+
plugin-inspector report --config <path> [--out <dir>] [--check] [--json]
|
|
109
|
+
plugin-inspector inspect --config <path> [--out <dir>] [--check] [--json]
|
|
110
|
+
plugin-inspector ci --config <path> [--out <dir>]
|
|
111
|
+
PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1 plugin-inspector capture <entrypoint> [--mock-sdk] [--plugin-root <path>] [--output <path>]
|
|
112
|
+
`);
|
|
113
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { renderPaddedMarkdownTable, writeJsonMarkdownArtifacts } from "./artifacts.js";
|
|
4
|
+
import { slugForArtifact } from "./path-utils.js";
|
|
5
|
+
|
|
6
|
+
export function buildColdImportReadiness(options = {}) {
|
|
7
|
+
const report = options.report;
|
|
8
|
+
if (!report) {
|
|
9
|
+
throw new TypeError("buildColdImportReadiness requires a compatibility report");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const rootDir = path.resolve(options.rootDir ?? process.cwd());
|
|
13
|
+
const sdkExports = new Set(report.targetOpenClaw.sdkExports ?? []);
|
|
14
|
+
const fixtures = [];
|
|
15
|
+
|
|
16
|
+
for (const fixture of report.fixtures) {
|
|
17
|
+
const sdkBlockers = (fixture.sdkImportDetails ?? [])
|
|
18
|
+
.filter((sdkImport) => !sdkExports.has(sdkImport.specifier))
|
|
19
|
+
.map((sdkImport) => `${sdkImport.specifier} @ ${sdkImport.ref}`);
|
|
20
|
+
const entrypoints = (fixture.packages ?? []).flatMap((packageSummary) =>
|
|
21
|
+
(packageSummary.openclaw?.entrypoints ?? []).map((entrypoint) =>
|
|
22
|
+
classifyEntrypointReadiness({
|
|
23
|
+
fixture,
|
|
24
|
+
packageSummary,
|
|
25
|
+
entrypoint,
|
|
26
|
+
rootDir,
|
|
27
|
+
sdkBlockers,
|
|
28
|
+
}),
|
|
29
|
+
),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
fixtures.push({
|
|
33
|
+
id: fixture.id,
|
|
34
|
+
priority: fixture.priority,
|
|
35
|
+
entrypoints,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const allEntrypoints = fixtures.flatMap((fixture) => fixture.entrypoints);
|
|
40
|
+
return {
|
|
41
|
+
generatedAt: report.generatedAt,
|
|
42
|
+
targetOpenClaw: {
|
|
43
|
+
status: report.targetOpenClaw.status,
|
|
44
|
+
configuredPath: report.targetOpenClaw.configuredPath,
|
|
45
|
+
sdkExportCount: report.targetOpenClaw.sdkExportCount ?? 0,
|
|
46
|
+
},
|
|
47
|
+
summary: {
|
|
48
|
+
fixtureCount: fixtures.length,
|
|
49
|
+
entrypointCount: allEntrypoints.length,
|
|
50
|
+
readyCount: allEntrypoints.filter((entrypoint) => entrypoint.status === "ready").length,
|
|
51
|
+
blockedCount: allEntrypoints.filter((entrypoint) => entrypoint.status !== "ready").length,
|
|
52
|
+
tsLoaderRequiredCount: allEntrypoints.filter((entrypoint) => entrypoint.status === "ts-loader-required").length,
|
|
53
|
+
buildRequiredCount: allEntrypoints.filter((entrypoint) => entrypoint.status === "build-required").length,
|
|
54
|
+
dependencyInstallRequiredCount: allEntrypoints.filter((entrypoint) =>
|
|
55
|
+
entrypoint.blockers.some((blocker) => blocker.code === "dependency-install-required"),
|
|
56
|
+
).length,
|
|
57
|
+
sdkAliasRequiredCount: allEntrypoints.filter((entrypoint) =>
|
|
58
|
+
entrypoint.blockers.some((blocker) => blocker.code === "sdk-alias-required"),
|
|
59
|
+
).length,
|
|
60
|
+
},
|
|
61
|
+
fixtures,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function validateColdImportReadiness(readiness) {
|
|
66
|
+
const errors = [];
|
|
67
|
+
|
|
68
|
+
for (const fixture of readiness.fixtures) {
|
|
69
|
+
for (const entrypoint of fixture.entrypoints) {
|
|
70
|
+
if (!entrypoint.id || !entrypoint.path) {
|
|
71
|
+
errors.push(`${fixture.id}: entrypoint is missing id or path`);
|
|
72
|
+
}
|
|
73
|
+
if (!entrypoint.status) {
|
|
74
|
+
errors.push(`${entrypoint.id}: missing readiness status`);
|
|
75
|
+
}
|
|
76
|
+
if (!Array.isArray(entrypoint.assertions) || entrypoint.assertions.length === 0) {
|
|
77
|
+
errors.push(`${entrypoint.id}: missing cold-import assertions`);
|
|
78
|
+
}
|
|
79
|
+
if (entrypoint.status !== "ready" && entrypoint.blockers.length === 0) {
|
|
80
|
+
errors.push(`${entrypoint.id}: blocked entrypoint has no blockers`);
|
|
81
|
+
}
|
|
82
|
+
for (const blocker of entrypoint.blockers) {
|
|
83
|
+
if (!blocker.code || !blocker.evidence) {
|
|
84
|
+
errors.push(`${entrypoint.id}: blocker is missing code or evidence`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return errors;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function writeColdImportReadiness(readiness, options = {}) {
|
|
94
|
+
return writeJsonMarkdownArtifacts({
|
|
95
|
+
jsonPath: options.jsonPath,
|
|
96
|
+
markdownPath: options.markdownPath,
|
|
97
|
+
json: readiness,
|
|
98
|
+
markdown: renderColdImportReadinessMarkdown(readiness, options),
|
|
99
|
+
check: options.check,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function renderColdImportReadinessMarkdown(readiness, options = {}) {
|
|
104
|
+
return [
|
|
105
|
+
`# ${options.title ?? "Plugin Inspector Cold Import Readiness"}`,
|
|
106
|
+
"",
|
|
107
|
+
`Generated: ${readiness.generatedAt}`,
|
|
108
|
+
"",
|
|
109
|
+
"## Summary",
|
|
110
|
+
"",
|
|
111
|
+
markdownTable(
|
|
112
|
+
[
|
|
113
|
+
["Fixtures", readiness.summary.fixtureCount],
|
|
114
|
+
["Entrypoints", readiness.summary.entrypointCount],
|
|
115
|
+
["Ready", readiness.summary.readyCount],
|
|
116
|
+
["Blocked", readiness.summary.blockedCount],
|
|
117
|
+
["TypeScript loader required", readiness.summary.tsLoaderRequiredCount],
|
|
118
|
+
["Build required", readiness.summary.buildRequiredCount],
|
|
119
|
+
["Dependency install required", readiness.summary.dependencyInstallRequiredCount],
|
|
120
|
+
["SDK alias required", readiness.summary.sdkAliasRequiredCount],
|
|
121
|
+
],
|
|
122
|
+
["Metric", "Value"],
|
|
123
|
+
),
|
|
124
|
+
"",
|
|
125
|
+
"## Entrypoints",
|
|
126
|
+
"",
|
|
127
|
+
markdownTable(
|
|
128
|
+
readiness.fixtures.flatMap((fixture) =>
|
|
129
|
+
fixture.entrypoints.map((entrypoint) => [
|
|
130
|
+
fixture.id,
|
|
131
|
+
entrypoint.kind,
|
|
132
|
+
entrypoint.status,
|
|
133
|
+
entrypoint.path,
|
|
134
|
+
entrypoint.blockers.map((blocker) => blocker.code).join(", ") || "-",
|
|
135
|
+
entrypoint.assertions.join("; "),
|
|
136
|
+
]),
|
|
137
|
+
),
|
|
138
|
+
["Fixture", "Kind", "Status", "Path", "Blockers", "Assertions"],
|
|
139
|
+
),
|
|
140
|
+
].join("\n");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function classifyEntrypointReadiness({ fixture, packageSummary, entrypoint, rootDir, sdkBlockers }) {
|
|
144
|
+
const blockers = [];
|
|
145
|
+
const resolvedPath = path.resolve(rootDir, entrypoint.relativePath);
|
|
146
|
+
const extension = path.extname(entrypoint.relativePath);
|
|
147
|
+
|
|
148
|
+
if (!entrypoint.exists) {
|
|
149
|
+
blockers.push({
|
|
150
|
+
code: entrypoint.requiresBuild ? "build-required" : "missing-entrypoint",
|
|
151
|
+
message: entrypoint.requiresBuild
|
|
152
|
+
? "entrypoint points at build output that is absent in the fixture checkout"
|
|
153
|
+
: "entrypoint path is missing in the fixture checkout",
|
|
154
|
+
evidence: entrypoint.relativePath,
|
|
155
|
+
});
|
|
156
|
+
} else if (extension === ".ts" || extension === ".tsx") {
|
|
157
|
+
blockers.push({
|
|
158
|
+
code: "ts-loader-required",
|
|
159
|
+
message: "entrypoint is TypeScript source and needs a loader or build step before Node cold import",
|
|
160
|
+
evidence: entrypoint.relativePath,
|
|
161
|
+
});
|
|
162
|
+
} else if (![".js", ".mjs", ".cjs"].includes(extension)) {
|
|
163
|
+
blockers.push({
|
|
164
|
+
code: "unknown-entrypoint-extension",
|
|
165
|
+
message: "entrypoint extension is not directly importable by the default Node runner",
|
|
166
|
+
evidence: entrypoint.relativePath,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (entrypoint.exists && existsSync(resolvedPath)) {
|
|
171
|
+
const source = readSourcePreviewSync(resolvedPath);
|
|
172
|
+
if (source && /\b(process\.env|spawn\(|execFile\(|exec\(|fetch\(|WebSocket\b)/.test(source)) {
|
|
173
|
+
blockers.push({
|
|
174
|
+
code: "top-level-side-effect-review",
|
|
175
|
+
message: "entrypoint source contains side-effect-prone tokens that cold import must sandbox or review",
|
|
176
|
+
evidence: entrypoint.relativePath,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const runtimeDependencies = unique([
|
|
182
|
+
...(packageSummary.dependencies ?? []),
|
|
183
|
+
...(packageSummary.peerDependencies ?? []),
|
|
184
|
+
...(packageSummary.optionalDependencies ?? []),
|
|
185
|
+
]);
|
|
186
|
+
if (entrypoint.exists && runtimeDependencies.length > 0) {
|
|
187
|
+
blockers.push({
|
|
188
|
+
code: "dependency-install-required",
|
|
189
|
+
message: "package declares runtime dependencies that must be installed before cold import",
|
|
190
|
+
evidence: runtimeDependencies.join(", "),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
for (const sdkBlocker of sdkBlockers) {
|
|
195
|
+
blockers.push({
|
|
196
|
+
code: "sdk-alias-required",
|
|
197
|
+
message: "fixture imports an SDK alias missing from target OpenClaw package exports",
|
|
198
|
+
evidence: sdkBlocker,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
id: `cold-import.${entrypoint.kind}:${fixture.id}:${slugForArtifact(entrypoint.relativePath)}`,
|
|
204
|
+
fixture: fixture.id,
|
|
205
|
+
packagePath: packageSummary.path,
|
|
206
|
+
kind: entrypoint.kind,
|
|
207
|
+
specifier: entrypoint.specifier,
|
|
208
|
+
path: entrypoint.relativePath,
|
|
209
|
+
status: readinessStatus(blockers),
|
|
210
|
+
blockers,
|
|
211
|
+
assertions: coldImportAssertions(blockers),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function readSourcePreviewSync(filePath) {
|
|
216
|
+
try {
|
|
217
|
+
return existsSync(filePath) ? readFileSync(filePath, "utf8").slice(0, 20000) : "";
|
|
218
|
+
} catch {
|
|
219
|
+
return "";
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function readinessStatus(blockers) {
|
|
224
|
+
if (blockers.length === 0) {
|
|
225
|
+
return "ready";
|
|
226
|
+
}
|
|
227
|
+
if (blockers.some((blocker) => blocker.code === "sdk-alias-required")) {
|
|
228
|
+
return "sdk-alias-required";
|
|
229
|
+
}
|
|
230
|
+
if (blockers.some((blocker) => blocker.code === "build-required")) {
|
|
231
|
+
return "build-required";
|
|
232
|
+
}
|
|
233
|
+
if (blockers.some((blocker) => blocker.code === "missing-entrypoint")) {
|
|
234
|
+
return "missing";
|
|
235
|
+
}
|
|
236
|
+
if (blockers.some((blocker) => blocker.code === "ts-loader-required")) {
|
|
237
|
+
return "ts-loader-required";
|
|
238
|
+
}
|
|
239
|
+
if (blockers.some((blocker) => blocker.code === "dependency-install-required")) {
|
|
240
|
+
return "dependency-install-required";
|
|
241
|
+
}
|
|
242
|
+
return "review-required";
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function coldImportAssertions(blockers) {
|
|
246
|
+
if (blockers.length === 0) {
|
|
247
|
+
return ["entrypoint can be imported by Node without fixture credentials", "registration capture shim receives plugin registrations"];
|
|
248
|
+
}
|
|
249
|
+
return blockers.map((blocker) => assertionForBlocker(blocker.code));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function assertionForBlocker(code) {
|
|
253
|
+
const assertions = {
|
|
254
|
+
"build-required": "plugin build or source alias resolution runs before cold import",
|
|
255
|
+
"dependency-install-required": "fixture dependencies are installed in an isolated workspace before cold import",
|
|
256
|
+
"missing-entrypoint": "plugin package metadata points at an existing OpenClaw entrypoint",
|
|
257
|
+
"sdk-alias-required": "target OpenClaw exports the imported SDK alias or provides a migration shim",
|
|
258
|
+
"top-level-side-effect-review": "cold import sandbox blocks network/process side effects before register capture",
|
|
259
|
+
"ts-loader-required": "TypeScript source entrypoint is compiled or loaded before cold import",
|
|
260
|
+
"unknown-entrypoint-extension": "entrypoint extension has an explicit loader",
|
|
261
|
+
};
|
|
262
|
+
return assertions[code] ?? "cold import blocker has a documented mitigation";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function unique(values) {
|
|
266
|
+
return [...new Set(values)];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function markdownTable(rows, headers) {
|
|
270
|
+
return renderPaddedMarkdownTable(rows, headers);
|
|
271
|
+
}
|