@openclaw/plugin-inspector 0.1.1 → 0.1.3
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 +25 -0
- package/README.md +82 -41
- package/examples/github-actions-plugin-inspector.yml +2 -2
- package/examples/plugin-inspector.config.json +3 -0
- package/package.json +1 -1
- package/src/advanced.js +10 -0
- package/src/api.js +14 -8
- package/src/capture-api.js +180 -12
- package/src/capture-cli.js +61 -0
- package/src/ci-summary.js +1 -1
- package/src/cli.js +138 -7
- package/src/config.js +16 -2
- package/src/contract-probes.js +5 -0
- package/src/fixture-summary.js +40 -16
- package/src/index.js +1 -0
- package/src/init.js +150 -0
- package/src/inspector.js +89 -14
- package/src/issues.js +8 -0
- package/src/mock-sdk-capture-runner.js +110 -17
- package/src/openclaw-target.js +27 -0
- package/src/report.js +1 -0
- package/src/runtime-capture-report.js +11 -1
- package/src/sdk-mock.js +1266 -13
- package/src/synthetic-probes-cli.js +101 -0
- package/src/synthetic-probes.js +153 -9
- package/src/workspace-plan.js +16 -4
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { mkdtemp, rm
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { register } from "node:module";
|
|
3
4
|
import os from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { pathToFileURL } from "node:url";
|
|
@@ -7,12 +8,16 @@ import { createCaptureApi } from "./capture-api.js";
|
|
|
7
8
|
import { createMockSdkPackage } from "./sdk-mock.js";
|
|
8
9
|
|
|
9
10
|
const options = JSON.parse(process.argv[2] ?? "{}");
|
|
11
|
+
let activeOutputCapture = null;
|
|
10
12
|
|
|
11
13
|
try {
|
|
12
14
|
const result = await run(options);
|
|
13
|
-
|
|
15
|
+
writeRunnerStdout(`${JSON.stringify(result, null, 2)}\n`);
|
|
14
16
|
} catch (error) {
|
|
15
|
-
|
|
17
|
+
if (error.failureClass) {
|
|
18
|
+
writeRunnerStderr(`[plugin-inspector:${error.failureClass}]\n`);
|
|
19
|
+
}
|
|
20
|
+
writeRunnerStderr(`${error.stack ?? error.message}\n`);
|
|
16
21
|
process.exitCode = 1;
|
|
17
22
|
}
|
|
18
23
|
|
|
@@ -22,37 +27,79 @@ async function run(options) {
|
|
|
22
27
|
const workspace = await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-mock-sdk-"));
|
|
23
28
|
|
|
24
29
|
try {
|
|
25
|
-
await createMockSdkPackage(workspace);
|
|
26
|
-
|
|
27
|
-
await
|
|
28
|
-
const linkedEntrypoint = path.join(linkedPluginRoot, path.relative(pluginRoot, entrypoint));
|
|
29
|
-
return await captureLinkedEntrypoint(linkedEntrypoint, options);
|
|
30
|
+
const { loaderPath } = await createMockSdkPackage(workspace, { pluginRoot });
|
|
31
|
+
register(pathToFileURL(loaderPath));
|
|
32
|
+
return await captureLinkedEntrypoint(entrypoint, options);
|
|
30
33
|
} finally {
|
|
31
34
|
await rm(workspace, { force: true, recursive: true });
|
|
32
35
|
}
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
async function captureLinkedEntrypoint(entrypoint, options) {
|
|
36
|
-
const
|
|
39
|
+
const outputCapture = installProcessOutputCapture();
|
|
40
|
+
activeOutputCapture = outputCapture;
|
|
41
|
+
|
|
42
|
+
let module;
|
|
43
|
+
try {
|
|
44
|
+
module = await import(pathToFileURL(entrypoint).href);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
await drainAsyncOutput();
|
|
47
|
+
throw capturePhaseError(error, "entrypoint-import-error");
|
|
48
|
+
}
|
|
37
49
|
const register = findRegisterExport(module);
|
|
38
50
|
|
|
39
51
|
if (!register) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
52
|
+
await drainAsyncOutput();
|
|
53
|
+
return withProcessOutput(
|
|
54
|
+
{
|
|
55
|
+
status: "no-register-export",
|
|
56
|
+
entrypoint: options.entrypoint,
|
|
57
|
+
mockSdk: true,
|
|
58
|
+
captured: [],
|
|
59
|
+
},
|
|
60
|
+
outputCapture,
|
|
61
|
+
);
|
|
46
62
|
}
|
|
47
63
|
|
|
48
64
|
const api = createCaptureApi(options.apiOptions);
|
|
49
|
-
|
|
50
|
-
|
|
65
|
+
try {
|
|
66
|
+
await register(api);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
await drainAsyncOutput();
|
|
69
|
+
throw capturePhaseError(error, "registration-execution-error");
|
|
70
|
+
}
|
|
71
|
+
await drainAsyncOutput();
|
|
72
|
+
|
|
73
|
+
const result = {
|
|
51
74
|
status: "captured",
|
|
52
75
|
entrypoint: options.entrypoint,
|
|
53
76
|
mockSdk: true,
|
|
54
77
|
captured: api.getCapturedContracts(),
|
|
55
78
|
};
|
|
79
|
+
if (options.apiOptions?.retainHandlers === true) {
|
|
80
|
+
result.retained = api.getRetainedContracts();
|
|
81
|
+
}
|
|
82
|
+
return withProcessOutput(result, outputCapture);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function withProcessOutput(result, outputCapture) {
|
|
86
|
+
const stdout = outputCapture.stdout();
|
|
87
|
+
const stderr = outputCapture.stderr();
|
|
88
|
+
if (stdout.length === 0 && stderr.length === 0) {
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
...result,
|
|
93
|
+
processOutput: {
|
|
94
|
+
stdout,
|
|
95
|
+
stderr,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function capturePhaseError(error, failureClass) {
|
|
101
|
+
error.failureClass = failureClass;
|
|
102
|
+
return error;
|
|
56
103
|
}
|
|
57
104
|
|
|
58
105
|
function findRegisterExport(module) {
|
|
@@ -67,3 +114,49 @@ function findRegisterExport(module) {
|
|
|
67
114
|
}
|
|
68
115
|
return null;
|
|
69
116
|
}
|
|
117
|
+
|
|
118
|
+
function installProcessOutputCapture() {
|
|
119
|
+
const stdoutChunks = [];
|
|
120
|
+
const stderrChunks = [];
|
|
121
|
+
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
122
|
+
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
123
|
+
|
|
124
|
+
process.stdout.write = (chunk, encoding, callback) => {
|
|
125
|
+
stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk));
|
|
126
|
+
invokeWriteCallback(encoding, callback);
|
|
127
|
+
return true;
|
|
128
|
+
};
|
|
129
|
+
process.stderr.write = (chunk, encoding, callback) => {
|
|
130
|
+
stderrChunks.push(Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk));
|
|
131
|
+
invokeWriteCallback(encoding, callback);
|
|
132
|
+
return true;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
originalStdoutWrite,
|
|
137
|
+
originalStderrWrite,
|
|
138
|
+
stdout: () => stdoutChunks.join(""),
|
|
139
|
+
stderr: () => stderrChunks.join(""),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function invokeWriteCallback(encoding, callback) {
|
|
144
|
+
if (typeof encoding === "function") {
|
|
145
|
+
encoding();
|
|
146
|
+
} else if (typeof callback === "function") {
|
|
147
|
+
callback();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function drainAsyncOutput() {
|
|
152
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
153
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function writeRunnerStdout(text) {
|
|
157
|
+
(activeOutputCapture?.originalStdoutWrite ?? process.stdout.write.bind(process.stdout))(text);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function writeRunnerStderr(text) {
|
|
161
|
+
(activeOutputCapture?.originalStderrWrite ?? process.stderr.write.bind(process.stderr))(text);
|
|
162
|
+
}
|
package/src/openclaw-target.js
CHANGED
|
@@ -31,6 +31,7 @@ export async function readOpenClawTargetSurface(options = {}) {
|
|
|
31
31
|
const apiBuilderPath = path.join(resolvedPath, "src/plugins/api-builder.ts");
|
|
32
32
|
const capturedRegistrationPath = path.join(resolvedPath, "src/plugins/captured-registration.ts");
|
|
33
33
|
const manifestTypesPath = path.join(resolvedPath, "src/plugins/manifest.ts");
|
|
34
|
+
const pluginSdkEntrypointsPath = path.join(resolvedPath, "src/plugin-sdk/entrypoints.ts");
|
|
34
35
|
const packagePath = path.join(resolvedPath, "package.json");
|
|
35
36
|
|
|
36
37
|
const registrySource = await readFile(registryPath, "utf8");
|
|
@@ -48,6 +49,18 @@ export async function readOpenClawTargetSurface(options = {}) {
|
|
|
48
49
|
const sdkExports = existsSync(packagePath)
|
|
49
50
|
? parsePluginSdkExports(JSON.parse(await readFile(packagePath, "utf8")))
|
|
50
51
|
: [];
|
|
52
|
+
const pluginSdkEntrypointsSource = existsSync(pluginSdkEntrypointsPath)
|
|
53
|
+
? await readFile(pluginSdkEntrypointsPath, "utf8")
|
|
54
|
+
: "";
|
|
55
|
+
const reservedSdkExports = pluginSdkEntrypointsSource
|
|
56
|
+
? parsePluginSdkEntrypointSpecifiers(pluginSdkEntrypointsSource, "reservedBundledPluginSdkEntrypoints")
|
|
57
|
+
: [];
|
|
58
|
+
const supportedFacadeSdkExports = pluginSdkEntrypointsSource
|
|
59
|
+
? parsePluginSdkEntrypointSpecifiers(pluginSdkEntrypointsSource, "supportedBundledFacadeSdkEntrypoints")
|
|
60
|
+
: [];
|
|
61
|
+
const publicPluginOwnedSdkExports = pluginSdkEntrypointsSource
|
|
62
|
+
? parsePluginSdkEntrypointSpecifiers(pluginSdkEntrypointsSource, "publicPluginOwnedSdkEntrypoints")
|
|
63
|
+
: [];
|
|
51
64
|
|
|
52
65
|
return {
|
|
53
66
|
configuredPath: requestedPath,
|
|
@@ -69,6 +82,13 @@ export async function readOpenClawTargetSurface(options = {}) {
|
|
|
69
82
|
packagePath: existsSync(packagePath) ? relativePath(rootDir, packagePath) : null,
|
|
70
83
|
sdkExportCount: sdkExports.length,
|
|
71
84
|
sdkExports,
|
|
85
|
+
pluginSdkEntrypointsPath: existsSync(pluginSdkEntrypointsPath)
|
|
86
|
+
? relativePath(rootDir, pluginSdkEntrypointsPath)
|
|
87
|
+
: null,
|
|
88
|
+
reservedSdkExportCount: reservedSdkExports.length,
|
|
89
|
+
reservedSdkExports,
|
|
90
|
+
supportedFacadeSdkExports,
|
|
91
|
+
publicPluginOwnedSdkExports,
|
|
72
92
|
manifestTypesPath: existsSync(manifestTypesPath) ? relativePath(rootDir, manifestTypesPath) : null,
|
|
73
93
|
manifestFieldCount: manifestFields.length,
|
|
74
94
|
manifestFields,
|
|
@@ -149,11 +169,18 @@ function emptyTargetSurface({ configuredPath, searchedPaths = undefined, status
|
|
|
149
169
|
apiRegistrars: [],
|
|
150
170
|
capturedRegistrars: [],
|
|
151
171
|
sdkExports: [],
|
|
172
|
+
reservedSdkExports: [],
|
|
173
|
+
supportedFacadeSdkExports: [],
|
|
174
|
+
publicPluginOwnedSdkExports: [],
|
|
152
175
|
manifestFields: [],
|
|
153
176
|
manifestContractFields: [],
|
|
154
177
|
};
|
|
155
178
|
}
|
|
156
179
|
|
|
180
|
+
export function parsePluginSdkEntrypointSpecifiers(source, exportName) {
|
|
181
|
+
return parseExportedStringArray(source, exportName).map((entrypoint) => `openclaw/plugin-sdk/${entrypoint}`).sort();
|
|
182
|
+
}
|
|
183
|
+
|
|
157
184
|
function parseCapturedRegistrars(source) {
|
|
158
185
|
return unique([...source.matchAll(/^\s*(register[A-Za-z0-9]+)\s*\(/gm)].map((match) => match[1])).sort();
|
|
159
186
|
}
|
package/src/report.js
CHANGED
|
@@ -160,6 +160,7 @@ export async function buildCompatibilityReport(options = {}) {
|
|
|
160
160
|
warningCount: warnings.length,
|
|
161
161
|
suggestionCount: suggestions.length,
|
|
162
162
|
decisionCount: decisions.length,
|
|
163
|
+
logCount: logs.length,
|
|
163
164
|
issueCount: issues.length,
|
|
164
165
|
p0IssueCount: issues.filter((issue) => issue.severity === "P0").length,
|
|
165
166
|
p1IssueCount: issues.filter((issue) => issue.severity === "P1").length,
|
|
@@ -71,13 +71,20 @@ export function renderRuntimeCaptureMarkdown(captureReport, options = {}) {
|
|
|
71
71
|
result.fixture,
|
|
72
72
|
result.status,
|
|
73
73
|
result.entrypoint,
|
|
74
|
-
(result.captured ?? []).map((item) => `${item.kind}:${item.name}`).join(", ") || result
|
|
74
|
+
(result.captured ?? []).map((item) => `${item.kind}:${item.name}`).join(", ") || formatCaptureError(result),
|
|
75
75
|
]),
|
|
76
76
|
["Fixture", "Status", "Entrypoint", "Captured"],
|
|
77
77
|
),
|
|
78
78
|
].join("\n");
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
function formatCaptureError(result) {
|
|
82
|
+
if (!result.error) {
|
|
83
|
+
return "-";
|
|
84
|
+
}
|
|
85
|
+
return result.failureClass ? `${result.failureClass}: ${result.error}` : result.error;
|
|
86
|
+
}
|
|
87
|
+
|
|
81
88
|
function captureTargets(fixture, rootDir) {
|
|
82
89
|
return fixture.packages.flatMap((packageSummary) => {
|
|
83
90
|
const packageRoot = path.dirname(path.resolve(rootDir, packageSummary.path));
|
|
@@ -124,6 +131,9 @@ async function captureTarget(target, options) {
|
|
|
124
131
|
packagePath: target.packagePath,
|
|
125
132
|
entrypoint: target.entrypoint.relativePath,
|
|
126
133
|
error: error.message,
|
|
134
|
+
...(error.failureClass ? { failureClass: error.failureClass } : {}),
|
|
135
|
+
...(error.missingExport ? { missingExport: error.missingExport } : {}),
|
|
136
|
+
...(error.missingModule ? { missingModule: error.missingModule } : {}),
|
|
127
137
|
captured: [],
|
|
128
138
|
};
|
|
129
139
|
}
|