@openclaw/plugin-inspector 0.3.11 → 0.3.12
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 +6 -0
- package/README.md +6 -2
- package/package.json +1 -1
- package/src/api.js +4 -0
- package/src/batch.js +297 -0
- package/src/cli.js +91 -5
- package/src/index.js +12 -0
- package/src/inspector.js +1 -0
- package/src/issues.js +4 -0
- package/src/report.js +22 -9
- package/src/sdk-mock.js +92 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.3.12 - 2026-06-09
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Hide maintainer-facing `inspector-gap` findings from author-facing `check`, `ci`, and `batch` output by default, with `--include-inspector-gaps` for internal coverage reports.
|
|
10
|
+
|
|
5
11
|
## 0.3.11 - 2026-05-26
|
|
6
12
|
|
|
7
13
|
### Fixed
|
package/README.md
CHANGED
|
@@ -170,6 +170,7 @@ Copy-ready examples live in:
|
|
|
170
170
|
| `plugin-inspector config` | Print resolved plugin-root config as text or JSON. |
|
|
171
171
|
| `plugin-inspector init` | Write starter config, scripts, and optional GitHub Actions workflow. |
|
|
172
172
|
| `plugin-inspector report` | Run a fixture-suite config with many plugins. |
|
|
173
|
+
| `plugin-inspector batch` | Discover plugin roots under a folder and write one aggregate impact report. |
|
|
173
174
|
| `plugin-inspector capture` | Runtime-capture one entrypoint directly. |
|
|
174
175
|
|
|
175
176
|
Common options:
|
|
@@ -186,6 +187,7 @@ Common options:
|
|
|
186
187
|
| `--mock-sdk` / `--sdk mock` | Use generated SDK and external-package mocks for runtime capture. |
|
|
187
188
|
| `--real-sdk` / `--sdk real` | Use installed real SDK dependencies instead of mocks. |
|
|
188
189
|
| `--allow-execute` | Permit commands that import plugin code. |
|
|
190
|
+
| `--include-inspector-gaps` | Include maintainer-facing scanner coverage gaps in `check`, `ci`, and `batch` output. |
|
|
189
191
|
| `--json` | Print machine-readable JSON to stdout. |
|
|
190
192
|
| `--sarif [path]` | Write SARIF from `check` or `inspect`; `ci` enables this by default. |
|
|
191
193
|
| `--junit [path]` | Write JUnit XML from `check` or `inspect`; `ci` enables this by default. |
|
|
@@ -305,8 +307,9 @@ Important report sections:
|
|
|
305
307
|
| `logs` | Informational inventory and coverage rows. |
|
|
306
308
|
| `decisions` | Maintainer-facing follow-up or compatibility-policy decisions. |
|
|
307
309
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
+
Author-facing `check`, `ci`, and `batch` output hides inspector gaps by default
|
|
311
|
+
because they are scanner coverage backlog, not plugin-author fixes. Pass
|
|
312
|
+
`--include-inspector-gaps` for Crabpot-style maintainer coverage reports.
|
|
310
313
|
|
|
311
314
|
## CI Policy And Shared Reporting Primitives
|
|
312
315
|
|
|
@@ -404,6 +407,7 @@ Stable grouped facades:
|
|
|
404
407
|
| `reports` | Render/write reports and classify issue findings. |
|
|
405
408
|
| `contracts` | Build, render, validate, and write contract captures and coverage. |
|
|
406
409
|
| `ci` | Build summaries, policy reports, execution results, SARIF, and JUnit outputs. |
|
|
410
|
+
| `batch` | Discover plugin roots and aggregate compatibility findings across a corpus. |
|
|
407
411
|
| `runtime` | Build runtime profiles, profile diffs, ref diffs, and import-loop profiles. |
|
|
408
412
|
| `synthetic` | Build and run synthetic probe plans. |
|
|
409
413
|
|
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -44,6 +44,7 @@ export async function inspectPluginRoot(options = {}) {
|
|
|
44
44
|
const config = await loadPluginConfig(options);
|
|
45
45
|
return inspectCompatibilityFixtureSet(config, {
|
|
46
46
|
generatedAt: options.generatedAt,
|
|
47
|
+
includeInspectorGaps: options.includeInspectorGaps,
|
|
47
48
|
openclawPath: options.openclawPath,
|
|
48
49
|
executionResults: options.executionResults,
|
|
49
50
|
targetOpenClaw: options.targetOpenClaw,
|
|
@@ -59,6 +60,7 @@ export async function inspectCompatibilityFixtureSetConfig(options = {}) {
|
|
|
59
60
|
const config = await loadFixtureSetConfig(options);
|
|
60
61
|
return inspectCompatibilityFixtureSet(config, {
|
|
61
62
|
generatedAt: options.generatedAt,
|
|
63
|
+
includeInspectorGaps: options.includeInspectorGaps,
|
|
62
64
|
openclawPath: options.openclawPath,
|
|
63
65
|
executionResults: options.executionResults,
|
|
64
66
|
targetOpenClaw: options.targetOpenClaw,
|
|
@@ -113,6 +115,7 @@ export async function buildFixtureSetColdImportReadiness(options = {}) {
|
|
|
113
115
|
options.report ??
|
|
114
116
|
(await inspectCompatibilityFixtureSet(config, {
|
|
115
117
|
generatedAt: options.generatedAt,
|
|
118
|
+
includeInspectorGaps: options.includeInspectorGaps,
|
|
116
119
|
openclawPath: options.openclawPath,
|
|
117
120
|
executionResults: options.executionResults,
|
|
118
121
|
targetOpenClaw: options.targetOpenClaw,
|
|
@@ -145,6 +148,7 @@ export async function buildFixtureSetWorkspacePlan(options = {}) {
|
|
|
145
148
|
options.report ??
|
|
146
149
|
(await inspectCompatibilityFixtureSet(config, {
|
|
147
150
|
generatedAt: options.generatedAt,
|
|
151
|
+
includeInspectorGaps: options.includeInspectorGaps,
|
|
148
152
|
openclawPath: options.openclawPath,
|
|
149
153
|
executionResults: options.executionResults,
|
|
150
154
|
targetOpenClaw: options.targetOpenClaw,
|
package/src/batch.js
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdtemp, readFile, readdir, rm } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { renderMarkdownTable, writeJsonMarkdownArtifacts } from "./artifacts.js";
|
|
6
|
+
import { runPluginCheck } from "./api.js";
|
|
7
|
+
|
|
8
|
+
const ignoredDirs = new Set([
|
|
9
|
+
".git",
|
|
10
|
+
".hg",
|
|
11
|
+
".svn",
|
|
12
|
+
"node_modules",
|
|
13
|
+
"reports",
|
|
14
|
+
"dist",
|
|
15
|
+
"build",
|
|
16
|
+
".plugin-inspector",
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
export async function runBatchAnalysis(options = {}) {
|
|
20
|
+
const rootDir = path.resolve(options.rootDir ?? options.inputDir ?? process.cwd());
|
|
21
|
+
const outDir = options.outDir ?? "reports";
|
|
22
|
+
const outRoot = path.resolve(rootDir, outDir);
|
|
23
|
+
const concurrency = Math.max(1, Math.min(Math.round(options.concurrency ?? 4), 32));
|
|
24
|
+
const keepPluginReports = options.keepPluginReports === true;
|
|
25
|
+
const pluginRoots = await discoverPluginRoots(rootDir);
|
|
26
|
+
const tempRoot = keepPluginReports ? null : await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-batch-"));
|
|
27
|
+
const entries = [];
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
await runWithConcurrency(pluginRoots, concurrency, async (pluginRoot) => {
|
|
31
|
+
const reportsRoot = keepPluginReports
|
|
32
|
+
? path.join(outRoot, "plugins", slugForPath(path.relative(rootDir, pluginRoot)))
|
|
33
|
+
: path.join(tempRoot, slugForPath(path.relative(rootDir, pluginRoot)));
|
|
34
|
+
entries.push(
|
|
35
|
+
await inspectBatchPlugin(pluginRoot, {
|
|
36
|
+
...options,
|
|
37
|
+
rootDir,
|
|
38
|
+
outDir: reportsRoot,
|
|
39
|
+
openclawPath: options.openclawPath,
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
} finally {
|
|
44
|
+
if (tempRoot) await rm(tempRoot, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
entries.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
48
|
+
const report = buildBatchReport({
|
|
49
|
+
rootDir,
|
|
50
|
+
entries,
|
|
51
|
+
generatedAt: options.generatedAt ?? new Date().toISOString(),
|
|
52
|
+
});
|
|
53
|
+
const paths = await writeBatchReport(report, { outDir: outRoot, check: options.checkArtifacts });
|
|
54
|
+
return { report, paths };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function discoverPluginRoots(rootDir) {
|
|
58
|
+
const roots = [];
|
|
59
|
+
await walk(path.resolve(rootDir));
|
|
60
|
+
roots.sort();
|
|
61
|
+
return roots;
|
|
62
|
+
|
|
63
|
+
async function walk(dir) {
|
|
64
|
+
if (await isPluginRoot(dir)) {
|
|
65
|
+
roots.push(dir);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
if (!entry.isDirectory() || ignoredDirs.has(entry.name)) continue;
|
|
71
|
+
await walk(path.join(dir, entry.name));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function writeBatchReport(report, options = {}) {
|
|
77
|
+
return writeJsonMarkdownArtifacts({
|
|
78
|
+
jsonPath: path.join(options.outDir ?? "reports", "plugin-inspector-batch-report.json"),
|
|
79
|
+
markdownPath: path.join(options.outDir ?? "reports", "plugin-inspector-batch-report.md"),
|
|
80
|
+
json: report,
|
|
81
|
+
markdown: renderBatchMarkdown(report),
|
|
82
|
+
check: options.check,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function buildBatchReport({ rootDir, entries, generatedAt }) {
|
|
87
|
+
const findingFrequency = findingFrequencyRows(entries);
|
|
88
|
+
const summary = {
|
|
89
|
+
pluginCount: entries.length,
|
|
90
|
+
passed: entries.filter((entry) => entry.status === "pass").length,
|
|
91
|
+
failed: entries.filter((entry) => entry.status !== "pass").length,
|
|
92
|
+
pluginsWithErrors: entries.filter((entry) => entry.errorCount > 0).length,
|
|
93
|
+
pluginsWithWarnings: entries.filter((entry) => entry.warningCount > 0).length,
|
|
94
|
+
errorCount: entries.reduce((sum, entry) => sum + entry.errorCount, 0),
|
|
95
|
+
warningCount: entries.reduce((sum, entry) => sum + entry.warningCount, 0),
|
|
96
|
+
findingCodeCount: findingFrequency.length,
|
|
97
|
+
};
|
|
98
|
+
return {
|
|
99
|
+
generatedAt,
|
|
100
|
+
rootDir,
|
|
101
|
+
summary,
|
|
102
|
+
findingFrequency,
|
|
103
|
+
plugins: entries,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function inspectBatchPlugin(pluginRoot, options) {
|
|
108
|
+
try {
|
|
109
|
+
const { report } = await runPluginCheck({
|
|
110
|
+
allowExecution: options.allowExecution,
|
|
111
|
+
capture: options.capture,
|
|
112
|
+
configPath: options.configPath,
|
|
113
|
+
includeInspectorGaps: options.includeInspectorGaps,
|
|
114
|
+
mockSdk: options.mockSdk,
|
|
115
|
+
openclawPath: options.openclawPath,
|
|
116
|
+
outDir: options.outDir,
|
|
117
|
+
pluginRoot,
|
|
118
|
+
});
|
|
119
|
+
const findings = normalizeReportFindings(report);
|
|
120
|
+
return {
|
|
121
|
+
pluginRoot,
|
|
122
|
+
relativePath: path.relative(options.rootDir ?? process.cwd(), pluginRoot) || ".",
|
|
123
|
+
status: report.status,
|
|
124
|
+
packageName: packageNameFromReport(report),
|
|
125
|
+
targetOpenClaw: report.targetOpenClaw,
|
|
126
|
+
errorCount: findings.filter((finding) => finding.kind === "error").length,
|
|
127
|
+
warningCount: findings.filter((finding) => finding.kind === "warning").length,
|
|
128
|
+
findings,
|
|
129
|
+
};
|
|
130
|
+
} catch (error) {
|
|
131
|
+
return {
|
|
132
|
+
pluginRoot,
|
|
133
|
+
relativePath: path.relative(options.rootDir ?? process.cwd(), pluginRoot) || ".",
|
|
134
|
+
status: "error",
|
|
135
|
+
packageName: path.basename(pluginRoot),
|
|
136
|
+
targetOpenClaw: null,
|
|
137
|
+
errorCount: 1,
|
|
138
|
+
warningCount: 0,
|
|
139
|
+
findings: [
|
|
140
|
+
{
|
|
141
|
+
kind: "error",
|
|
142
|
+
code: "plugin-inspector-batch-failure",
|
|
143
|
+
message: error instanceof Error ? error.message : String(error),
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function normalizeReportFindings(report) {
|
|
151
|
+
return [
|
|
152
|
+
...(report.breakages ?? []).map((finding) => normalizeFinding(finding, "error")),
|
|
153
|
+
...(report.issues ?? []).map((finding) =>
|
|
154
|
+
normalizeFinding(
|
|
155
|
+
finding,
|
|
156
|
+
finding.status === "blocking" || finding.severity === "P0" ? "error" : "warning",
|
|
157
|
+
),
|
|
158
|
+
),
|
|
159
|
+
...(report.warnings ?? []).map((finding) => normalizeFinding(finding, "warning")),
|
|
160
|
+
...(report.suggestions ?? []).map((finding) => normalizeFinding(finding, "warning")),
|
|
161
|
+
];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function normalizeFinding(finding, kind) {
|
|
165
|
+
return {
|
|
166
|
+
kind,
|
|
167
|
+
code: finding.code ?? "plugin-inspector-finding",
|
|
168
|
+
severity: finding.severity,
|
|
169
|
+
issueClass: finding.issueClass,
|
|
170
|
+
message: finding.message ?? finding.title ?? "See plugin report.",
|
|
171
|
+
evidence: finding.evidence,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function findingFrequencyRows(entries) {
|
|
176
|
+
const byCode = new Map();
|
|
177
|
+
for (const entry of entries) {
|
|
178
|
+
const seenForPlugin = new Set();
|
|
179
|
+
for (const finding of entry.findings) {
|
|
180
|
+
const current = byCode.get(finding.code) ?? {
|
|
181
|
+
code: finding.code,
|
|
182
|
+
count: 0,
|
|
183
|
+
plugins: 0,
|
|
184
|
+
errors: 0,
|
|
185
|
+
warnings: 0,
|
|
186
|
+
};
|
|
187
|
+
current.count += 1;
|
|
188
|
+
if (!seenForPlugin.has(finding.code)) {
|
|
189
|
+
current.plugins += 1;
|
|
190
|
+
seenForPlugin.add(finding.code);
|
|
191
|
+
}
|
|
192
|
+
if (finding.kind === "error") current.errors += 1;
|
|
193
|
+
else current.warnings += 1;
|
|
194
|
+
byCode.set(finding.code, current);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return [...byCode.values()].sort((a, b) => b.plugins - a.plugins || b.count - a.count);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function renderBatchMarkdown(report) {
|
|
201
|
+
return [
|
|
202
|
+
"# Plugin Inspector Batch Report",
|
|
203
|
+
"",
|
|
204
|
+
`Generated: ${report.generatedAt}`,
|
|
205
|
+
`Root: ${report.rootDir}`,
|
|
206
|
+
"",
|
|
207
|
+
"## Summary",
|
|
208
|
+
"",
|
|
209
|
+
renderMarkdownTable(
|
|
210
|
+
[
|
|
211
|
+
["Plugins", report.summary.pluginCount],
|
|
212
|
+
["Passed", report.summary.passed],
|
|
213
|
+
["Failed", report.summary.failed],
|
|
214
|
+
["Plugins with errors", report.summary.pluginsWithErrors],
|
|
215
|
+
["Plugins with warnings", report.summary.pluginsWithWarnings],
|
|
216
|
+
["Errors", report.summary.errorCount],
|
|
217
|
+
["Warnings", report.summary.warningCount],
|
|
218
|
+
],
|
|
219
|
+
["Metric", "Value"],
|
|
220
|
+
),
|
|
221
|
+
"",
|
|
222
|
+
"## Finding Frequency",
|
|
223
|
+
"",
|
|
224
|
+
report.findingFrequency.length
|
|
225
|
+
? renderMarkdownTable(
|
|
226
|
+
report.findingFrequency.map((row) => [
|
|
227
|
+
row.code,
|
|
228
|
+
row.plugins,
|
|
229
|
+
row.count,
|
|
230
|
+
row.errors,
|
|
231
|
+
row.warnings,
|
|
232
|
+
]),
|
|
233
|
+
["Code", "Plugins", "Findings", "Errors", "Warnings"],
|
|
234
|
+
)
|
|
235
|
+
: "_No findings._",
|
|
236
|
+
"",
|
|
237
|
+
"## Plugins",
|
|
238
|
+
"",
|
|
239
|
+
report.plugins.length
|
|
240
|
+
? renderMarkdownTable(
|
|
241
|
+
report.plugins.map((plugin) => [
|
|
242
|
+
plugin.packageName,
|
|
243
|
+
plugin.relativePath,
|
|
244
|
+
plugin.status,
|
|
245
|
+
plugin.errorCount,
|
|
246
|
+
plugin.warningCount,
|
|
247
|
+
]),
|
|
248
|
+
["Package", "Path", "Status", "Errors", "Warnings"],
|
|
249
|
+
)
|
|
250
|
+
: "_No plugin roots discovered._",
|
|
251
|
+
].join("\n");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function runWithConcurrency(items, concurrency, worker) {
|
|
255
|
+
let index = 0;
|
|
256
|
+
const runners = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
257
|
+
while (index < items.length) {
|
|
258
|
+
const item = items[index];
|
|
259
|
+
index += 1;
|
|
260
|
+
await worker(item);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
await Promise.all(runners);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function isPluginRoot(dir) {
|
|
267
|
+
if (existsSync(path.join(dir, "plugin-inspector.config.json"))) return true;
|
|
268
|
+
if (existsSync(path.join(dir, ".plugin-inspector.json"))) return true;
|
|
269
|
+
if (existsSync(path.join(dir, "openclaw.plugin.json"))) return true;
|
|
270
|
+
const packageJsonPath = path.join(dir, "package.json");
|
|
271
|
+
if (!existsSync(packageJsonPath)) return false;
|
|
272
|
+
try {
|
|
273
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
|
|
274
|
+
return Boolean(packageJson.openclaw || packageJson.pluginInspector || packageJson["plugin-inspector"]);
|
|
275
|
+
} catch {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function packageNameFromReport(report) {
|
|
281
|
+
return (
|
|
282
|
+
report.fixtures?.[0]?.package?.packageJson?.name ??
|
|
283
|
+
report.fixtures?.[0]?.package?.name ??
|
|
284
|
+
report.fixtures?.[0]?.name ??
|
|
285
|
+
report.fixtures?.[0]?.id ??
|
|
286
|
+
"plugin"
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function slugForPath(value) {
|
|
291
|
+
return (
|
|
292
|
+
String(value)
|
|
293
|
+
.replaceAll(path.sep, "-")
|
|
294
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
295
|
+
.replace(/^-+|-+$/g, "") || "plugin"
|
|
296
|
+
);
|
|
297
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import {
|
|
4
4
|
loadPluginConfig,
|
|
5
5
|
renderTextSummary,
|
|
6
|
+
runBatchAnalysis,
|
|
6
7
|
sanitizeReportArtifact,
|
|
7
8
|
runPluginCheck,
|
|
8
9
|
} from "./index.js";
|
|
@@ -43,6 +44,8 @@ try {
|
|
|
43
44
|
}
|
|
44
45
|
} else if (command === "ci") {
|
|
45
46
|
await runCi(commandArgs);
|
|
47
|
+
} else if (command === "batch") {
|
|
48
|
+
await runBatch(commandArgs);
|
|
46
49
|
} else if (command === "capture") {
|
|
47
50
|
await runCapture(commandArgs);
|
|
48
51
|
} else {
|
|
@@ -53,6 +56,38 @@ try {
|
|
|
53
56
|
process.exitCode = 1;
|
|
54
57
|
}
|
|
55
58
|
|
|
59
|
+
async function runBatch(commandArgs) {
|
|
60
|
+
const inputDir = readFirstPositional(commandArgs, new Set(["--out", "--openclaw", "--concurrency"]));
|
|
61
|
+
const outDir = readFlag(commandArgs, "--out") ?? "reports";
|
|
62
|
+
const openclawPath = commandArgs.includes("--no-openclaw") ? false : readFlag(commandArgs, "--openclaw");
|
|
63
|
+
const concurrency = Number(readFlag(commandArgs, "--concurrency") ?? "4");
|
|
64
|
+
const json = commandArgs.includes("--json");
|
|
65
|
+
const check = commandArgs.includes("--check");
|
|
66
|
+
const keepPluginReports = commandArgs.includes("--keep-plugin-reports");
|
|
67
|
+
const includeInspectorGaps = commandArgs.includes("--include-inspector-gaps");
|
|
68
|
+
if (!inputDir) {
|
|
69
|
+
throw new Error("batch requires a folder of plugin roots");
|
|
70
|
+
}
|
|
71
|
+
const { report, paths } = await runBatchAnalysis({
|
|
72
|
+
rootDir: inputDir,
|
|
73
|
+
outDir,
|
|
74
|
+
openclawPath,
|
|
75
|
+
concurrency,
|
|
76
|
+
includeInspectorGaps,
|
|
77
|
+
keepPluginReports,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (json) {
|
|
81
|
+
console.log(JSON.stringify(report, null, 2));
|
|
82
|
+
} else {
|
|
83
|
+
console.log(renderBatchTextSummary(report, paths));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (check && report.summary.pluginsWithErrors > 0) {
|
|
87
|
+
throw new Error(`plugin-inspector batch found ${report.summary.pluginsWithErrors} plugin(s) with errors`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
56
91
|
async function runConfig(commandArgs) {
|
|
57
92
|
const configPath = readFlag(commandArgs, "--config");
|
|
58
93
|
const pluginRoot = readFlag(commandArgs, "--plugin-root") ?? readFlag(commandArgs, "--root");
|
|
@@ -75,10 +110,12 @@ async function runCheck(commandArgs) {
|
|
|
75
110
|
const mockSdk = readMockSdkFlag(commandArgs);
|
|
76
111
|
const allowExecution = readAllowExecutionFlag(commandArgs);
|
|
77
112
|
const ciOutputs = readCiOutputFlags(commandArgs);
|
|
113
|
+
const includeInspectorGaps = commandArgs.includes("--include-inspector-gaps");
|
|
78
114
|
const { report, paths } = await runPluginCheck({
|
|
79
115
|
allowExecution,
|
|
80
116
|
capture,
|
|
81
117
|
configPath,
|
|
118
|
+
includeInspectorGaps,
|
|
82
119
|
mockSdk,
|
|
83
120
|
openclawPath,
|
|
84
121
|
outDir,
|
|
@@ -164,10 +201,12 @@ async function runCi(commandArgs) {
|
|
|
164
201
|
const mockSdk = readMockSdkFlag(commandArgs);
|
|
165
202
|
const allowExecution = readAllowExecutionFlag(commandArgs);
|
|
166
203
|
const ciOutputs = readCiOutputFlags(commandArgs, { defaultEnabled: true });
|
|
204
|
+
const includeInspectorGaps = commandArgs.includes("--include-inspector-gaps");
|
|
167
205
|
const { report, reportDir } = await runCiCompatibilityReport({
|
|
168
206
|
allowExecution,
|
|
169
207
|
capture,
|
|
170
208
|
configPath,
|
|
209
|
+
includeInspectorGaps,
|
|
171
210
|
mockSdk,
|
|
172
211
|
openclawPath,
|
|
173
212
|
outDir,
|
|
@@ -204,10 +243,19 @@ async function runCi(commandArgs) {
|
|
|
204
243
|
}
|
|
205
244
|
}
|
|
206
245
|
|
|
207
|
-
async function runCiCompatibilityReport({
|
|
246
|
+
async function runCiCompatibilityReport({
|
|
247
|
+
allowExecution,
|
|
248
|
+
capture,
|
|
249
|
+
configPath,
|
|
250
|
+
includeInspectorGaps,
|
|
251
|
+
mockSdk,
|
|
252
|
+
openclawPath,
|
|
253
|
+
outDir,
|
|
254
|
+
pluginRoot,
|
|
255
|
+
}) {
|
|
208
256
|
if (configPath) {
|
|
209
257
|
const config = await loadInspectorConfig(configPath, { cwd: pluginRoot });
|
|
210
|
-
const report = await inspectCompatibilityFixtureSet(config, { openclawPath });
|
|
258
|
+
const report = await inspectCompatibilityFixtureSet(config, { includeInspectorGaps, openclawPath });
|
|
211
259
|
await writeCompatibilityReport(report, { cwd: config.rootDir, outDir });
|
|
212
260
|
return {
|
|
213
261
|
report,
|
|
@@ -215,7 +263,15 @@ async function runCiCompatibilityReport({ allowExecution, capture, configPath, m
|
|
|
215
263
|
};
|
|
216
264
|
}
|
|
217
265
|
|
|
218
|
-
const { report } = await runPluginCheck({
|
|
266
|
+
const { report } = await runPluginCheck({
|
|
267
|
+
allowExecution,
|
|
268
|
+
capture,
|
|
269
|
+
includeInspectorGaps,
|
|
270
|
+
mockSdk,
|
|
271
|
+
openclawPath,
|
|
272
|
+
outDir,
|
|
273
|
+
pluginRoot,
|
|
274
|
+
});
|
|
219
275
|
return {
|
|
220
276
|
report,
|
|
221
277
|
reportDir: path.resolve(pluginRoot ?? process.cwd(), outDir),
|
|
@@ -326,6 +382,34 @@ function renderCiTextSummary(summary) {
|
|
|
326
382
|
].join("\n");
|
|
327
383
|
}
|
|
328
384
|
|
|
385
|
+
function renderBatchTextSummary(report, paths) {
|
|
386
|
+
const lines = [
|
|
387
|
+
"Plugin Inspector Batch",
|
|
388
|
+
`Plugins: ${report.summary.pluginCount}`,
|
|
389
|
+
`Plugins with errors: ${report.summary.pluginsWithErrors}`,
|
|
390
|
+
`Plugins with warnings: ${report.summary.pluginsWithWarnings}`,
|
|
391
|
+
`Finding codes: ${report.summary.findingCodeCount}`,
|
|
392
|
+
"",
|
|
393
|
+
"Reports:",
|
|
394
|
+
`- JSON: ${paths.jsonPath}`,
|
|
395
|
+
`- Markdown: ${paths.markdownPath}`,
|
|
396
|
+
];
|
|
397
|
+
const topFinding = report.findingFrequency[0];
|
|
398
|
+
if (topFinding) {
|
|
399
|
+
lines.push("", `Top finding: ${topFinding.code} (${topFinding.plugins} plugin(s))`);
|
|
400
|
+
}
|
|
401
|
+
return lines.join("\n");
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function readFirstPositional(args, valueFlags = new Set()) {
|
|
405
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
406
|
+
const arg = args[index];
|
|
407
|
+
if (!arg.startsWith("-")) return arg;
|
|
408
|
+
if (valueFlags.has(arg)) index += 1;
|
|
409
|
+
}
|
|
410
|
+
return undefined;
|
|
411
|
+
}
|
|
412
|
+
|
|
329
413
|
function initCommandSummary(result) {
|
|
330
414
|
return {
|
|
331
415
|
dryRun: result.dryRun,
|
|
@@ -353,16 +437,18 @@ function printHelp() {
|
|
|
353
437
|
|
|
354
438
|
Usage:
|
|
355
439
|
plugin-inspector
|
|
356
|
-
plugin-inspector check [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--allow-execute] [--json]
|
|
440
|
+
plugin-inspector check [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--allow-execute] [--include-inspector-gaps] [--json]
|
|
357
441
|
plugin-inspector config [--plugin-root <path>] [--config <path>] [--json]
|
|
358
442
|
plugin-inspector init [--plugin-root <path>] [--config <path>] [--ci] [--scripts] [--package-manager npm|pnpm|yarn|bun] [--dry-run] [--json] [--force]
|
|
359
443
|
plugin-inspector report --config <path> [--out <dir>] [--check] [--json]
|
|
444
|
+
plugin-inspector batch <folder> [--out <dir>] [--openclaw <path>] [--no-openclaw] [--concurrency <n>] [--keep-plugin-reports] [--include-inspector-gaps] [--check] [--json]
|
|
360
445
|
plugin-inspector inspect [--plugin-root <path>] [--config <path>] [--out <dir>] [--check] [--json] [--sarif [path]] [--junit [path]] [--allow-execute]
|
|
361
|
-
plugin-inspector ci [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--allow-execute] [--json] [--no-sarif] [--no-junit]
|
|
446
|
+
plugin-inspector ci [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--allow-execute] [--include-inspector-gaps] [--json] [--no-sarif] [--no-junit]
|
|
362
447
|
plugin-inspector capture <entrypoint> [--mock-sdk|--real-sdk] [--allow-execute] [--plugin-root <path>] [--output <path>]
|
|
363
448
|
|
|
364
449
|
Default check runs from the current plugin root and writes reports/ unless --out is set.
|
|
365
450
|
CI writes SARIF and JUnit artifacts by default; check/inspect can write them with --sarif and --junit.
|
|
366
451
|
Runtime capture is opt-in because it imports plugin code; use --runtime with --allow-execute or PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1.
|
|
452
|
+
Inspector coverage gaps are hidden by default; pass --include-inspector-gaps for maintainer coverage reports.
|
|
367
453
|
`);
|
|
368
454
|
}
|
package/src/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import * as runtimeReconciliationApi from "./runtime-reconciliation.js";
|
|
|
17
17
|
import * as syntheticEntrypointApi from "./synthetic-entrypoint.js";
|
|
18
18
|
import * as syntheticProbeSuiteApi from "./synthetic-probe-suite.js";
|
|
19
19
|
import * as syntheticProbesApi from "./synthetic-probes.js";
|
|
20
|
+
import * as batchApi from "./batch.js";
|
|
20
21
|
|
|
21
22
|
export const pluginRoot = Object.freeze({
|
|
22
23
|
loadConfig: pluginApi.loadPluginConfig,
|
|
@@ -26,6 +27,12 @@ export const pluginRoot = Object.freeze({
|
|
|
26
27
|
setup: pluginApi.setupPluginInspector,
|
|
27
28
|
});
|
|
28
29
|
|
|
30
|
+
export const batch = Object.freeze({
|
|
31
|
+
discoverPluginRoots: batchApi.discoverPluginRoots,
|
|
32
|
+
run: batchApi.runBatchAnalysis,
|
|
33
|
+
writeReport: batchApi.writeBatchReport,
|
|
34
|
+
});
|
|
35
|
+
|
|
29
36
|
export const fixtureSuites = Object.freeze({
|
|
30
37
|
loadConfig: configApi.loadInspectorConfig,
|
|
31
38
|
inspect: pluginApi.inspectCompatibilityFixtureSetConfig,
|
|
@@ -131,6 +138,11 @@ export const synthetic = Object.freeze({
|
|
|
131
138
|
defaultRegistrationArguments: syntheticProbesApi.defaultSyntheticRegistrationArguments,
|
|
132
139
|
});
|
|
133
140
|
|
|
141
|
+
export {
|
|
142
|
+
discoverPluginRoots,
|
|
143
|
+
runBatchAnalysis,
|
|
144
|
+
writeBatchReport,
|
|
145
|
+
} from "./batch.js";
|
|
134
146
|
export {
|
|
135
147
|
capturePluginEntrypoint,
|
|
136
148
|
buildFixtureSetColdImportReadiness,
|
package/src/inspector.js
CHANGED
|
@@ -37,6 +37,7 @@ export async function inspectCompatibilityFixtureSet(config, options = {}) {
|
|
|
37
37
|
failures,
|
|
38
38
|
generatedAt: options.generatedAt,
|
|
39
39
|
executionResults: options.executionResults,
|
|
40
|
+
includeInspectorGaps: options.includeInspectorGaps,
|
|
40
41
|
targetOpenClaw,
|
|
41
42
|
buildFixtureReport: ({ fixture, inspection }) =>
|
|
42
43
|
buildCompatibilityFixtureReport({
|
package/src/issues.js
CHANGED
|
@@ -333,6 +333,10 @@ export function classifyIssueFinding(finding, targetOpenClaw, metadata = {}) {
|
|
|
333
333
|
};
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
+
export function isInspectorGapFinding(finding, targetOpenClaw) {
|
|
337
|
+
return issueMetadata(finding, targetOpenClaw).issueClass === "inspector-gap";
|
|
338
|
+
}
|
|
339
|
+
|
|
336
340
|
export function summarizeIssueClasses(issues) {
|
|
337
341
|
const summary = {
|
|
338
342
|
"compat-gap": 0,
|
package/src/report.js
CHANGED
|
@@ -3,7 +3,7 @@ import { renderMarkdownTable, writeArtifacts, writeJsonMarkdownArtifacts } from
|
|
|
3
3
|
import { renderCompatibilityIssuesReport, renderCompatibilityMarkdownReport } from "./compatibility-report.js";
|
|
4
4
|
import { buildContractProbes } from "./contract-probes.js";
|
|
5
5
|
import { classifyCompatibilityFixture } from "./fixture-summary.js";
|
|
6
|
-
import { buildIssues, summarizeIssueClasses } from "./issues.js";
|
|
6
|
+
import { buildIssues, isInspectorGapFinding, summarizeIssueClasses } from "./issues.js";
|
|
7
7
|
import { sanitizeReportArtifact } from "./report-sanitizer.js";
|
|
8
8
|
import { applyRuntimeExecutionCoverage } from "./runtime-reconciliation.js";
|
|
9
9
|
|
|
@@ -141,18 +141,24 @@ export async function buildCompatibilityReport(options = {}) {
|
|
|
141
141
|
decisions,
|
|
142
142
|
});
|
|
143
143
|
|
|
144
|
+
const visibleWarnings = filterVisibleFindings(warnings, targetOpenClaw, options);
|
|
145
|
+
const visibleSuggestions = filterVisibleFindings(suggestions, targetOpenClaw, options);
|
|
144
146
|
const runtimeCoverage = applyRuntimeExecutionCoverage({
|
|
145
|
-
findings: [...
|
|
147
|
+
findings: [...visibleWarnings, ...visibleSuggestions],
|
|
146
148
|
executionResults: options.executionResults,
|
|
147
149
|
});
|
|
148
150
|
const issues = buildIssues({
|
|
149
151
|
breakages,
|
|
150
|
-
warnings,
|
|
151
|
-
suggestions,
|
|
152
|
+
warnings: visibleWarnings,
|
|
153
|
+
suggestions: visibleSuggestions,
|
|
152
154
|
targetOpenClaw,
|
|
153
155
|
idPrefix: options.issueIdPrefix,
|
|
154
156
|
});
|
|
155
|
-
const contractProbes = buildContractProbes({
|
|
157
|
+
const contractProbes = buildContractProbes({
|
|
158
|
+
warnings: visibleWarnings,
|
|
159
|
+
suggestions: visibleSuggestions,
|
|
160
|
+
fixtures: fixtureReports,
|
|
161
|
+
});
|
|
156
162
|
const issueSummary = summarizeIssueClasses(issues);
|
|
157
163
|
const openIssues = issues.filter((issue) => issue.status !== "runtime-covered");
|
|
158
164
|
const openIssueSummary = summarizeIssueClasses(openIssues);
|
|
@@ -165,8 +171,8 @@ export async function buildCompatibilityReport(options = {}) {
|
|
|
165
171
|
fixtureCount: fixtureReports.length,
|
|
166
172
|
highPriorityFixtures: fixtureReports.filter((fixture) => fixture.priority === "high").length,
|
|
167
173
|
breakageCount: breakages.length,
|
|
168
|
-
warningCount:
|
|
169
|
-
suggestionCount:
|
|
174
|
+
warningCount: visibleWarnings.length,
|
|
175
|
+
suggestionCount: visibleSuggestions.length,
|
|
170
176
|
decisionCount: decisions.length,
|
|
171
177
|
logCount: logs.length,
|
|
172
178
|
issueCount: issues.length,
|
|
@@ -190,8 +196,8 @@ export async function buildCompatibilityReport(options = {}) {
|
|
|
190
196
|
},
|
|
191
197
|
fixtures: fixtureReports,
|
|
192
198
|
breakages,
|
|
193
|
-
warnings,
|
|
194
|
-
suggestions,
|
|
199
|
+
warnings: visibleWarnings,
|
|
200
|
+
suggestions: visibleSuggestions,
|
|
195
201
|
issues,
|
|
196
202
|
contractProbes,
|
|
197
203
|
logs,
|
|
@@ -199,6 +205,13 @@ export async function buildCompatibilityReport(options = {}) {
|
|
|
199
205
|
};
|
|
200
206
|
}
|
|
201
207
|
|
|
208
|
+
function filterVisibleFindings(findings, targetOpenClaw, options) {
|
|
209
|
+
if (options.includeInspectorGaps === true) {
|
|
210
|
+
return findings;
|
|
211
|
+
}
|
|
212
|
+
return findings.filter((finding) => !isInspectorGapFinding(finding, targetOpenClaw));
|
|
213
|
+
}
|
|
214
|
+
|
|
202
215
|
export function classifyCompatRecordCoverage({ targetOpenClaw, findings, suggestions, logs, decisions }) {
|
|
203
216
|
if (targetOpenClaw.status !== "ok") {
|
|
204
217
|
logs.push({
|
package/src/sdk-mock.js
CHANGED
|
@@ -671,6 +671,24 @@ function resolveExistingSourcePath(target) {
|
|
|
671
671
|
` : ""}
|
|
672
672
|
function createMockValue(name) {
|
|
673
673
|
function fn(...args) {
|
|
674
|
+
if (name === "resolveDefaultAgentDir") {
|
|
675
|
+
return mockAgentDir();
|
|
676
|
+
}
|
|
677
|
+
if (name === "resolveAgentDir") {
|
|
678
|
+
return mockAgentDir(args[1]);
|
|
679
|
+
}
|
|
680
|
+
if (name === "resolveUserPath") {
|
|
681
|
+
return typeof args[0] === "string" ? args[0] : mockAgentDir();
|
|
682
|
+
}
|
|
683
|
+
if (name === "resolveAuthProfileOrder") {
|
|
684
|
+
return [];
|
|
685
|
+
}
|
|
686
|
+
if (name === "resolveWindowsSpawnProgram") {
|
|
687
|
+
return mockWindowsSpawnProgram(args[0]);
|
|
688
|
+
}
|
|
689
|
+
if (name === "materializeWindowsSpawnProgram") {
|
|
690
|
+
return mockWindowsSpawnInvocation(args[0], args[1]);
|
|
691
|
+
}
|
|
674
692
|
if (name === "resolvePreferredOpenClawTmpDir") {
|
|
675
693
|
return process.env.TMPDIR || "/tmp";
|
|
676
694
|
}
|
|
@@ -707,6 +725,80 @@ function createMockValue(name) {
|
|
|
707
725
|
});
|
|
708
726
|
}
|
|
709
727
|
|
|
728
|
+
function mockAgentDir(agentId = "main") {
|
|
729
|
+
const base = process.env.TMPDIR || process.env.TEMP || process.env.TMP || "/tmp";
|
|
730
|
+
const safeAgentId = String(agentId || "main").replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
731
|
+
return base.replace(/[\\/]+$/, "") + "/plugin-inspector-openclaw/agents/" + safeAgentId + "/agent";
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function mockWindowsSpawnProgram(params = {}) {
|
|
735
|
+
return {
|
|
736
|
+
command: typeof params.command === "string" && params.command.trim() ? params.command : process.execPath,
|
|
737
|
+
leadingArgv: [],
|
|
738
|
+
resolution: "mock",
|
|
739
|
+
packageName: typeof params.packageName === "string" ? params.packageName : undefined,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function mockWindowsSpawnInvocation(program = {}, argv = []) {
|
|
744
|
+
const command = typeof program.command === "string" && program.command.trim() ? program.command : process.execPath;
|
|
745
|
+
if (program.packageName === "@openai/codex") {
|
|
746
|
+
return {
|
|
747
|
+
command: process.execPath,
|
|
748
|
+
argv: ["-e", mockCodexAppServerScript()],
|
|
749
|
+
resolution: program.resolution ?? "mock",
|
|
750
|
+
windowsHide: true,
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
return {
|
|
754
|
+
command,
|
|
755
|
+
argv: [...(Array.isArray(program.leadingArgv) ? program.leadingArgv : []), ...(Array.isArray(argv) ? argv : [])],
|
|
756
|
+
resolution: program.resolution ?? "mock",
|
|
757
|
+
shell: program.shell,
|
|
758
|
+
windowsHide: program.windowsHide,
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function mockCodexAppServerScript() {
|
|
763
|
+
return [
|
|
764
|
+
"const readline = require('node:readline');",
|
|
765
|
+
"const rl = readline.createInterface({ input: process.stdin });",
|
|
766
|
+
"let idleTimer;",
|
|
767
|
+
"function scheduleIdleExit() {",
|
|
768
|
+
" if (idleTimer) clearTimeout(idleTimer);",
|
|
769
|
+
" idleTimer = setTimeout(() => process.exit(0), 1000);",
|
|
770
|
+
"}",
|
|
771
|
+
"function write(id, result) { process.stdout.write(JSON.stringify({ id, result }) + String.fromCharCode(10)); }",
|
|
772
|
+
"rl.on('line', (line) => {",
|
|
773
|
+
" let message;",
|
|
774
|
+
" try { message = JSON.parse(line); } catch { return; }",
|
|
775
|
+
" if (message.id === undefined || message.id === null) return;",
|
|
776
|
+
" switch (message.method) {",
|
|
777
|
+
" case 'initialize':",
|
|
778
|
+
" write(message.id, { userAgent: 'openclaw/999.0.0 (plugin-inspector mock)' });",
|
|
779
|
+
" break;",
|
|
780
|
+
" case 'model/list':",
|
|
781
|
+
" write(message.id, { data: [] });",
|
|
782
|
+
" break;",
|
|
783
|
+
" case 'thread/list':",
|
|
784
|
+
" case 'mcpServerStatus/list':",
|
|
785
|
+
" case 'skills/list':",
|
|
786
|
+
" write(message.id, { data: [] });",
|
|
787
|
+
" break;",
|
|
788
|
+
" case 'account/read':",
|
|
789
|
+
" write(message.id, null);",
|
|
790
|
+
" break;",
|
|
791
|
+
" case 'account/rateLimits/read':",
|
|
792
|
+
" write(message.id, null);",
|
|
793
|
+
" break;",
|
|
794
|
+
" default:",
|
|
795
|
+
" process.stdout.write(JSON.stringify({ id: message.id, error: { code: -32601, message: 'mock method not implemented' } }) + String.fromCharCode(10));",
|
|
796
|
+
" }",
|
|
797
|
+
" scheduleIdleExit();",
|
|
798
|
+
"});",
|
|
799
|
+
].join("\\n");
|
|
800
|
+
}
|
|
801
|
+
|
|
710
802
|
function createZNamespace() {
|
|
711
803
|
const namespace = {
|
|
712
804
|
any: () => createSchema(),
|